使用 Provider 管理 Flutter 應用狀態 (上)

Cold_Stone發表於2019-09-30

前言

一個應用內通常會有兩種資料,部件內部的使用的臨時性資料以及很多部件使用的全域性性資料,部件內部使用的資料可以通過 StatefulWidget 來管理,但是全域性性的資料如果通過從上到下傳遞的方式會使程式碼寫的十分繁瑣,這時就需要一個狀態管理工具來進行管理了,本文說明如何使用 Provider 來管理這種應用的全域性性的資料

什麼是 Provider

官方的定義是: A mixture between dependency injection (DI) and state management, built with widgets for widgets. 翻譯過來大意是一種依賴注入和狀態管理的混合方案,使用部件建立,作用於部件 ?

官方文件

為什麼要使用 Provider

應用中通常會有一些很多部件都需要的資料,如使用者的登入資訊,使用者設定,地理位置等,如果只是使用 StatefullWeight 的話就需要將狀態提升到一個父部件中然後向下進行傳遞,會很繁瑣,使用 provider 的話可以將對一種狀態資料的操作放到一個檔案內,然後使用到這個資料的部件只需要使用就可以了,當資料有變化時,部件會自動的重新構建,使介面更新。

一個例子 ?

使用一個 todo 應用來說明如何在 Flutter 應用中使用 Provider,最終的完成的應用是這樣的,可以新增,編輯和刪除 todo。

原始碼地址

使用 Provider 管理 Flutter 應用狀態 (上)

建立應用

首先使用命令列建立一個專案

flutter create flutter_provider_todos
複製程式碼

然後在專案的 pubspec.yml 新增 provider

dependencies:
  provider: ^3.1.0
複製程式碼

建立一個 store 資料夾以及 todos.dart 用來存放應用中需要用到的全域性性資料,新建一個 widget 目錄,用來存放應用中的部件以及一個顯示 todo 的頁面 todos_page.dart

使用 Provider 管理 Flutter 應用狀態 (上)

首先建立 todos 這個全域性性的資料,修改 store/todos.dart,建立一個 Todo 類表示一個代辦事項,然後實現 Todos 類, Todos 混合了 ChangeNotifier 類,為了使用 notifyListeners 方法來通知 UI 更新,因此需要匯入 foundation.dart,Todos 類使用一個 _items 陣列存放 Todo 資料,以及其它對 Todo 進行操作的方法。

import 'package:flutter/foundation.dart';

class Todo {
  bool finish;
  String thing;

  Todo({
    @required this.thing,
    this.finish = false,
  });
}

class Todos extends ChangeNotifier {
  List<Todo> _items = [
    Todo(thing: 'Play lol', finish: true),
    Todo(thing: 'Learn flutter', finish: false),
    Todo(thing: 'Read book', finish: false),
    Todo(thing: 'Watch anime', finish: false),
  ];

  get items {
    return [..._items];
  }

  get finishTodos {
    return _items.where((todo) => todo.finish);
  }

  void refresh() {
    notifyListeners();
  }

  void addTodo(Todo todo) {
    _items.insert(0, todo);

    refresh();
  }

  void removeTodo(int index) {
    _items.removeAt(index);

    refresh();
  }

  void editTodo(int index, String newThing, bool isFinish) {
    Todo todo = _items[index];
    todo.thing = newThing;
    todo.finish = isFinish;

    refresh();
  }

  void toggleFinish(int index) {
    final todo = _items[index];
    todo.finish = !todo.finish;

    refresh();
  }

  bool isTodoExist(String thing) {
    bool isExist = false;

    for (var i = 0; i < _items.length; i++) {
      final todo = _items[i];
      if (todo.thing == thing) {
        isExist = true;
      }
    }

    return isExist;
  }
}

複製程式碼

然後使用 provider 提供的 ChangeNotifierProvider 方法將資料註冊到整個應用,如果有多個資料就需要使用 MultiProvider 方法

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

import 'todos_page.dart';
import 'store/todos.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Todos',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        primarySwatch: Colors.green,
      ),
      home: ChangeNotifierProvider(
        builder: (context) => Todos(),
        child: TodosPage(),
      ),
    );
  }
}
複製程式碼

列表頁面

