Flutter State Management Examples

2019-06-14 21:00发布

问题:

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:

  1. Page 1: A 'Dynamic' page that need to display the latest value of gintCount.
  2. Page 2: Another 'Dynamic' page that need to display the latest value of gintCount, with a Text Input Field.
  3. 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:

  1. Using setState
  2. Using ScopedModal
  3. Using Rxdart with BLoC
  4. 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:

  1. gv.dart: Stores all the Global Variables.
  2. ScreenVariable.dart: Get the height/width/font size of screen etc. You may ignore this.
  3. BottomBar.dart: The bottom navigation bar.
  4. main.dart: The main program.
  5. Page1.dart: Page 1 widget.
  6. Page2.dart: Page 2 widget.
  7. 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:

  1. A CHAT app, that 'gintCount' is a message sent to you by someone else thru socket.io server. Or,

  2. 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!

回答1:

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



回答2:

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.