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

Cold_Stone發表於2019-09-30

緊接上篇,對於一個代辦事項的新增,修改,刪除功能都已經完成了,但是資料都是儲存在記憶體中的,重新啟動應用資料就重置了,為了儲存資料可以將資料存到手機的儲存裡面或者存到遠端伺服器上,本文就實現如何使用 dio 將資料存到伺服器

原始碼地址

開發準備

pubspec.yaml 新增 dio 依賴;一個儲存資料的服務,我用的是 jsonbox

dependencies:
  dio: ^3.0.1
複製程式碼

配置 dio

由於這個應用只有一個服務地址,所以建立一個 dio 的單例來進行請求就很好了,新建一個 request.dart 檔案配置 dio,使用一個函式返回建立的 dio 例項

  • 設定基礎的請求地址
  • 設定請求超時時間
  • 設定在除錯控制檯輸出請求響應體方便檢視請求

基本設定下就可以用了,其它設定可以檢視 dio 的文件

import 'package:dio/dio.dart';

const DB_URL = 'https://jsonbox.io/box_7ea9df49e805cf99509b';

Dio craeteDio() {
  BaseOptions options = BaseOptions(
    baseUrl: DB_URL,
    connectTimeout: 5000,
    receiveTimeout: 3000,
  );

  Dio dio = Dio(options);

  dio.interceptors.add(LogInterceptor(
    error: true,
    request: false,
    responseBody: true,
    responseHeader: false,
    requestHeader: false,
  ));

  return dio;
}
複製程式碼

修改 Todo 模型

由於需要從伺服器上獲取 todo 資料,服務返回的資料是 json 格式,所以需要在拿到資料的時候將單個 todo 的 json 資料轉成 Todo 例項,新建一個 model/todo.dart 檔案,比之前多的是兩個方法而已,fromJson 這個工廠函式作用是使用 json 資料例項化一個 Todo,toJson 方法用來將一個 Todo 轉成一個 Map 結構的資料

如果一個模型的欄位較少可以手寫,但是當欄位較多比較複雜的時候就需要使用工具來幫助生成程式碼了,我使用的是 quicktype 這個工具

class Todo {
  String id;
  bool finish;
  String thing;

  Todo({
    this.id,
    this.thing,
    this.finish,
  });

  factory Todo.fromJson(Map<String, dynamic> json) => Todo(
        id: json["_id"].toString(),
        thing: json["thing"],
        finish: json["finish"],
      );

  Map<String, dynamic> toJson() => {
        "id": id,
        "thing": thing,
        "finish": finish,
      };
}
複製程式碼

傳送請求

配置好 dio 就可以在 todos.dart 向伺服器傳送請求了,修改 store/todos.dart,給 Todos 類新增了一個 _dio 屬性用來傳送請求,一個 getTodos 方法用來獲取全部 todo 的列表資料,然後修改 addTodo,removeTodo,editTodo 方法使用 _dio 向伺服器傳送 post,delelte,put 請求。

需要注意的一點是將 json 轉換成例項的問題,很容易就會出現類似

type 'List<dynamic>' is not a subtype of type 'List<Todo>'
複製程式碼

這種錯誤,這種都是型別轉換的問題,我看了一篇文章後才算弄懂了一點 parsing-complex-json-in-flutter

import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';

import '../request.dart';
import '../model/todo.dart';

class Todos extends ChangeNotifier {
  List<Todo> _items = [];

  Dio _dio = craeteDio();

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

  void refresh() {
    notifyListeners();
  }

  Future<List> getTodos() async {
    try {
      Response response = await _dio.get('/todos');

      final list = response.data as List;
      _items = List<Todo>.from(list.map((i) => Todo.fromJson(i)).toList());

      return items;
    } on DioError catch (err) {
      throw err;
    }
  }

  Future addTodo(String thing) async {
    try {
      Response response = await _dio.post('/todos', data: {
        "thing": thing,
        "finish": false,
      });

      Todo todo = Todo(
        thing: thing,
        id: response.data["_id"],
        finish: response.data["finish"],
      );

      _items.insert(0, todo);
      refresh();
    } on DioError catch (err) {
      throw err;
    }
  }

  Future removeTodo(int index) async {
    try {
      String todoId = _items[index].id;
      await _dio.delete("/todos/$todoId");
      _items.removeAt(index);
      refresh();
    } catch (err) {
      throw err;
    }
  }