接著就是實現顯示 todo 列表的頁面,這個頁面就是要用到 Todos 類裡面的資料的部件,要使用 provider 的資料首先要匯入 provider 以及對應的資料類 Todos,然後用 Consumer 加型別 Todos 來使用這個資料

Consumer<Todos>(
  builder: (ctx, todos, child) {
    return YourWidget()
  },
)
複製程式碼

這個頁面使用了一個 ListView.builder() 來渲染 Todos,然後每一項使用一個 ListTile 展示。新增,編輯和刪除對應了 3 個不同的部件,分別是 AddTodoButton(),EditTodoButton(), RemoveTodoButton()

// todos_page.dart

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

import 'store/todos.dart';
import 'widget/add_todo_button.dart';
import 'widget/edit_todo_button.dart';
import 'widget/remove_todo_button.dart';

class TodosPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Flutter Provider Todos')),
      body: Consumer<Todos>(
        builder: (ctx, todos, child) {
          List<Todo> items = todos.items;

          return ListView.builder(
            itemCount: items.length,
            itemBuilder: (_, index) => Column(
              children: <Widget>[
                ListTile(
                  title: Text(
                    items[index].thing,
                    style: TextStyle(
                      color: items[index].finish ? Colors.green : Colors.grey,
                    ),
                  ),
                  trailing: Container(
                    width: 150,
                    child: Row(
                      mainAxisAlignment: MainAxisAlignment.end,
                      children: <Widget>[
                        EditTodoButton(todoIndex: index),
                        RemoveTodoButton(todoIndex: index),
                      ],
                    ),
                  ),
                ),
                Divider(),
              ],
            ),
          );
        },
      ),
      floatingActionButton: AddTodoButton(),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
    );
  }
}

複製程式碼

實現功能

接下來就是要實現這 3 個按鈕了,在 widget 目錄建立對應的檔案,每個按鈕都會使用到 Todos 類裡面定義的方法,所以都需要匯入 provider 和 Todos 類,點選按鈕會彈出一個對話方塊詢問對應的操作,

使用 Provider 管理 Flutter 應用狀態 (上)

新增 Todo 按鈕

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

import '../store/todos.dart';

class AddTodoButton extends StatefulWidget {
  @override
  _AddTodoButtonState createState() => _AddTodoButtonState();
}

class _AddTodoButtonState extends State<AddTodoButton> {
  final _formKey = GlobalKey<FormState>();
  final _controller = TextEditingController();

  @override
  void dispose() {
    _formKey.currentState.dispose();
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Consumer<Todos>(
      builder: (_, todos, child) {
        return FloatingActionButton(
          child: Icon(Icons.add),
          onPressed: () {
            print('add todo');
            return showDialog(
              context: context,
              builder: (BuildContext _) {
                return SimpleDialog(
                  title: Text('新增 Todo'),
                  contentPadding: const EdgeInsets.all(24.0),
                  children: <Widget>[
                    Form(
                      key: _formKey,
                      child: Column(
                        children: <Widget>[
                          TextFormField(
                            autofocus: true,
                            autovalidate: false,
                            controller: _controller,
                            keyboardType: TextInputType.text,
                            decoration: InputDecoration(
                              border: OutlineInputBorder(),
                              labelText: '輸入你想做的事',
                            ),
                            validator: (val) {
                              if (val.isEmpty) {
                                return '想做的事不能為空';
                              }

                              bool isExist = todos.isTodoExist(val);

                              if (isExist) {
                                return '這件事情已經存在了';
                              }
                              return null;
                            },
                          ),
                          SizedBox(height: 20),
                          Row(
                            mainAxisAlignment: MainAxisAlignment.end,
                            children: <Widget>[
                              FlatButton(
                                child: Text('取消'),
                                onPressed: () {
                                  Navigator.pop(context);
                                },
                              ),
                              RaisedButton(
                                child: Text(
                                  '確定',
                                  style: TextStyle(color: Colors.white),
                                ),
                                color: Theme.of(context).primaryColor,
                                onPressed: () {
                                  final isValid =
                                      _formKey.currentState.validate();

                                  if (!isValid) {
                                    return;
                                  }

                                  final thing = _controller.value.text;

                                  todos.addTodo(Todo(
                                    thing: thing,
                                    finish: false,
                                  ));
                                  _controller.clear();
                                  Navigator.pop(context);
                                },
                              )
                            ],
                          ),
                        ],
                      ),
                    ),
                  ],
                );
              },
            );
          },
        );
      },
    );
  }
}

