1. 程式人生 > Android開發 >Flutter-狀態管理

Flutter-狀態管理

一、什麼是狀態管理

大到整個app的狀態,使用者使用app是登入狀態,還是遊客狀態;小到一個按鈕的狀態,按鈕是點選選中狀態還是未點選狀態等等,這些都是狀態管理。

二、指令式程式設計和宣告式程式設計狀態管理的區別

  • iOS是如何管理狀態的,一般都是獲取這個控制元件然後設定你想要的狀態
  • 當你的 Flutter 應用的狀態發生改變時(例如,使用者在設定介面中點選了一個開關選項)你改變了狀態,這將會觸發使用者介面的重繪。去改變使用者介面本身是沒有必要的(例如 widget.setText )—你改變了狀態,那麼使用者介面將重新構建。

三、狀態管理中的宣告式程式設計思維

Flutter 應用是 宣告式 的,這也就意味著 Flutter 構建的使用者介面就是應用的當前狀態。

一旦你的介面狀態發生改變,就會觸發介面的重新繪製,繪製出你想要的介面,而不是像iOS的OC語言那樣去獲取需要改變狀態的控制元件,然後修改它

四、短時 (ephemeral) 和應用 (app) 狀態的區別

Flutter中的狀態管理又分為短時狀態和應用狀態。

  • 短時狀態,就是在單個頁面需要保持的狀態,比如頁面資料載入到了第幾頁,關注按鈕是已關注還是未關注等,都是在單個頁面需要保持的狀態。widget樹中其他部分不需要訪問這種狀態。不需要去序列化這種狀態,這種狀態也不會以複雜的方式改變。換句話說,不需要使用狀態管理架構(例如 ScopedModel,Redux)去管理這種狀態。你需要用的只是一個 StatefulWidget。

在下方你可以看到一個底部導航欄中當前被選中的專案是如何被被儲存在 _MyHomepageState 類的 _index 變數中。在這個例子中,_index 是一個短時狀態。

class MyHomepage extends StatefulWidget {
  @override
  _MyHomepageState createState() => _MyHomepageState();
}

class _MyHomepageState extends State<MyHomepage> {
  int _index = 0;

  @override
  Widget build(BuildContext context) {
    return
BottomNavigationBar( currentIndex: _index,onTap: (newIndex) { setState(() { _index = newIndex; }); },// ... items ... ); } } 複製程式碼

在這裡,使用 setState() 和一個變數就能達到管理狀態的目的。你的 app 中的其他部分不需要訪問 _index。這個變數只會在 MyHomepage widget 中改變。而且,如果使用者關閉並重啟這個 app,_index會被重置而不會繼續保持原來的狀態。

  • 應用狀態,如果你想在你的應用中的多個部分之間共享一個非短時的狀態,並且在使用者會話期間保留這個狀態,我們稱之為應用狀態(有時也稱共享狀態)。 應用狀態的一些例子:

      1、使用者選項
    
      2、登入資訊
    
      3、一個社交應用中的通知
    
      4、一個電商應用中的購物車
    
      5、一個新聞應用中的文章已讀/未讀狀態
    複製程式碼

五、共享狀態管理

在 Flutter 中,一般是將儲存狀態的物件置於 widget 樹中對應 widget 的上層,當它發生改變的時候,它對應的widget會從上層開始重構。因為這個機制,所以 widget 無需考慮生命週期的問題—它只需要針對 上層儲存資料的物件 宣告所需顯示內容即可。當內容發生改變的時候,舊的 widget 就會消失,完全被新的 widget 替代。 Flutter原生提供了兩個方法來管理共享狀態:

5.1 --InheritedWidget

class ADCounterWidget extends InheritedWidget {
  // 1. 共享的資料
  final int counter;

  // 2. 定義構造方法
  ADCounterWidget({this.counter,Widget child}): super(child: child);

  // 3. 找到當前Widget樹中最近的InheritedWidget
  static ADCounterWidget of(BuildContext context) {
    // 沿著Element樹,去找到最近的ADCounterElement,從Element中取出Widget物件
    return context.dependOnInheritedWidgetOfExactType();
  }

  // 4. 要不要回調State中的didChangeDependencies方法
  @override
  bool updateShouldNotify(ADCounterWidget oldWidget) {
    return oldWidget.counter != counter;
  }
}

複製程式碼
  • 上面定義了一個of方法,該方法通過context開始去查詢父級的HYDataWidget
  • updateShouldNotify方法是對比新舊HYDataWidget,是否需要對更新相關依賴的Widget
class HYHomePage extends StatefulWidget {
  @override
  _HYHomePageState createState() => _HYHomePageState();
}

class _HYHomePageState extends State<HYHomePage> {
  int data = 100;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("InheritedWidget"),),body: HYDataWidget(
        counter: data,child: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,children: <Widget>[
              HYShowData()
            ],floatingActionButton: FloatingActionButton(
        child: Icon(Icons.add),onPressed: () {
          setState(() {
            data++;
          });
        },);
  }
}

複製程式碼

建立HYDataWidget,並且傳入資料(這裡點選按鈕會修改資料,並且出發重新build)

5.2 --Provider

Provider庫有三個主要用到的類:

  • ChangeNotifier:真正資料(狀態)存放的地方
  • ChangeNotifierProvider:Widget樹中提供資料(狀態)的地方,會在其中建立對應的ChangeNotifier
  • Consumer:Widget樹中需要使用資料(狀態)的地方

第一步 在程式的最頂層建立自己的ChangeNotifier

  • 將ChangeNotifierProvider放到了頂層,這樣方便在整個應用的任何地方可以使用CounterProvider
  • 在ChangeNotifier中建立一個私有的_counter,並且提供了getter和setter
  • 在setter中我們監聽到_counter的改變,就呼叫notifyListeners方法,通知所有的Consumer進行更新
void main() {
  runApp(ChangeNotifierProvider(
    create: (context) => CounterProvider(),child: MyApp(),));
}

class CounterProvider extends ChangeNotifier {
  int _counter = 100;
  intget counter {
    return _counter;
  }
  set counter(int value) {
    _counter = value;
    notifyListeners();
  }
}

複製程式碼

第二步 在首頁中使用Consumer引入和修改狀態

  • 在body中使用Consumer,Consumer需要傳入一個builder回撥函式,當資料發生變化時,就會通知依賴資料的Consumer重新呼叫builder方法來構建
  • 在floatingActionButton中使用Consumer,當點選按鈕時,修改CounterNotifier中的counter資料
class HYHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("列表測試"),body: Center(
        child: Consumer<CounterProvider>(
          builder: (ctx,counterPro,child) {
            return Text("當前計數:${counterPro.counter}",style: TextStyle(fontSize: 20,color: Colors.red),);
          }
        ),floatingActionButton: Consumer<CounterProvider>(
        builder: (ctx,child) {
          return FloatingActionButton(
            child: child,onPressed: () {
              counterPro.counter += 1;
            },);
        },child: Icon(Icons.add),);
  }
}

複製程式碼

Consumer的builder方法有三個引數:

  • context,每個build方法都會有上下文,目的是知道當前樹的位置
  • ChangeNotifier對應的例項,也是我們在builder函式中主要使用的物件
  • child,目的是進行優化,如果builder下面有一顆龐大的子樹,當模型發生改變的時候,我們並不希望重新build這顆子樹,那麼就可以將這顆子樹放到Consumer的child中,在這裡直接引入即可(注意我案例中的Icon所放的位置)

第三步 建立一個新的頁面,在新的頁面中修改資料

class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("第二個頁面"),);
  }
}

複製程式碼