Flutter Scoped_Model 淺析

愛小麗0427發表於2019-06-10

在前端開發中,我們經常能聽到 redux 等狀態管理的詞彙。

但是對於我這種搞移動端出身的人,對這些詞彙就不是很熟悉。

Flutter 作為借鑑了很多 React 思想的語言,自然也會有相對應的狀態管理。

那什麼是狀態管理?為什麼需要狀態管理?

什麼是狀態管理?

個人認為 狀態管理解決的是元件之間的通訊以及狀態集中管理和分發的問題

舉個例子:

比如我多個頁面同時使用了 User 物件,當我其中一個地方改了以後,想要其他的地方也都要更改,那這個時候就需要狀態管理來集中管理資料。

為什麼需要狀態管理?

前面已經說過一點,另一點:

我們已經使用過 StatefulWidget,也知道它維護了一個 State,也就是當前 Widget的狀態。

當我們需要改變 Widget 的狀態的時候,就需要 setState(),這樣就會重新走一遍 build 方法來重繪。

當頁面簡單的時候還好說,如果頁面複雜了,我們每次點選、或者滑動都要來進行整個頁面的 build 嗎?

很明顯,這樣不符合常理。

相信很多人已經聽過 provide redux... 等等狀態管理的方案,

那麼 Scoped_Model 是什麼?

Scoped_Model

先看一下Scoped_Model GitHub 文件上的內容:

A set of utilities that allow you to easily pass a data Model from a parent Widget down to it's descendants. In addition, it also rebuilds all of the children that use the model when the model is updated. This library was originally extracted from the Fuchsia codebase.

一組實用程式,允許您輕鬆地將資料模型從父視窗小部件傳遞給它的後代。此外,它還重建了模型更新時使用模型的所有子代。這個庫最初是從 Fuchsia 基程式碼中提取的。

和其他的狀態管理一樣,它也是使用的 InheritedWidget, 利用 InheritedWidget 來管理使用了該資料的Widget。

這樣就可以在資料改變的時候更新該 Widget 了。

簡單使用 Scoped_Model

來看一下官方給出的Demo:

import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';

void main() {
  runApp(MyApp(
    model: CounterModel(),
  ));
}

class MyApp extends StatelessWidget {
  final CounterModel model;

  const MyApp({Key key, @required this.model}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // At the top level of our app, we'll, create a ScopedModel Widget. This
    // will provide the CounterModel to all children in the app that request it
    // using a ScopedModelDescendant.
    return ScopedModel<CounterModel>(
      model: model,
      child: MaterialApp(
        title: 'Scoped Model Demo',
        home: CounterHome('Scoped Model Demo'),
      ),
    );
  }
}

// Start by creating a class that has a counter and a method to increment it.
//
// Note: It must extend from Model.
class CounterModel extends Model {
  int _counter = 0;

  int get counter => _counter;

  void increment() {
    // First, increment the counter
    _counter++;

    // Then notify all the listeners.
    notifyListeners();
  }
}

class CounterHome extends StatelessWidget {
  final String title;

  CounterHome(this.title);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('You have pushed the button this many times:'),
            // Create a ScopedModelDescendant. This widget will get the
            // CounterModel from the nearest parent ScopedModel<CounterModel>.
            // It will hand that CounterModel to our builder method, and
            // rebuild any time the CounterModel changes (i.e. after we
            // `notifyListeners` in the Model).
            ScopedModelDescendant<CounterModel>(
              builder: (context, child, model) {
                return Text(
                  model.counter.toString(),
                  style: Theme.of(context).textTheme.display1,
                );
              },
            ),
          ],
        ),
      ),
      // Use the ScopedModelDescendant again in order to use the increment
      // method from the CounterModel
      floatingActionButton: ScopedModelDescendant<CounterModel>(
        builder: (context, child, model) {
          return FloatingActionButton(
            onPressed: model.increment,
            tooltip: 'Increment',
            child: Icon(Icons.add),
          );
        },
      ),
    );
  }
}
複製程式碼

程式碼有點長,但是沒關係,大部分都是註釋。

直接copy程式碼到專案中,執行看一下效果:

Flutter Scoped_Model 淺析

效果非常簡單,和我們剛開始學Flutter一樣的例子。

下面就解釋一下程式碼,

可以看到,首先是把 ScopedModel 放在了APP 最頂部來初始化:

class MyApp extends StatelessWidget {
  final CounterModel model;

  const MyApp({Key key, @required this.model}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    // At the top level of our app, we'll, create a ScopedModel Widget. This
    // will provide the CounterModel to all children in the app that request it
    // using a ScopedModelDescendant.
    return ScopedModel<CounterModel>(
      model: model,
      child: MaterialApp(
        title: 'Scoped Model Demo',
        home: CounterHome('Scoped Model Demo'),
      ),
    );
  }
}
複製程式碼

