Flutter Scoped_Model 淺析

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

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

Flutter 做爲借鑑了不少 React 思想的語言,天然也會有相對應的狀態管理。github

那什麼是狀態管理?爲何須要狀態管理?redux

什麼是狀態管理?

我的認爲 狀態管理解決的是組件之間的通信以及狀態集中管理和分發的問題markdown

舉個例子:app

好比我多個頁面同時使用了 User 對象,當我其中一個地方改了之後,想要其餘的地方也都要更改,那這個時候就須要狀態管理來集中管理數據。less

爲何須要狀態管理?

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

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

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

當頁面簡單的時候還好說,若是頁面複雜了,咱們每次點擊、或者滑動都要來進行整個頁面的 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同樣的例子。

下面就解釋一下代碼,

能夠看到,首先是把 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 更新

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

因此咱們本身寫一個示例。

該示例在多個頁面同時使用同一個數據,而後在其中一個頁面更新數據。

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

效果以下:

主要代碼以下:

// 點擊事件
@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");
複製代碼

運行效果以下:

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

總結

在Flutter 中狀態管理有不少,redux、fish_redux 等等等等。

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

由於我是搞移動開發的,因此我會選擇 Scoped_Model。

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

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

相關文章
相關標籤/搜索