  Future editTodo(int index, String thing, bool finish) async {
    String todoId = _items[index].id;

    try {
      await _dio.put("/todos/$todoId", data: {
        "thing": thing,
        "finish": finish,
      });

      Todo todo = _items[index];
      todo.thing = thing;
      todo.finish = finish;
      refresh();
    } catch (e) {
      throw e;
    }
  }

  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;
  }
}

複製程式碼

使用資料

有了資料後就可以在列表頁使用了,由於現在資料是從伺服器返回的,會有請求耗時,所以需要使用 FutureBuilder 這個部件渲染列表,FutureBuilder 需要一個設定一個 future 來判斷狀態,這裡自然是 Todos 類的 getTodos 方法返回的 Future 物件,然後 builder 就是一個函式,有兩個引數,一個是 context 上下文物件,一個是 snapshot 物件,表示的是這個 future 的狀態。

在 builder 方法裡面用一個 switch 語句判斷這個 future 的狀態,根據狀態返回需要渲染的部件,有以下幾種狀態 none(狀態不存在),active(執行中),waiting(等待中),done(完成),如果都不匹配就,返回一個 null 值。

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: FutureBuilder(
        future: Provider.of<Todos>(context).getTodos(),
        builder: (context, snapshot) {
          switch (snapshot.connectionState) {
            case ConnectionState.none:
              return Text('Press button to start.');
            case ConnectionState.active:
            case ConnectionState.waiting:
              return Center(child: CircularProgressIndicator());
            case ConnectionState.done:
              if (snapshot.hasError) {
                print(snapshot.error);
                return Center(
                  child: Text(
                    '出錯了,請重試',
                    style: TextStyle(fontSize: 18.0, color: Colors.red),
                  ),
                );
              }

              List items = snapshot.data;

              if (items == null) {
                return Center(
                  child: Text(
                    '還沒有代辦事項,快去新增吧',
                    style: TextStyle(fontSize: 18.0),
                  ),
                );
              }

              return ListView.builder(
                  itemCount: items.length,
                  itemBuilder: (_, index) {
                    return 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(),
                      ],
                    );
                  });
          }
          return null;
        },
      ),
      floatingActionButton: Consumer<Todos>(
        builder: (_, todos, child) {
          return AddTodoButton();
        },
      ),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
    );
  }
}
複製程式碼

修改按鈕

接下來就是需要修改新增,編輯,刪除代辦的按鈕了,同理由於現在需要跟服務端進行通訊,所以需要根據請求狀態來處理邏輯,主要的修改就是使用 async/await 語法等到一個請求完成後,根據返回值進行處理。

新增 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) {
        _addTodo() async {
          final isValid = _formKey.currentState.validate();

          if (!isValid) {
            return;
          }

          final thing = _controller.value.text;

          try {
            await todos.addTodo(thing);
            Navigator.pop(context);
            _controller.clear();
          } catch (e) {
            Scaffold.of(context).showSnackBar(
              SnackBar(content: Text('新增代辦失敗了,請重試。')),
            );
          }
        }

        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: _addTodo,
                              ),
                            ],
                          ),
                        ],
                      ),
                    ),
                  ],
                );
              },
            );
          },
        );
      },
    );
  }
}

複製程式碼

編輯 Todo 按鈕

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

import '../model/todo.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: (_) {
                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: () async {
                                  final isValid =
                                      _formKey.currentState.validate();

                                  if (!isValid) {
                                    return;
                                  }

                                  try {
                                    await todos.editTodo(
                                      todoIndex,
                                      todo.thing,
                                      todo.finish,
                                    );
                                    Navigator.pop(context);
                                  } catch (e) {
                                    Scaffold.of(context).showSnackBar(
                                      SnackBar(content: Text('修改代辦失敗了,請重試。')),
                                    );
                                  }
                                },
                              )
                            ],
                          ),
                        ],
                      ),
                    ),
                  ],
                );
              },
            );
          },
        );
      },
    );
  }
}
複製程式碼

刪除 Todo 按鈕

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

import '../model/todo.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: () async {
                      await todos.removeTodo(todoIndex);
                      Navigator.pop(context);
                    },
                  ),
                ],
              );
            },
          );
        },
      );
    });
  }
}
複製程式碼

結語

至此所有的資料都儲存在伺服器上了,重啟應用資料也會從伺服器上獲取了。

原文地址

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

相關文章