Widget状态和应用数据管理

2,566 阅读6分钟

欢迎点赞,转载请注明出处

本节示例项目源代码下载点击这里 counter_demos,该示例集合了Flutter计数器项目的各种"变形"版
本节示例需要下载多个flutter包依赖,如果下载失败,可以尝试设置pub国内镜像地址环境变量,macOS下export PUB_HOSTED_URL=https://pub.flutter-io.cn,Windows下PUB_HOSTED_URL=https://pub.flutter-io.cn

状态管理

在构建高质量的应用程序时,状态管理就变得至关重要。如果需要在应用中的多个部分之间共享一个非短时的状态,并且在用户会话期间保留这个状态,我们称之为应用状态(有时也称共享状态)。相对于应用状态,StatefulWidget内的UI状态则称为短时状态或者局部状态。上一章讲到的路由间传递的参数变量可以认为是应用状态。

状态管理的方法和策略有很多种,下面我们对几种比较典型的方案进行介绍。

全局变量

在一个类中声明一个静态变量,可以整个应用中跨页面全局读取。例如:

Class GlobalState{
    static int counter;
}

我们在计数器模板项目里简单做一个全局变量的使用说明:

全局变量的弊端显然易见,对变量状态值的变化难以追踪和控制,容易产生缺陷。

provider插件

我们可以使用观察者模式来实现应用状态管理。观察者模式是软件设计模式的一种。在此种模式中,一个目标对象管理所有相依于它的观察者对象,并且在它本身的状态改变时主动发出通知。这通常通过呼叫各观察者所提供的方法来实现。此种模式通常被用来实时事件处理系统。观察者模式又被称为发布者-订阅者模式。provider的状态管理是观察者模式的一种。
provider使用到了ChangeNotifier类。ChangeNotifier是Flutter SDK中的一个简单的模型类。它用于向监听器发送通知。换言之,如果Widget关联了ChangeNotifier,可以订阅它的状态变化,当ChangeNotifier模型发生改变并且需要更新UI的时候可以调用ChangeNotifier的notifyListeners()方法。
自己实现观察者模式比较复杂一些,我们可以利用provider package,在pubspec.yaml中增加provider的导入,provider的版本号与项目使用的Flutter版本号有关。

dependencies:

  # Import the provider package.
  provider: ^3.1.0

provider的使用有以下几个要点:

  • provider的ChangeNotifierProvider Widget可以向其子孙节点暴露一个ChangeNotifier实例,相当于观察者模式的发布者。
  • provider的Consumer Widget订阅个ChangeNotifier实例,相当于订阅者。Consumer Widget唯一必须的参数就是 builder。当 ChangeNotifier调用notifyListeners()时,所有和Consumer相关的builder方法都会被调用。
  • 有的时候不需要模型中的数据来改变UI,但是可能还是需要访问该数据。可以使用 Provider.of,并且将listen设置为false。

我们对照下用provider和widget state的方式来实现Flutter Project App默认模板项目计数器功能:

左图main_provider.dart使用的Counter类来管理应用状态,右图main_state.dart使用State类里的_counter实例变量来管理短时状态。我们可以看到代码的复杂度基本一样,但随着应用复杂度的增加,provider的观察者模式优势会更明显。provider的官方示例代码如下:

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

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => Counter(),
      child: MyApp(),
    ),
  );
}

class Counter with ChangeNotifier {
  int value = 0;