複製程式碼
使用 Provider 管理 Flutter 應用狀態 (上)

編輯 Todo 按鈕

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

import '../store/todos.dart';

class EditTodoButton extends StatefulWidget {
  final todoIndex;

  const EditTodoButton({Key key, this.todoIndex}) : super(key: key);

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

class _EditTodoButtonState extends State<EditTodoButton> {
  final _formKey = GlobalKey<FormState>();

  @override
  void dispose() {
    _formKey?.currentState?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Consumer<Todos>(
      builder: (context, todos, child) {
        final todoIndex = widget.todoIndex;
        final Todo todo = todos.items[todoIndex];

        return IconButton(
          color: Colors.blue,
          icon: Icon(Icons.edit),
          onPressed: () {
            return showDialog(
              context: context,
              builder: (context) {
                return SimpleDialog(
                  title: Text('編輯 Todo'),
                  contentPadding: const EdgeInsets.all(24.0),
                  children: <Widget>[
                    Form(
                      key: _formKey,
                      child: Column(
                        children: <Widget>[
                          TextFormField(
                            autofocus: false,
                            autovalidate: false,
                            initialValue: todo.thing,
                            decoration: InputDecoration(
                              border: OutlineInputBorder(),
                              labelText: '輸入你想做的事',
                            ),
                            onChanged: (val) {
                              todo.thing = val;
                            },
                            validator: (val) {
                              if (val.isEmpty) {
                                return '想做的事不能為空';
                              }
                              return null;
                            },
                          ),
                          SizedBox(height: 20),
                          SwitchListTile(
                            title: const Text('是否完成'),
                            value: todo.finish,
                            onChanged: (bool value) {
                              todo.finish = value;
                            },
                          ),
                          SizedBox(height: 20),
                          Row(
                            mainAxisAlignment: MainAxisAlignment.end,
                            children: <Widget>[
                              FlatButton(
                                child: Text('取消'),
                                onPressed: () => Navigator.pop(context),
                              ),
                              RaisedButton(
                                child: Text(
                                  '確定',
                                  style: TextStyle(color: Colors.white),
                                ),
                                color: Theme.of(context).primaryColor,
                                onPressed: () {
                                  final isValid =
                                      _formKey.currentState.validate();

                                  if (!isValid) {
                                    return;
                                  }

                                  Navigator.pop(context);

                                  todos.editTodo(
                                    todoIndex,
                                    todo.thing,
                                    todo.finish,
                                  );
                                },
                              )
                            ],
                          ),
                        ],
                      ),
                    ),
                  ],
                );
              },
            );
          },
        );
      },
    );
  }
}

複製程式碼
使用 Provider 管理 Flutter 應用狀態 (上)

刪除 Todo 按鈕

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

import '../store/todos.dart';

class RemoveTodoButton extends StatelessWidget {
  final int todoIndex;

  const RemoveTodoButton({Key key, this.todoIndex}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Consumer<Todos>(builder: (_, todos, child) {
      final Todo todo = todos.items[todoIndex];

      return IconButton(
        color: Colors.red,
        icon: Icon(Icons.delete),
        onPressed: () {
          print('delete todo');
          showDialog(
            context: context,
            builder: (BuildContext context) {
              return AlertDialog(
                title: Text('確認刪除 ${todo.thing}?'),
                actions: <Widget>[
                  FlatButton(
                    child: Text(
                      '取消',
                      style: TextStyle(color: Colors.grey),
                    ),
                    onPressed: () => Navigator.pop(context),
                  ),
                  FlatButton(
                    child: Text('確認'),
                    onPressed: () {
                      todos.removeTodo(todoIndex);
                      Navigator.pop(context);
                    },
                  ),
                ],
              );
            },
          );
        },
      );
    });
  }
}

複製程式碼
使用 Provider 管理 Flutter 應用狀態 (上)

可以看到要使用對應的方法需要的只是向對應的部件注入這個資料,然後使用就可以了

結語

使用了 provider 後,資料以及對一個 Todo 的操作都放在一個檔案裡面了,不用在多個層級間傳遞資料,並且在資料變化時自動更新了 UI,所以是十分有必要的。

原文地址

使用 Provider 管理 Flutter 應用狀態 (下)

相關文章