Flutter State Management Examples - redux

In a complex app, sometimes a Global Variable 'attached' to a widget, can be changed by some 'EXTERNAL EVENT' such as (1) A timer that run in another thread, or (2) socket.io server emit event (3) Others ......
Let's call this global variable gintCount and the app has 3 pages, namely:
Page 1: A 'Dynamic' page that need to display the latest value of gintCount.
Page 2: Another 'Dynamic' page that need to display the latest value of gintCount, with a Text Input Field.
Page 3: A 'Static' page that do nothing when gintCount changes.
Suppose the user is doing something in Page 1 or Page 2, when and where should we 'Refresh' the page to display the latest value that may/might be changed by EXTERNAL event?
I read the other Q&A in Stack Overflow and it is said that there are 4 ways for the State Management of Flutter, they are namely:
Using setState
Using ScopedModal
Using Rxdart with BLoC
Using Redux
Since I'm a newbie in Flutter, I am completely lost in 2 to 4, so I've build an app using no. 1, i.e. setState. to demonstrate how we can manage states in flutter. And I hope, in the future, I am able to (or somebody else) provide answers by using no. 2 to 4.
Let's take a look at the running app in the following animation gif:
Screen Shot Gif Link
As you can see in the gif, there is a Global Counter in Page 1 and Page 2, and Page 3 is a static Page.
Let me explain how I did it:
The complete source code can be found at the following address:
https://github.com/lhcdims/statemanagement01
There are 7 dart files, they are namely:
gv.dart: Stores all the Global Variables.
ScreenVariable.dart: Get the height/width/font size of screen etc. You may ignore this.
BottomBar.dart: The bottom navigation bar.
main.dart: The main program.
Page1.dart: Page 1 widget.
Page2.dart: Page 2 widget.
Page3.dart: Page 3 widget.
Let's first take a look at gv.dart:
import 'package:flutter/material.dart';
class gv {
static var gstrCurPage = 'page1'; // gstrCurPage stores the Current Page to be loaded
static var gintBottomIndex = 0; // Which Tab is selected in the Bottom Navigator Bar
static var gintCount = 0; // The Global Counter
static var gintCountLast = 0; // Check whether Global Counter has been changed
static var gintPage1Counter = 0; // No. of initState called in Page 1
static var gintPage2Counter = 0; // No. of initState called in Page 2
static var gintPage3Counter = 0; // No. of initState called in Page 3
static bool gbolNavigatorBeingPushed = false; // Since Navigator.push will called the initState TWICE, this variable make sure the initState only be called once effectively!
static var gctlPage2Text = TextEditingController(); // Controller for the text field in Page 2
}
How did I simulate an External Event that changes the global variable gv.gintCount?
Ok, I create a thread in main.dart that runs the timer 'funTimerExternal', and increment gv.gintCount every second!
Now, let's take a look at main.dart:
// This example tries to demonstrate how to maintain the state of widgets when
// variables are changed by External Event
// e.g. by a timer of another thread, or by socket.io
// This example uses setState and a timer to maintain States of Multiple Pages
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import "package:threading/threading.dart";
import 'gv.dart';
import 'Page1.dart';
import 'Page2.dart';
import 'Page3.dart';
import 'ScreenVariables.dart';
void main() { // Main Program
var threadExternal = new Thread(funTimerExternal); // Create a new thread to simulate an External Event that changes a global variable defined in gv.dart
threadExternal.start();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp])
.then((_) {
sv.Init(); // Init Screen Variables
runApp(new MyApp()); // Run MainApp
});
}
void funTimerExternal() async { // The following function simulates an External Event e.g. a global variable is changed by socket.io and see how all widgets react with this global variable
while (true) {
await Thread.sleep(1000);
gv.gintCount += 1;
}
}
class MyApp extends StatefulWidget { // Main App
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
initState() {
super.initState();
var threadTimerDefault = new Thread(funTimerDefault); // *** Set funTimerDefault, to listen to change of Vars ***
threadTimerDefault.start();
}
void funTimerDefault() async {
while (true) {
await Thread.sleep(500); // Allow this thread to run each XXX milliseconds
if (gv.gintCount != gv.gintCountLast) { // Check any changes need to setState here, if anything changes, setState according to gv.gstrCurPage
gv.gintCountLast = gv.gintCount;
switch (gv.gstrCurPage) {
case 'page1':
setState(() {}); // Page 1: Refresh Page
break;
case 'page2':
setState(() {}); // Page 2: Refresh Page
break;
default: // Page 3: Do Nothing, since Page 3 is static
break;
}
}
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false, // Disable Show Debug
home: MainBody(),
);
}
}
class MainBody extends StatefulWidget {
#override
_MainBodyState createState() => _MainBodyState();
}
class _MainBodyState extends State<MainBody> {
#override
initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
switch (gv.gstrCurPage) { // Here Return Page According to gv.gstrCurPage
case 'page1':
return ClsPage1();
break;
case 'page2':
return ClsPage2();
break;
default:
return ClsPage3();
break;
}
return ClsPage1(); // The following code will never be run, to avoid warning only
}
}
As you can see, I use another timer 'funTimerDefault' to keep track of changes in gv.gintCount, and determine whether setState should be called every XXX milliseconds. (XXX is currently set at 500)
I know, this is stupid!
How can I create similar examples by using ScopedModal, or Rxdart with BLoC, or Redux?
Before anyone provides any answers, please bear in mind that the Global Variable gintCount, is not changed by ANY USER INTERACTION, but an EXTERNAL EVENT that IS NOT PART OF ANY WIDGETS. For example, you can regard this app as:
A CHAT app, that 'gintCount' is a message sent to you by someone else thru socket.io server. Or,
A Multi-Player On-line Game, that 'gintCount' is the position of another player in YOUR SCREEN, which is controlled by that player using another Mobile Phone!