  void increment() {
    value += 1;
    notifyListeners();
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter provider Demo'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text('You have pushed the button this many times:'),
            Consumer<Counter>(
              builder: (context, counter, child) => Text(
                '${counter.value}',
                style: Theme.of(context).textTheme.display1,
              ),
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () =>
            Provider.of<Counter>(context, listen: false).increment(),
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

跟Provider类似的第三方库还有ScopedModel、redux、mobx等。

StreamBuilder

Flutter自带的StreamBuilder可以应用于状态管理,StreamBuilder是Stream在UI方面的一种使用场景。
所有类型值都可以通过流传递。从值,事件,对象,集合,映射,错误或甚至另一个流,可以由stream传达任何类型的数据。
使用StreamController来控制Stream,StreamController通过sink属性公开了Stream入口,StreamController通过stream属性公开了Stream的出口。因为StreamController对Sink入口数据处理是异步的,所以我们需要监听Stream出口。
一个输出输入字符串长度的的StreamController的使用示例如下:

import 'dart:async';

void main() {
  final StreamController<String> ctrl = StreamController<String>();
  
  final transformer =
      StreamTransformer<String, int>.fromHandlers(handleData: (value, sink) {
    sink.add(value.length);
  });
  
  ctrl.stream
      .transform(transformer)
      .listen((data) => print("String length is $data"));
  
  ctrl.sink.add('groupones');
  ctrl.close();
}

StreamBuilder依赖Stream来做异步数据获取,计数器StreamBuilder的版本实现如下:

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

void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home:CounterPage(),
    );
  }
}

class CounterPage extends StatefulWidget {
  @override
  _CounterPageState createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  int _counter = 0;
  final StreamController<int> _streamController = StreamController<int>();

  @override
  void dispose(){
    _streamController.close();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Flutter StreamBuilder Demo')),
      body: Center(
        child: StreamBuilder<int>(
            stream: _streamController.stream,
            initialData: _counter,
            builder: (BuildContext context, AsyncSnapshot<int> snapshot){
              return Text('You have pushed the button this many times:: ${snapshot.data}');
            }
        ),
      ),
      floatingActionButton: FloatingActionButton(
        child: const Icon(Icons.add),
        onPressed: (){
          _streamController.sink.add(++_counter);
        },
      ),
    );
  }
}

通过StreamBuilder第1个参数stream来监听数据的变化,执行第3个参数build参数,完成之前setState同步更新UI的功能。snapshot代表最新的stream的快照。

BLoC模式

BLoC由谷歌2018年设计,代表业务逻辑组件(Business Logic Component),具有责任分离、可测性高、布局自由、Widget build次数减少等优点。BLoc将UI和业务逻辑相分离,它的原理图如下:

BLoC有几个核心的概念:

  • 事件(Event)会被输入到Bloc中,通常是为了响应用户交互或者是生命周期事件而添加它们。
  • 状态(State)是Bloc所输出的东西,是程序状态的一部分。它可以通知UI组件,并根据当前状态(State)重建(build) 其自身的某些部分。
  • 从一种状态状态(State)到另一种状态状态(State)的变动称之为转换(Transitions) 。转换是由当前状态,事件和下一个状态组成。
  • 流(Stream) 是一系列非同步的数据。

BLoC的计数器版本示例代码如下:

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

abstract class CounterEvent{}

class IncrementEvent extends CounterEvent{}

class CounterBLoC{

  int _counter = 0;

  final counterStateController = StreamController<int>();
  final counterEventController = StreamController<CounterEvent>();

  CounterBLoC() {  counterEventController.stream.listen(_count);  }

  void _count(CounterEvent event) => counterStateController.sink.add(++_counter);

  void dispose(){
    counterStateController.close();
    counterEventController.close();
  }
}