隨後定義了一個 CounterModel:

// Start by creating a class that has a counter and a method to increment it.
//
// Note: It must extend from Model.
class CounterModel extends Model {
  int _counter = 0;

  int get counter => _counter;

  void increment() {
    // First, increment the counter
    _counter++;

    // Then notify all the listeners.
    notifyListeners();
  }
}
複製程式碼

註釋上面寫的很清楚,必須繼承自 Model

為什麼?我們看Model原始碼:

abstract class Model extends Listenable {
  final Set<VoidCallback> _listeners = Set<VoidCallback>();
  int _version = 0;
  int _microtaskVersion = 0;

  /// [listener] 將在Model更改時呼叫。
  @override
  void addListener(VoidCallback listener) {
    _listeners.add(listener);
  }

  /// [listener] 移除時呼叫。
  @override
  void removeListener(VoidCallback listener) {
    _listeners.remove(listener);
  }

  /// Returns the number of listeners listening to this model.
  int get listenerCount => _listeners.length;

  /// 僅當Model已更改時由[model]呼叫。
  @protected
  void notifyListeners() {
    // 我們安排一個微任務來消除可能同時發生的多個更改。
    if (_microtaskVersion == _version) {
      _microtaskVersion++;
      scheduleMicrotask(() {
        _version++;
        _microtaskVersion = _version;

        // Convert the Set to a List before executing each listener. This
        // prevents errors that can arise if a listener removes itself during
        // invocation!
        _listeners.toList().forEach((VoidCallback listener) => listener());
      });
    }
  }
}
複製程式碼

可以看到,Model 繼承了 Listenable,所以我們在自己定義 Model 的時候才可以呼叫 notifyListeners()方法。

最後在需要該 Model的地方使用 ScopedModelDescendant 來獲取。

ScopedModelDescendant<CounterModel>(
  builder: (context, child, model) {
    return Text(
      model.counter.toString(),
      style: Theme.of(context).textTheme.display1,
    );
  },
),
複製程式碼

有人可能覺得這種方式不是很優雅,程式碼太多。

官方也提供了另一種方法:ScopedModel.of<CounterModel>(context)

狀態的集中管理以及 Widget 更新

官方示例只是提供一個簡單的例子,並不能展現出它的威力,

所以我們自己寫一個示例。

該示例在多個頁面同時使用同一個資料,然後在其中一個頁面更新資料。

這樣就達到了我們所謂狀態的集中管理。

效果如下:

Flutter Scoped_Model 淺析

主要程式碼如下:

// 點選事件
@override
Widget build(BuildContext context) {
  return FloatingActionButton(
    onPressed: ScopedModel.of<ScopedCounter>(context).increment,
    tooltip: 'Increment',
    child: const Icon(Icons.add),
  );
}


// 接收事件
class CounterLabel extends StatelessWidget {
  const CounterLabel({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    print("third counter label build");
    final counter = ScopedModel.of<ScopedCounter>(context, rebuildOnChange: true);
    return Column(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        const Text(
          'You have pushed the button this many times:',
        ),
        Text(
          '${counter.count}',
          style: Theme.of(context).textTheme.display1,
        ),
      ],
    );
  }
}
複製程式碼

可以看到我們的 Widget 都是無狀態的,也就是說我們確實達到了資料更新就更新UI的要求。

那麼我們再列印log 看一下,是否只是更新了 使用該 Model 的 Widget。

還是整個Page 都 build 了。

我們在 Page 的 build方法中列印:

class MyHomePage extends StatelessWidget {
  const MyHomePage({Key key}) : super(key: key);
  @override
  Widget build(BuildContext context) {
    print("home page build");
  }
}

........ // 第二第三頁同理
print("second home page build");
print("third counter label build");
複製程式碼

然後在 CounterLabel 中 列印

class CounterLabel extends StatelessWidget {
  const CounterLabel({Key key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    print("home counter label build");
  }
}

........ // 第二第三頁同理
print("second counter label build");
print("third counter label build");
複製程式碼

執行效果如下:

Flutter Scoped_Model 淺析

可以看到,確實只更新了使用該 Model 的 Widget。

總結

在Flutter 中狀態管理有很多,redux、fish_redux 等等等等。

而Scoped_Model 是我用過最簡單,最舒服的一種。

因為我是搞移動開發的,所以我會選擇 Scoped_Model。

下一篇簡單講講 Scoped_Model 的原理。

完整程式碼已經傳至GitHub:github.com/wanglu1209/…

Flutter Scoped_Model 淺析