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

緊接上篇,對於一個代辦事項的新增,修改,刪除功能都已經完成了,可是數據都是保存在內存中的,從新啓動應用數據就重置了,爲了存儲數據能夠將數據存到手機的存儲裏面或者存到遠程服務器上,本文就實現如何使用 dio 將數據存到服務器git

源碼地址github

開發準備

pubspec.yaml 添加 dio 依賴;一個存儲數據的服務,我用的是 jsonboxjson

dependencies:
 dio: ^3.0.1
複製代碼

配置 dio

因爲這個應用只有一個服務地址,因此建立一個 dio 的單例來進行請求就很好了,新建一個 request.dart 文件配置 dio,使用一個函數返回建立的 dio 實例服務器

  • 設置基礎的請求地址
  • 設置請求超時時間
  • 設置在調試控制檯輸出請求響應體方便查看請求

基本設置下就能夠用了,其它設置能夠查看 dio 的文檔app

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 結構的數據less

若是一個模型的字段較少能夠手寫,可是當字段較多比較複雜的時候就須要使用工具來幫助生成代碼了,我使用的是 quicktype 這個工具async

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 請求。ide

須要注意的一點是將 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 應用狀態 (上)

相關文章
相關標籤/搜索