For your need, you should definitely look more into the architectures available, that you talked about.
For example, REDUX matches exactly what you need to solve your issue.
I can only advise you to take a look at this presentation of REDUX :
https://www.youtube.com/watch?v=zKXz3pUkw9A
It is very understandable even for newbies of this pattern (which I was not so long ago).
When you've done that, take a look at http://fluttersamples.com/
This website contains example projects for a dozen of different patterns. That may help you get started

I've rewritten the example using Redux, let's take a look at the screen cap:
As you can see, there are 2 counters in Page 1, the variables are stored in gv.dart
In gv.dart (The dart file that stores all Global Variables), I created a 'Store':
import 'package:flutter/material.dart';
import 'package:redux/redux.dart';
import 'dart:convert';
enum Actions { Increment } // The reducer, which takes the previous count and increments it in response to an Increment action.
int counterReducer(int intSomeInteger, dynamic action) {
if (action == Actions.Increment) {
// print('Store Incremented: ' + (intSomeInteger + 1).toString());
return intSomeInteger + 1;
}
return intSomeInteger;
}
class gv {
static Store<int> storeState = new Store<int>(counterReducer, initialState: 0);
static var gstrCurPage = 'page1'; // gstrCurPage stores the Current Page to be loaded
static var gintBottomIndex = 0; // Which Tab is selected in the Bottom Navigator Bar
static var gintGlobal1 = 0; // Global Counter 1
static var gintGlobal2 = 0; // Global Counter 2
static var gintPage1Counter = 0; // No. of initState called in Page 1
static var gintPage2Counter = 0; // No. of initState called in Page 2
static var gintPage3Counter = 0; // No. of initState called in Page 3
static bool gbolNavigatorBeingPushed = false; // Since Navigator.push will called the initState TWICE, this variable make sure the initState only be called once effectively!
static var gctlPage2Text = TextEditingController(); // Controller for the text field in Page 2
}
Again, in main.dart, I created another thread 'funTimerExternal' to simulate an 'External Event' that some global variables are changed by, say, socket.io server emit event.
At the end of 'funTimerExternal', after some variables are changed, I called:
gv.storeState.dispatch(Actions.Increment);
to change the state of Page1 OR Page2, IF AND ONLY IF the user is navigating Page 1 or Page 2. (i.e. do nothing when user is navigating Page 3)
main.dart :
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:threading/threading.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
import 'gv.dart';
import 'Page1.dart';
import 'Page2.dart';
import 'Page3.dart';
import 'ScreenVariables.dart';
void main() { // Main Program
var threadExternal = new Thread(
funTimerExternal); // Create a new thread to simulate an External Event that changes a global variable defined in gv.dart
threadExternal.start();
SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp])
.then((_) {
sv.Init(); // Init Screen Variables
runApp(new MyApp()); // Run MainApp
});
}
void funTimerExternal() async { // The following function simulates an External Event e.g. a global variable is changed by socket.io and see how all widgets react with this global variable
while (true) {
await Thread.sleep(1000);
gv.gintGlobal1 += 1;
gv.gintGlobal2 = (gv.gintGlobal1 / 2).toInt();
gv.storeState.dispatch(Actions.Increment);
}
}
class MyApp extends StatefulWidget { // Main App
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return StoreProvider(
store: gv.storeState,
child: MaterialApp(
debugShowCheckedModeBanner: false, // Disable Show Debug
home: MainBody(),
),
);
}
}
class MainBody extends StatelessWidget {
#override
Widget build(BuildContext context) {
switch (gv.gstrCurPage) {
// Here Return Page According to gv.gstrCurPage
case 'page1':
gv.gintPage1Counter += 1;
return StoreConnector<int, int>(
builder: (BuildContext context, int intTemp) {
return new ClsPage1(intTemp);
}, converter: (Store<int> sintTemp) {
return sintTemp.state;
},);
break;
case 'page2':
gv.gintPage2Counter += 1;
return StoreConnector<int, int>(
builder: (BuildContext context, int intTemp) {
return new ClsPage2(intTemp);
}, converter: (Store<int> sintTemp) {
return sintTemp.state;
},);
break;
default:
return ClsPage3();
break;
}
}
}
Unlike the example provided on the web, the 'Store' is not declared inside main.dart, but inside another dart file gv.dart. i.e. I separated the UI and data!
The complete example can be found here:
https://github.com/lhcdims/statemanagement02
Thanks again for the help of Miiite and shadowsheep.