void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter BLoC Demo'),
    );
  }
}

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

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final _bloc = CounterBLoC();

  @override
  void dispose(){
    _bloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            StreamBuilder(
                stream: _bloc.counterStateController.stream,
                initialData: 0,
                builder: (context, snapshot) {
                  return Center(child: Text( snapshot.data.toString() ));
                }
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _bloc.counterEventController.sink.add(IncrementEvent()),
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

更为详细地BLoC介绍及示例,可以参考这里。你也可以使用flutter bloc库实现更复杂的BLoC模式。

shared_preferences插件

shared_preferences插件是Flutter应用变量本地持久化存储的一种方式,它等价于iOS本地存储NSUserDefaults类,Android本地存储的SharedPreferences类以及Web的localStorage对象。使用shared_preferences库时,需要在pubspec.yml配置shared_preferences: ^0.5.4+8依赖声明,它的版本号同项目的Flutter版本号相匹配,shared_preferences的计时器示例代码如下:

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

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter shared_preferences Demo'),
    );
  }
}

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

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {

  int _counter;
  @override
  void initState() {
    _retrieveCounter();
  }
  void _incrementCounter() async {
    SharedPreferences prefs = await SharedPreferences.getInstance();
    int localCount= (prefs.getInt('counter') ?? 0) + 1;
    await prefs.setInt('counter', localCount);
    setState(() {
      _counter=localCount;
    });
  }
  void _retrieveCounter() async {
    SharedPreferences prefs =await  SharedPreferences.getInstance();
    setState(() {
      _counter = prefs.getInt('counter') ?? 0;
    });
  }
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

需要注意SharedPreferences.getInstance()是需要await同步获取的。getInt('counter')表示从本地存储的'counter'的唯一键值里读取一个整数值,setInt表示写入。和不同于前面其他几个例子,当该计数器应用被关闭再重新打开时,界面上计数值为上一次关闭时的值。

文件读写

对于大量的字符串或二进制流使用设备的磁盘文件的读写操作会相对方便一些。文件读写的计数器示例代码如下:

import 'dart:async';
import 'dart:io';

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';

void main() {
  runApp(
    MaterialApp(
      title: 'Flutter Demo',
      home: FlutterDemo(storage: CounterStorage()),
    ),
  );
}

class CounterStorage {
  Future<String> get _localPath async {
    final directory = await getTemporaryDirectory();

    return directory.path;
  }

  Future<File> get _localFile async {
    final path = await _localPath;
    return File('$path/counter.txt');
  }

  Future<int> readCounter() async {
    try {
      final file = await _localFile;

      // Read the file
      String contents = await file.readAsString();

      return int.parse(contents);
    } catch (e) {
      // If encountering an error, return 0
      return 0;
    }
  }

  Future<File> writeCounter(int counter) async {
    final file = await _localFile;

    // Write the file
    return file.writeAsString('$counter');
  }
}

class FlutterDemo extends StatefulWidget {
  final CounterStorage storage;

  FlutterDemo({Key key, @required this.storage}) : super(key: key);

  @override
  _FlutterDemoState createState() => _FlutterDemoState();
}

class _FlutterDemoState extends State<FlutterDemo> {
  int _counter;

  @override
  void initState() {
    super.initState();
    widget.storage.readCounter().then((int value) {
      setState(() {
        _counter = value;
      });
    });
  }

  Future<File> _incrementCounter() {
    setState(() {
      _counter++;
    });

    // Write the variable as a string to the file.
    return widget.storage.writeCounter(_counter);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Flutter File Demo')),
      body: Center(
        child: Text(
          'You have pushed the button this many times: $_counter.',
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

这个示例中我们将counter.txt文件被存在iOS设备的缓存目录下(NSCachesDirectory)或Android设备的缓存目录下(getCacheDir),我们使用这个path_provider插件完成平台无关的统一路径获取。因此,我们需要在pubspec.yml文件配置path_provider: 1.5.0依赖声明。

sqflite插件

shared_preferences只适用于存储简单、少量的数据结构,而如果存储复杂结构化的数据结构则需要使用SQLite数据库了。Android和iOS都支持SQLite数据库。sqflite插件(注意sqflite的拼写不是sqlite)是用于Flutter的SQLite插件,它具有以下特点:

  • 支持事务和批处理
  • 数据库版本自动化管理
  • 插入/查询/更新/删除帮助类
  • iOS和Android后台线程数据库操作

使用sqflite库,需要在pubspec.yml配置sqflite: ^1.1.8依赖声明。sqflite的计时器示例代码如下,在学习下面代码之前,你需要掌握SQL关系数据库的基础概念。

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

final String tableCounter = 'counter';
final String columnId = '_id';
final String columnValue = 'value';

class Counter {
  String id;
  int value;
  Map<String, dynamic> toMap() {
    var map = <String, dynamic>{columnId: id, columnValue: value};
    return map;
  }
  Counter.fromMap(Map<String, dynamic> map) {
    id = map[columnId] as String;
    value = map[columnValue] as int;
  }
}

class CounterProvider {
  CounterProvider._constr();
  static final CounterProvider dbInstance = new CounterProvider._constr();
  factory CounterProvider() => dbInstance;

  Database _db;
  Future<Database> get db async {
    if (_db != null) {
      return _db;
    }
    _db = await open();
    return _db;
  }

  Future<Database> open() async {
    String databasesPath = await getDatabasesPath();
    String path = databasesPath + "/demo.db";
    return await openDatabase(path, version: 1,
        onCreate: (Database db, int version) async {
      await db.execute('''
        create table $tableCounter ( 
          $columnId text primary key, 
          $columnValue integer not null)
      ''');
    });
  }

  Future<Counter> insert(Counter counter) async {
    final dbClient = await db;
    await dbClient.insert(tableCounter, counter.toMap());
    return counter;
  }

  Future<Counter> getCounter(String id) async {
    final dbClient = await db;
    List<Map<String, dynamic>> maps = await dbClient.query(tableCounter,
        columns: [columnValue],
        where: '$columnId = ?',
        whereArgs: <dynamic>[id]);
    if (maps.length > 0) {
      return Counter.fromMap(maps.first);
    }
    return null;
  }

  Future<int> delete(String id) async {
    final dbClient = await db;
    return await dbClient
        .delete(tableCounter, where: '$columnId = ?', whereArgs: <dynamic>[id]);
  }

  Future<int> update(Counter counter) async {
    final dbClient = await db;
    return await dbClient.update(tableCounter, counter.toMap(),
        where: '$columnId = ?', whereArgs: <dynamic>[counter.id]);
  }
  
  Future close() async => {if (_db != null) _db.close()};
}

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter sqflite Demo'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

  @override
  void initState()  {
    _retrieveCounter();
  }

  void _incrementCounter() async {
    Counter counter = await CounterProvider.dbInstance.getCounter("demo");
    _counter = counter == null ? 0 : counter.value;
    _counter++;
    if (counter == null)
      await CounterProvider.dbInstance
          .insert(Counter.fromMap(<String, dynamic>{columnId:"demo",columnValue:_counter}));
    else
      await CounterProvider.dbInstance
          .update(Counter.fromMap(<String, dynamic>{columnId:"demo",columnValue:_counter}));
    setState(() {});
  }

  void _retrieveCounter() async {
    Counter counter = await CounterProvider.dbInstance.getCounter("demo");
    setState(() {
      _counter = counter == null ? 0 : counter.value;
    });
  }

  @override
  void dispose() {
    CounterProvider.dbInstance.close();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

为了操作方便,构建了一个CounterProvider的sqlite的帮助类,封装了数据库创建,数据库关闭,表创建,插入,查询,更新和删除等常见操作。CounterProvider.dbInstance使用的是单例模式。

小结

与状态管理相关的概念和方法,还包括Scoped Model,Redux,RxDart,Mobx,ValueNotifer等,本节就不一一展开介绍了。 如果数据状态管理涉及到UI状态更新可以考虑使用provider,BLoC等,如果涉及到业务逻辑数据且需要App本地持久化保存则使用Share Preferences或sqflite。

实验十

在实验二和实验九的基础上完成以下功能:

  1. 增加账号注册功能,注册用户信息保存在App本地SQLite数据库,后续登录时,同SQLite中注册的账号和密码比对,比对成功方可登录。(此时,原有的固定账号密码比对逻辑作废);
  2. 登录成功后,使用provider将实验二的功能尝试重构实现。

上一篇 路由导航与跨页传参 下一篇 HTTP协议与JSON解析