# 状态管理方案-ScopedModel

# 核心概念

ScopedModel是由3个类组成:

  • Model

    Model是一个保存数据和与数据相关的业务逻辑的类,它是以Listenable的形式实现的,并且可以通知(notifyListeners())任何可能有兴趣知道变化的人。

  • ScopedModel

    ScopedModel是一个Widget,类似于一个Provider,它 "持有 "Model,并允许:

    • 通过ScopedModel.of<Model>(context)调用来检索Model。
    • 当请求时,将上下文注册为底层InheritedWidget的依赖关系。

    ScopedModel基于AnimatedBuilder (opens new window),它监听Model发送的通知,然后重建一个InheritedWidget (opens new window),而InheritedWidget又会请求重建所有的依赖关系。

  • ScopedModelDescendant

    ScopedModelDescendant是一个Widget,它对Model的变化做出反应;当Model通知发生变化时,它会重建。

# 计数器应用

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

// 1.创建ScopedModel -> state: _count -> increment
class CountModel extends Model {
  int _count = 10;

  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }

  void decrement() {
    _count--;
    notifyListeners();
  }
}

void main() {
  runApp(const MyApp());
}

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  
  Widget build(BuildContext context) {
    return ScopedModel<CountModel>(
      model: CountModel(),
      // 2.ScopedModelDescendant -> builder -> model
      child: ScopedModelDescendant<CountModel>(
        builder: (context, child, model) => Scaffold(
          appBar: AppBar(
            title: Text(widget.title),
          ),
          body: Center(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                const Text(
                  'You have pushed the button this many times:',
                ),
                Text(
                  '${model.count}',
                  style: Theme.of(context).textTheme.headline4,
                ),
                // ignore: prefer_const_constructors
                ChildPage(),
              ],
            ),
          ),
          floatingActionButton: Column(
            mainAxisAlignment: MainAxisAlignment.end,
            children: [
              FloatingActionButton(
                // 操作model
                onPressed: model.increment,
                tooltip: 'Increment',
                child: const Icon(Icons.add),
              ),
              FloatingActionButton(
                onPressed: model.decrement,
                tooltip: 'Decrement',
                child: const Icon(Icons.remove),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

Tips: 当回调函数的形参与调用的函数的形参一致的时候,可以使用省略的写法:

onPressed: () {
  return model.increment();
}

等价于:

onPressed: () => model.increment()

等价于:

onPressed: model.increment

在子页面中的使用model中的state:

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

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

  
  Widget build(BuildContext context) {
	  // ScopedModel.of -> 取state,操作model
    int count = ScopedModel.of<CountModel>(context).count;
    return Container(
      child: Text(
        count.toString(),
      ),
    );
  }
}

# 封装getModel方法

import 'package:scoped_model/scoped_model.dart';

T getModel<T extends Model>(context) {
  return ScopedModel.of<T>(context, rebuildOnChange: true);
}

注意:Dart是跟TypeScript一样的有类型的语言,<>在这里代表着类型入参。

T代表着泛型,可以类比着TypeScript中的泛型学习。

使用:

修改child.dart文件:

import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:scoped_model_example/main.dart';
import 'package:scoped_model_example/utils/get_model.dart';

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

  
  Widget build(BuildContext context) {
    // 方法二:通过rebuildOnChange来设置获取最新的model中的数据
    // ScopedModel.of<CountModel>(context, rebuildOnChange: true) -> GetModel<T>(context)
    int count = getModel<CountModel>(context).count;
    // int count =
    //     ScopedModel.of<CountModel>(context, rebuildOnChange: true).count;
    return Container(
      child: Text(
        count.toString(),
      ),
    );
  }
}

# 全局状态共享

目标:

  • 学习页面跳转,父子页面跳转;
  • 学习scoped_model的单例方案,借助get_it实现;
  • 学习get_it包的作用及常见的API;
  • 实现跨页面的状态共享;

目前scoped_model,只能做到父子状态共享,无法实现跨页面的状态共享,大家可以自行尝试。

scoped_model如何实现页面间状态共享?

步骤:

  • 创建一个base_view.dart的类,封装scoped_model;
  • 使用get_it,并创建一个单例;
  • 使用BaseView来使用model;

创建lib/setup_get_it.dart文件:

import 'package:get_it/get_it.dart';
import 'package:scoped_model_example/scoped_model/count_model.dart';

final getIt = GetIt.instance;

void setupGetIt() {
  getIt.registerSingleton<CountModel>(CountModel());
  // todo
  // getIt.registerLazySingleton<CountModel>(() => CountModel());
}

区别:

  • getIt.registerSingleton<CountModel>(CountModel());初始化即注册;
  • getIt.registerLazySingleton<CountModel>(() => CountModel());第一次使用即注册;

然后,在main.dart中进行调用:

import 'package:scoped_model_example/setup_get_it.dart';

void main() {
  setupGetIt();
  runApp(const MyApp());
}

创建lib/views/base_view.dart文件:

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

class BaseView<T extends Model> extends StatelessWidget {
  final Widget Function(BuildContext context, Widget? child, T model) builder;

  const BaseView(this.builder, {Key? key}) : super(key: key);

  
  Widget build(BuildContext context) {
    return ScopedModel<T>(
      model: getIt<T>(),
      child: ScopedModelDescendant<T>(
        builder: builder,
      ),
    );
  }
}

创建子页面lib/second.dart

import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:scoped_model_example/scoped_model/count_model.dart';
import 'package:scoped_model_example/setup_get_it.dart';
import 'package:scoped_model_example/views/base_view.dart';

class Second extends StatefulWidget {
  Second({Key? key}) : super(key: key);

  
  _SecondState createState() => _SecondState();
}

class _SecondState extends State<Second> {
  
  Widget build(BuildContext context) {
    // 在这里我跟我特scoped_model
    // 思考:这里为什么要传CountModel? 答:如果不写,那么BaseView不知道去使用哪个Model!
    return BaseView<CountModel>(
      (context, child, model) => Scaffold(
        appBar: AppBar(
          title: const Text('second page'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              const Text(
                'You have pushed the button this many times:',
              ),
              Text(
                '${model.count}',
                style: Theme.of(context).textTheme.headline4,
              ),
            ],
          ),
        ),
        floatingActionButton: Column(
          mainAxisAlignment: MainAxisAlignment.end,
          children: [
            FloatingActionButton(
              heroTag: 'btn1',
              onPressed: model.increment,
              tooltip: 'Increment',
              child: const Icon(Icons.add),
            ),
            FloatingActionButton(
              heroTag: 'btn2',
              onPressed: model.decrement,
              tooltip: 'Decrement',
              child: const Icon(Icons.remove),
            ),
          ],
        ),
      ),
    );
  }
}

首页,组件封装与状态共享:

封装CounterModel:

import 'package:scoped_model/scoped_model.dart';

class CountModel extends Model {
  int _count = 10;

  int get count => _count;

  void increment() {
    _count++;
    // 必须要调用,否则页面不更新
    notifyListeners();
  }

  void decrement() {
    _count--;
    // 必须要调用,否则页面不更新
    notifyListeners();
  }
}

修改首页,使用BaseView:

import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:scoped_model_example/child.dart';
import 'package:scoped_model_example/scoped_model/count_model.dart';
import 'package:scoped_model_example/second.dart';
import 'package:scoped_model_example/setup_get_it.dart';
import 'package:scoped_model_example/views/base_view.dart';
// 1.创建ScopedModel -> state: _count -> increment
// 2.ScopedModelDescendant -> builder -> model
// ScopedModel.of -> 取state,操作model

// 1.抽离Model的逻辑
// 2.共享所有的Model状态
// 3.封装一个BaseView的组件 -> 减少书写 ScopedModelDescendant

void main() {
  setupGetIt();
  runApp(const MyApp());
}

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

  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  
  Widget build(BuildContext context) {
    return BaseView<CountModel>(newMethod);
  }

  Widget newMethod(context, child, model) => Scaffold(
        appBar: AppBar(
          title: Text(widget.title),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: <Widget>[
              const Text(
                'You have pushed the button this many times:',
              ),
              Text(
                '${model.count}',
                style: Theme.of(context).textTheme.headline4,
              ),
              // 方法一:配置ignore: prefer_const_constructors去除警告的
              const ChildPage(),
              ElevatedButton(
                  onPressed: () {
                    // 跳转到Second页面
                    Navigator.of(context).push(MaterialPageRoute(
                      builder: (context) => Second(),
                    ));
                  },
                  child: const Text('跳转Second'))
            ],
          ),
        ),
        floatingActionButton: Column(
          mainAxisAlignment: MainAxisAlignment.end,
          children: [
            FloatingActionButton(
              // 多个FloatingActionButton,需要设置heroTag
              heroTag: 'btn1',
              onPressed: model.increment,
              tooltip: 'Increment',
              child: const Icon(Icons.add),
            ),
            FloatingActionButton(
              // 多个FloatingActionButton,需要设置heroTag
              heroTag: 'btn2',
              onPressed: model.decrement,
              tooltip: 'Decrement',
              child: const Icon(Icons.remove),
            ),
          ],
        ),
      );
}

PS: 由于scoped_model的逻辑清晰且简单,待办清单的扩展就当成是作业,自行尝试完成。