Related

Navigation Drawer: how make fragments persistent (keep alive) while switching (not rotating)

With Fragment:setRetainInstance(true); the fragment is not re-instantiated on a phones orientation change.
And of course i want my fragments to be kept alive while switching from one fragment to another.
But the Android Studio 4 provides a wizard-template with only
DrawerLayout drawer = findViewById(R.id.drawer_layout);
NavigationView navigationView = findViewById(R.id.nav_view);
// Passing each menu ID as a set of Ids because each
// menu should be considered as top level destinations.
mAppBarConfiguration = new AppBarConfiguration.Builder(
R.id.nav_home, R.id.nav_gallery, R.id.nav_slideshow)
.setDrawerLayout(drawer)
.build();
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
NavigationUI.setupActionBarWithNavController(this, navController, mAppBarConfiguration);
NavigationUI.setupWithNavController(navigationView, navController);
From hours of debugging and searching the net if think it would need to inherent from the class FragmentNavigator so i can overwrite FragmentNavigator:naviagte where a new fragment gets created via final Fragment frag = instantiateFragment(.. and then is added with ft.replace(mContainerId, frag);
So i could find my old fragment and use ftNew.show and ftOld.hide instead.
Of course this is a stupid idea, because this navigate method is full of other internal stuff.
And i have no idea where that FrameNavigator is created.
I can retrieve it in the MainActivity:OnCreate with
NavigatorProvider navProvider = navController.getNavigatorProvider ();
Navigator<NavDestination> navigator = navProvider.getNavigator("fragment");
But at that time i could only replace it with my derived version. And there is no replaceNavigtor method but only a addNavigator method, which is called where ?
And anyways this all will be far to complicated and therefore error prone.
Why is there no simple option to keep my fragments alive :-(
In older Wizard-Templates there was the possibility of
#Override
public void onNavigationDrawerItemSelected(int position) {
Fragment fragment;
switch (position) {
case 1:
fragment = fragment1;
break;
case 2:
fragment = fragment2;
break;
case 3:
fragment = fragment3;
break;
}
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
if(mCurrentFragment == null) {
ft.add(R.id.container, fragment).commit();
mCurrentFragment = fragment;
} else if(fragment.isAdded()) {
ft.hide(mCurrentFragment).show(fragment).commit();
} else {
ft.hide(mCurrentFragment).add(R.id.container, fragment).commit();
}
mCurrentFragment = fragment;
}
but i have no idea how to do this with the Android 4.0 template where my MainActivity is only derived as:
public class MainActivity extends AppCompatActivity {
private AppBarConfiguration mAppBarConfiguration;
Ideas welcome :'(
Hi there & sorry for my late answer! I had a similar problem with navigation drawers and navigation component. I tried around a little and found a working solution, which might be helpful for others too.
The key is the usage of a custom FragmentFactory in the FragmentManager of the MainActivity. See the code for this below:
public class StaticFragmentFactory extends FragmentFactory {
private myNavHostFragment1 tripNavHostFragment;
private myNavHostFragment2 settingsNavHostFragment;
#NonNull
#Override
public Fragment instantiate(#NonNull ClassLoader classLoader, #NonNull String className) {
if (MyNavHostFragment1.class.getName().equals(className)) {
if (this.myNavHostFragment1 == null) {
this.myNavHostFragment1 = new MyNavHostFragment1();
}
return this.myNavHostFragment1 ;
} else if (MyNavHostFragment2.class.getName().equals(className)) {
if (this.myNavHostFragment2 == null) {
this.myNavHostFragment2 = new MyNavHostFragment2();
}
return this.myNavHostFragment2;
}
return super.instantiate(classLoader, className);
}
}
The FragmentFactory survives the navigation between different fragments using the NavigationComponent of AndroidX. To keep the fragments alive, the FragmentFactory stores an instance of the fragments which should survive and returns this instance if this is not null. You can find a similar pattern when using a singleton pattern in classes.
You have to register the FragmentFactory in the corresponding activity by calling
this.getSupportFragmentManager().setFragmentFactory(new StaticFragmentFactory())
Please note also that I'm using nesten fragments here, so one toplevel fragment (called NavHostFragmen here) contains multiple child fragments. All fragments are using the same FragmentFactory of their parent fragments. The custom FragmentFactory above returns the result of the super class method, when the fragment to be instantiated is not known to keep alive.

How to disable button in MaterialAlertDialogBuilder

How can I disable button in MaterialAlertDialogBuilder?
I want to make similar functionality like in this screenshot:
enter image description here
I wrote the following code (dialog contains EditText where user should input his favorite food name).
final MaterialAlertDialogBuilder dialogEnterDishName = new MaterialAlertDialogBuilder(context);
//...
final EditText editTextEnterDishName = new EditText(context);
dialogEnterDishName.setView(editTextEnterDishName);
dialogEnterDishName.setPositiveButton(getString(R.string.dialog_enter_dish_name_positive_button), new DialogInterface.OnClickListener() {
#Override
public void onClick(DialogInterface dialog, int which) {
if (!editTextEnterDishName.getText().toString().equals(""))
//...
else {
//TODO Make posititve button disabled until the user enters any character
}
}
});
//...
dialogEnterDishName.show();
}
I already knew, that class AlertDialog (MaterialAlertDialogBuilder extends AlertDialog.Builder) have a method public Button getButton(int whichButton), but I can't use it in MaterialAlertDialogBuilder.
Please, help!
Make sure that you are calling getButton() function after you inflate your AlertDialog (through .show() call). If you are doing it other way around there is no button to get.
In order to enable button back you can use TextWatcher. More details here: Android TextWatcher.afterTextChanged vs TextWatcher.onTextChanged
val customLayout = LayoutInflater.from(context).inflate(R.layout.your_alert_dialog, null, false)
val dialog = MaterialAlertDialogBuilder(context)
.setTitle("Provide name")
.setView(customLayout)
.setNeutralButton("Cancel") { dialog, _ -> dialog.dismiss() }
.setPositiveButton("Confirm") { _, _ -> }
.create()
dialog.show()
dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false

Rebuild stateful widget on update of other stateful widget in Flutter

I have two stateful widgets.
One I'll call the parent, because it's producing a list (of map markers) that is being passed to the second widget (the child, a map, in a different dart file).
I have a default list of markers that is updated in an async function in the parent widget.
Despite using setState() to update the list, the child widget (map) is unaffected by this update.
Is there any way I can await before passing data to a widget in the build method?
class ParentWidget extends StatefulWidget{
.............
}
class _ParentWidgetState extends State<ParentWidget> {
List markers = [LatLng(0,0)];
#override
void initState() {
super.initState();
updateMarkers();
}
updateMarkers () async {
updatedMarkers = await getUpdatedMarkers()
setState((){
markers = updatedMarkers
});
# # print(updatedMarkers)
# # ^^ prints the correct data that I want to pass in MyMap
}
#override
Widget build(BuildContext context) {
return Scaffold(
............
body: MyMap(markers: markers);
# # The original markers ([LatLng(0,0)]) is the only data getting passed to MyMap
# # How can I get the updatedMarkers passed to the MyMap widget???????
}
I think this video might be your answer: https://www.youtube.com/watch?v=d_m5csmrf7I
He has two StatefulWidgets (Slider and Chart). By using a Provider, he changesthe Chart data when the slider moves.

How to trigger an event in a JavaFX Application (Stage) when some keys are pressed on the KeyBoard?

I'm trying to setup a backdoor on an application which I'm working on.
I want to load a new Window when the user presses "CTRL + ALT + F12".
This is what I have tried so far though it is terribly bad.
//packages
import java.awt.EventQueue;
import java.awt.KeyEventDispatcher;
import java.awt.KeyboardFocusManager;
//...
private void setupBackPass(){
HashMap<KeyStroke, Action> actionMap = new HashMap<KeyStroke, Action>();
KeyStroke key1 = KeyStroke.getKeyStroke(KeyEvent.ALT, KeyEvent.CTRL_DOWN_MASK);
actionMap.put(key1, new AbstractAction("action1") {
#Override
public void actionPerformed(ActionEvent e) {
System.out.println("Ctrl-ALT pressed: " +e);
}
});
KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager();
kfm.addKeyEventDispatcher(new KeyEventDispatcher() {
#Override
public boolean dispatchKeyEvent(KeyEvent e) {
KeyStroke keyStroke = KeyStroke.getKeyStrokeForEvent(e);
if(actionMap.containsKey(keyStroke)){
final Action a = actionMap.get(keyStroke);
final ActionEvent ae = new ActionEvent(e.getSource(), e.getID(), null);
EventQueue.invokeLater(new Runnable() {
#Override
public void run() {
}
});
return false;
}
});
});
}
Am not sure how to do it but I would like it that if the keys are pressed then a Super admin window should be opened.
From the code you posted, it looks like you are using Swing and not JavaFX. It also looks like you are trying to use Key Bindings. As explained in that link, you need to modify both the input map and the action map. Also, you don't create an action map, you use the existing one. Again, that is explained in the page that I provided a link to.

Getting JavaFX WebEngine Interpreted Document

Calling the getDocument() method on the WebEngine object for me only returns the source retrieved from the server, without the JavaScript being executed (there are still elements). This is the kind of source you would see if you used "View Source" in Chrome. How do I retrieve the interpreted source with the JavaScript already run?
public Browser() {
WebView browser = new WebView();
final WebEngine webEngine = browser.getEngine();
webEngine.load("*******************************");
webEngine.getLoadWorker().stateProperty().addListener(new ChangeListener<State>() {
#Override
public void changed(ObservableValue<? extends State> ov, State oldState, State newState) {
if (newState == State.SUCCEEDED) {
Document doc = webEngine.getDocument();
printDocument(doc);
}
}
});
}
This works as expected for me. In this example, the div contains a text node with the text that is set by the Javascript function:
import javafx.application.Application;
import javafx.concurrent.Worker;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.web.WebView;
import javafx.stage.Stage;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
public class WebViewOnLoadExample extends Application {
#Override
public void start(Stage primaryStage) {
WebView webView = new WebView();
webView.getEngine()
.getLoadWorker()
.stateProperty()
.addListener((obs, oldState, newState) -> {
if (newState == Worker.State.SUCCEEDED) {
Document doc = webView.getEngine().getDocument();
showNodeContent(doc, 0);
}
});
BorderPane root = new BorderPane(webView);
primaryStage.setScene(new Scene(root, 600, 400));
primaryStage.show();
webView.getEngine().loadContent("<html>"
+"<head><script>"
+"function setText() {"
+" document.getElementById(\"target\").appendChild(document.createTextNode(\"Hello World\"));"
+"}"
+"</script></head>"
+"<body onload='setText()'>"
+"<div id='target'></div></body></html>");
}
private void showNodeContent(Node n, int depth) {
for (int i=0; i<depth; i++) {
System.out.print(" ");
}
System.out.println(n.getNodeName()+":"+n.getNodeValue());
NodeList children = n.getChildNodes() ;
for (int i=0; i<children.getLength(); i++) {
showNodeContent(children.item(i), depth+1);
}
}
public static void main(String[] args) {
launch(args);
}
}
The problem you are facing is the following: the LoadWorker's state is set to SUCCEEDED before JavaScript is done running. JavaScript does in fact run (as shown in #James_D's reply) but there is no callback to signal when it finishes. AFAIK, there is no reliable way to detect when the WebEngine is done executing JS.
What you could do as a workaround is play a PauseTransition after the state changes to SUCCEEDED, which can be abused to act like a sleep on the JavaFX thread (JS is executed in the background thread that also loads the Document, so JS will not pause). However, sleeping (to wait for JS to finish) is inherently a violation of JavaFX's core principle never to block the UI thread. On top of that, waiting for a period of time does not guarantee that JS is done executing before that period passes.
I've faced the same problem and I have not found a decent solution. Let me know if you do!
I'm not sure if I'm getting your question right, but if you are looking for a way go print the visible content of the web you are loading, getting the DocumentElement from Document will allow you to dive into its structure and filter what you need.
This method will print the content of the desired tags:
private void printElement(Element el, int level){
NodeList childNodes = el.getChildNodes();
for(int j=0; j<level; j++) System.out.print("-");
System.out.print("tag: "+el.getNodeName());
if(el.getNodeName().equals("A")){
System.out.print(", content: "+el.getTextContent());
}
System.out.println("");
for(int i=0; i<childNodes.getLength(); i++){
Node item = childNodes.item(i);
if(item instanceof Element){
printElement((Element)item, level++);
}
}
}
so once you've loaded the URL, just call it:
if(newState==State.SUCCEEDED){
Document doc = webEngine.getDocument();
Element el = doc.getDocumentElement();
printElement(el,0);
}
This will print all the DOM tags with their level of indentation, and for the tag specified, it will print also the content. In this case, with the tag "A" it will print the content of all the links.
I'm not sure if this will help. Please clarify your question otherwise.

Resources