Github 地址:GITHUB
沒圖說個錘子,先上效果圖:
最簡陋那個是測試直接通過hive讀數據
開發環境:
老規矩先上環境:
- Flutter SDK ---->2.5.3 & Dart sdk 2.14.4
- flutter_bloc ^7.0.0
- Jdk 1.8
插件依賴
創建項目 並依賴下方插件
flutter_slidable: ^1.0.0
hive: ^2.0.4
hive_flutter: ^1.1.0
uuid: ^3.0.5
material_design_icons_flutter: ^5.0.6295
flutter_material_color_picker: ^1.1.0+2
google_fonts: ^2.1.0
animated_text_kit: ^4.2.1
intl: ^0.17.0
bloc: ^7.0.0
equatable: ^2.0.0
flutter_bloc: ^7.0.0
meta: ^1.3.0
在項目根目錄創建 packages的路徑,如下圖
創建項目工具包
新建兩個包選擇項目根目錄的packages位置(如圖):
選擇packagey因為我們只是在這邊寫一些方法,如果創建插件項目就太大並不合適
兩個包名分別為:
todos_repository
todo_repository_simple
todo_repository的編寫
在 todo_repository 包添加 依賴
hive: ^2.0.4
hive_flutter: ^1.1.0
創建一個 todo_modle 的dart文件寫入如下代碼
在Teminal中cd 到 todo_repository 的目錄
執行 這行代碼,生成.g文件。也就是 todo_model 的適配器,可以自己手寫
flutter packages pub run build_runner build
如果報錯嘗試執行這條
flutter packages pub run build_runner build --delete-conflicting-outputs
如果還是報錯那就直接copy這里的代碼
todo_model:
// GENERATED CODE - DO NOT MODIFY BY HAND part of 'todo_model.dart'; // ************************************************************************** // TypeAdapterGenerator // ************************************************************************** class TodoCategoryAdapter extends TypeAdapter<TodoCategory> { @override final int typeId = 1; @override TodoCategory read(BinaryReader reader) { switch (reader.readByte()) { case 0: return TodoCategory.personal; case 1: return TodoCategory.work; case 2: return TodoCategory.shopping; default: return TodoCategory.work; } } @override void write(BinaryWriter writer, TodoCategory obj) { switch (obj) { case TodoCategory.personal: writer.writeByte(0); break; case TodoCategory.work: writer.writeByte(1); break; case TodoCategory.shopping: writer.writeByte(2); break; } } @override int get hashCode => typeId.hashCode; @override bool operator ==(Object other) => identical(this, other) || other is TodoCategoryAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } class MyColorAdapter extends TypeAdapter<MyColor> { @override final int typeId = 2; @override MyColor read(BinaryReader reader) { switch (reader.readByte()) { case 0: return MyColor.red; case 1: return MyColor.orange; case 2: return MyColor.teal; case 3: return MyColor.pink; case 4: return MyColor.blueGrey; case 5: return MyColor.blue; case 6: return MyColor.purple; default: return MyColor.red; } } @override void write(BinaryWriter writer, MyColor obj) { switch (obj) { case MyColor.red: writer.writeByte(0); break; case MyColor.orange: writer.writeByte(1); break; case MyColor.teal: writer.writeByte(2); break; case MyColor.pink: writer.writeByte(3); break; case MyColor.blueGrey: writer.writeByte(4); break; case MyColor.blue: writer.writeByte(5); break; case MyColor.purple: writer.writeByte(6); break; } } @override int get hashCode => typeId.hashCode; @override bool operator ==(Object other) => identical(this, other) || other is MyColorAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } class TodoModelAdapter extends TypeAdapter<TodoModel> { @override final int typeId = 0; @override TodoModel read(BinaryReader reader) { final numOfFields = reader.readByte(); final fields = <int, dynamic>{ for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; return TodoModel( content: fields[0] as String, done: fields[1] as bool, time: fields[2] as DateTime, category: fields[3] as TodoCategory, color: fields[4] as MyColor, ); } @override void write(BinaryWriter writer, TodoModel obj) { writer ..writeByte(5) ..writeByte(0) ..write(obj.content) ..writeByte(1) ..write(obj.done) ..writeByte(2) ..write(obj.time) ..writeByte(3) ..write(obj.category) ..writeByte(4) ..write(obj.color); } @override int get hashCode => typeId.hashCode; @override bool operator ==(Object other) => identical(this, other) || other is TodoModelAdapter && runtimeType == other.runtimeType && typeId == other.typeId; }
todo_model.g.dart:

// GENERATED CODE - DO NOT MODIFY BY HAND part of 'todo_model.dart'; // ************************************************************************** // TypeAdapterGenerator // ************************************************************************** class TodoCategoryAdapter extends TypeAdapter<TodoCategory> { @override final int typeId = 1; @override TodoCategory read(BinaryReader reader) { switch (reader.readByte()) { case 0: return TodoCategory.personal; case 1: return TodoCategory.work; case 2: return TodoCategory.shopping; default: return TodoCategory.work; } } @override void write(BinaryWriter writer, TodoCategory obj) { switch (obj) { case TodoCategory.personal: writer.writeByte(0); break; case TodoCategory.work: writer.writeByte(1); break; case TodoCategory.shopping: writer.writeByte(2); break; } } @override int get hashCode => typeId.hashCode; @override bool operator ==(Object other) => identical(this, other) || other is TodoCategoryAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } class MyColorAdapter extends TypeAdapter<MyColor> { @override final int typeId = 2; @override MyColor read(BinaryReader reader) { switch (reader.readByte()) { case 0: return MyColor.red; case 1: return MyColor.orange; case 2: return MyColor.teal; case 3: return MyColor.pink; case 4: return MyColor.blueGrey; case 5: return MyColor.blue; case 6: return MyColor.purple; default: return MyColor.red; } } @override void write(BinaryWriter writer, MyColor obj) { switch (obj) { case MyColor.red: writer.writeByte(0); break; case MyColor.orange: writer.writeByte(1); break; case MyColor.teal: writer.writeByte(2); break; case MyColor.pink: writer.writeByte(3); break; case MyColor.blueGrey: writer.writeByte(4); break; case MyColor.blue: writer.writeByte(5); break; case MyColor.purple: writer.writeByte(6); break; } } @override int get hashCode => typeId.hashCode; @override bool operator ==(Object other) => identical(this, other) || other is MyColorAdapter && runtimeType == other.runtimeType && typeId == other.typeId; } class TodoModelAdapter extends TypeAdapter<TodoModel> { @override final int typeId = 0; @override TodoModel read(BinaryReader reader) { final numOfFields = reader.readByte(); final fields = <int, dynamic>{ for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), }; return TodoModel( content: fields[0] as String, done: fields[1] as bool, time: fields[2] as DateTime, category: fields[3] as TodoCategory, color: fields[4] as MyColor, ); } @override void write(BinaryWriter writer, TodoModel obj) { writer ..writeByte(5) ..writeByte(0) ..write(obj.content) ..writeByte(1) ..write(obj.done) ..writeByte(2) ..write(obj.time) ..writeByte(3) ..write(obj.category) ..writeByte(4) ..write(obj.color); } @override int get hashCode => typeId.hashCode; @override bool operator ==(Object other) => identical(this, other) || other is TodoModelAdapter && runtimeType == other.runtimeType && typeId == other.typeId; }
創建兩個抽象方法,分別是
TodoRepository
abstract class TodoRepository { /// 加載待辦事項 Future<List<TodoModel>> loadTodos(); /// 保存待辦事項 Future saveTodos(List<TodoModel> todos); }
ReactiveTodosRepository
abstract class ReactiveTodosRepository { /// 添加新的待辦 Future<void> addNewTodo(TodoModel todo); /// 刪除待辦事項 Future<void> deleteTodo(List<String> idList); /// 獲取全部待辦事項 Stream<List<TodoModel>> todos(); /// 更新待辦事項 Future<void> updateTodo(TodoModel todo); }
熟悉java的都知道,這和java項目中定義的接口是一樣的,應該都能看懂
todos_repository_simple
todos_repository的代碼基本上就這樣,接下來看看 todo_repository_simple
在todo_repository_simple 我們要做的事情是請求網絡數據或者本地數據,並將數據返回到頁面給到用戶觀看的一個方法
首先還是添加插件
hive: ^2.0.4
hive_flutter: ^1.1.0
todos_repository:
path: ../todos_repository
hive插件我們已經分別添加了三次,因為項目里不能像安卓那樣直接通過根項目拿到依賴,所以只能重新再依賴一次
而 todos_repository則是我們剛剛寫的插件,我們需要在這邊去實現它的方法,所以就得把它依賴進來
好,那接下來創建一個本地存儲的方法
FileStorage代碼:
class FileStorage { final String tag; final Future<Directory> Function() getDirectory; const FileStorage( this.tag, this.getDirectory, ); Future<List<TodoModel>> loadTodos() async { final todoBox = await Hive.openBox<TodoModel>("todos"); List<TodoModel> todos = []; todos.addAll(todoBox.values.map((e) => e).toList()); return todos; } Future saveTodos(List<TodoModel> todos) async { final settingsBox = await Hive.openBox<bool>('settings'); final todosBox = await Hive.openBox<TodoModel>('todos'); await todosBox.addAll(todos); await settingsBox.put('initialized', true); } Future saveTodo(TodoModel todoModel) async { if (todoModel.isInBox) { final key = todoModel.key; Hive.box<TodoModel>('todos').put(key, todoModel); } else { await Hive.box<TodoModel>('todos').add(todoModel); } } }
在這里實現兩個方法,其中 loadTodos 是通過 hive中讀取用戶的全部待辦事項,當然,當前 hive box 還沒初始化,如果現在直接調用肯定是會報錯的,等會會在根項目中進行初始化
saveTodos 保存全部待辦事項
創建一個 WebClient
這個方法理應上是獲取網絡的數據,不過目前沒有搭后台,所以就先寫個模擬數據用用
WebClient:
class WebClient { final Duration delay; const WebClient([this.delay = const Duration(milliseconds: 3000)]); Future<List<TodoModel>> fetchTodos() async { return Future.delayed( delay, () => [ TodoModel( category: TodoCategory.personal, color: MyColor.purple, content: '去散步', done: false, time: DateTime.now().subtract(const Duration(days: 1))), TodoModel( category: TodoCategory.shopping, color: MyColor.orange, content: '去工作', done: false, time: DateTime.now().subtract(const Duration(days: 2))), TodoModel( category: TodoCategory.work, color: MyColor.blueGrey, content: '去運動', done: true, time: DateTime.now().subtract(const Duration(days: 3))) ]); } Future<bool> postTodos(List<TodoModel> todos) async { return Future.value(true); } }
可以看到上面設置一個Duration的東西,用來模擬請求網絡的一個延遲效果
fetchTodos方法中則是模擬獲取數據的
postTodos 是往服務器推待辦數據,還是因為后台沒寫,擱置先,后面有心情在繼續更
創建TodosRepositoryFlutter
這個方法是繼承todo_repository的方法,並通過上面寫的兩個方法獲得數據,代碼如下
// Copyright 2018 The Flutter Architecture Sample Authors. All rights reserved. // Use of this source code is governed by the MIT license that can be found // in the LICENSE file. import 'dart:async'; import 'dart:core'; import 'package:meta/meta.dart'; import 'package:todos_repository/todo_repository_core.dart'; import 'file_storage.dart'; import 'web_client.dart'; class TodosRepositoryFlutter implements TodoRepository { final FileStorage fileStorage; final WebClient webClient; const TodosRepositoryFlutter({ required this.fileStorage, this.webClient = const WebClient(), }); ///首先從文件存儲中加載待辦事項,如果不存在則通過web端去加載 @override Future<List<TodoModel>> loadTodos() async { try { final todos = await fileStorage.loadTodos(); if(todos.isEmpty){ final todos = await webClient.fetchTodos(); fileStorage.saveTodos(todos); return todos; } return todos; } catch (e) { final todos = await webClient.fetchTodos(); fileStorage.saveTodos(todos); return todos; } } //將TODO持久化到本地磁盤和web @override Future saveTodos(List<TodoModel> todos) { return Future.wait<dynamic>([ fileStorage.saveTodos(todos), webClient.postTodos(todos), ]); } @override Future saveTodo(TodoModel todoModel) { return Future.wait<dynamic>([ fileStorage.saveTodo(todoModel) ]); } }
其實 saveTodo 並不應該寫在這里,這個方法最開始想要達到得結果是讀取全部數據和添加全部數據,
而像添加新的待辦事項和刪除等操作是應該寫另一個地方,但是這里出於偷懶得原因,直接就寫這里了,后續完善后會進行修改
那todo_repository_simple的代碼也就暫時告一段落,接下來就是根項目的編寫了
回到 flutter_bloc_hive_todo中
首先還是依賴包,依賴我們剛剛寫的兩個包
todo_repository_simple:
path: packages/todo_repository_simple
todos_repository:
path: packages/todos_repository
在lib中創建兩個個文件夾 分別是 blocs 和view
先來看看bloc。在AndroidStudio中安裝bloc插件
然后右鍵創建文件的時候就會出現,方便快速創建初始代碼
BLOC
在使用bloc前,我們需要了解到Bloc是個什么玩意,emmm網上的文章很多,說得都非常詳細,我就不班門弄斧了,只說說我的理解和感受
Bloc 給我的感覺就是
View 通過 event 去調用bloc的方法 bloc在通過修改state 去修改界面顯示的信息,互不干擾,這倒是和java的mvp模式有些異曲同工之處
好了,來看看state的代碼
abstract class TodosState extends Equatable { const TodosState(); @override List<Object> get props => []; } /// 待辦事項加載中 class TodosLoadInProgress extends TodosState {} /// 待辦事項加載完成 class TodosLoadSuccess extends TodosState { /// 待辦事項集合 final List<TodoModel> todos; const TodosLoadSuccess({this.todos = const []}); @override List<Object> get props => [todos]; @override String toString() => 'TodosLoadSuccess { todos: $todos }'; } /// 待辦事項加載失敗 class TodosLoadFailure extends TodosState {} /// 添加待辦失敗 class TodoAddFailure extends TodosState{}
可以看到除了加載成功的狀態里有寫方法,其他三個均不用理會,因為它們嫩不會返回數據給到界面,我們僅需知道它是什么狀態即可
再來看看 event
abstract class TodosEvent extends Equatable { const TodosEvent(); @override List<Object> get props => []; } /// 待辦事項加載中 class TodosLoaded extends TodosEvent {} ///添加一個待辦事項 class TodoAdded extends TodosEvent{ final TodoModel todo; const TodoAdded(this.todo); @override List<Object> get props => [todo]; @override String toString() => 'TodoAdded { todo: $todo }'; } /// 更新一個待辦事項 class TodoUpdated extends TodosEvent{ final TodoModel todo; const TodoUpdated(this.todo); @override List<Object> get props => [todo]; @override String toString() => 'TodoAdded { todo: $todo }'; } /// 刪除一個待辦事項 class TodoDeleted extends TodosEvent{ final TodoModel todo; const TodoDeleted(this.todo); @override List<Object> get props => [todo]; @override String toString() => 'TodoDeleted { todo: $todo }'; } /// 清空全部待辦事項 class ClearCompleted extends TodosEvent {} /// 選中全部待辦事項 class ToggleAll extends TodosEvent {}
另外兩個方法沒想好怎么做,暫時不寫,刪除添加等代碼其實都一樣,都是傳入一個 待辦對象,應該沒什么好說的
所以。直接來看 bloc
bloc:
class TodosBloc extends Bloc<TodosEvent, TodosState> { /// 待辦事項方法 final TodosRepositoryFlutter todosRepository; TodosBloc(this.todosRepository) : super(TodosLoadInProgress()) { on<TodosEvent>((event, emit) { // TODO: implement event handler }); on<TodosLoaded>(_onLoaded); on<TodoAdded>(_onAddTodo); } /// 加載待辦事項 void _onLoaded(TodosEvent event, Emitter<TodosState> emit) async { try { /// 獲得初始默認待辦事項 final todos = await todosRepository.loadTodos(); emit(TodosLoadSuccess( todos: todos, )); } catch (e) { emit(TodosLoadFailure()); } } /// 添加待辦事項 void _onAddTodo(TodoAdded event, Emitter<TodosState> emit) async { try { if (state is TodosLoadSuccess) { emit(TodosLoadSuccess( todos: List.from((state as TodosLoadSuccess).todos) ..add(event.todo))); _saveTodo(event.todo); } } catch (e) { emit(TodoAddFailure()); } } /// 刪除待辦事項 void _onDelTodo(TodosEvent event, Emitter<TodosState> emit) async {} /// 修改待辦事項 void _onUpdateTodo(TodosEvent event, Emitter<TodosState> emit) async {} /// 將todo事項持久化 Future _saveTodo(TodoModel todo) { return todosRepository.saveTodo(todo); } }
如果有用過bloc的都會發現 沒有 mapEventToState 方法,這個方法在以前是必備的,所有的event都是通過它來判斷,但是在7.0后,官方已經逐漸棄用 mapEventToState 取而代之的是 on的這種寫法,mapEventToState 將會在flutter_bloc 8.0版本徹底移除,所以這也是我寫這篇文章的原因
仔細看這行代碼,我們在super中定義默認的bloc狀態是加載中,你也可以寫其他狀態,但是 這個狀態必須是繼承你創建的state,而不能直接繼承Equatable
TodosBloc(this.todosRepository) : super(TodosLoadInProgress())
這個 TodosRepositoryFlutter 則是我們剛剛寫的插件,我們通過它來獲得待辦數據
final TodosRepositoryFlutter todosRepository;
在 _onLoaded 我們調用它的 loadTodos 方法,這個方法我們剛才的做法是如果沒有本地數據就會獲取網絡數據,當然只是模擬請求后台
而后通過 emit 去修改state 狀態,並將待辦數據傳給 成功的狀態的 待辦列表,如果出現異常則返回 TodosLoadFailure
在添加待辦事項中,我們是是直接將添加的待辦數據添加到 state中,然后才會進行網絡很本地存儲
View
接下來在view中創建這三個文件
因為我們是顯示state中的數據,所以為了查看hive中是否存儲成功了,所以就創建一個test的界面測試
那先看最簡單的的test,很簡單,就是監聽hive中有沒有數據變化,有變化就修改界面
class TestPage extends StatefulWidget { const TestPage({Key? key}) : super(key: key); @override _TestPageState createState() => _TestPageState(); } class _TestPageState extends State<TestPage> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(), body: ValueListenableBuilder<Box<TodoModel>>( valueListenable: Hive.box<TodoModel>('todos').listenable(), builder: (context, box, _) { final todos = box.values.toList(); return ListView.builder(itemBuilder: (context,index){ return Text(todos[index].content!); },itemCount: todos.length,); }), ); } }
todo_editor_dialog是彈窗添加待辦的一個彈出界面
代碼如下:
import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc_hive_todo/blocs/todos/todos_bloc.dart'; import 'package:flutter_material_color_picker/flutter_material_color_picker.dart'; import 'package:hive/hive.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:todos_repository/todo_repository_core.dart'; class TodoEditorDialog extends StatefulWidget { const TodoEditorDialog({Key? key,required this.todo}):super(key: key); final TodoModel todo; @override _TodoEditorDialogState createState() => _TodoEditorDialogState(); } class _TodoEditorDialogState extends State<TodoEditorDialog> { final TodoModel _todo = TodoModel(); @override void initState() { super.initState(); _todo.content = widget.todo.content; _todo.category = widget.todo.category; _todo.time = widget.todo.time; _todo.color = widget.todo.color; _todo.done = widget.todo.done; } @override Widget build(BuildContext context) { return Dialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), child: Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.all(8.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Align( alignment: Alignment.centerLeft, child: Text( '添加新待辦', style: TextStyle(fontSize: 20), ), ), if (widget.todo.isInBox) FloatingActionButton( heroTag: 'remove_todo_button', child: const Icon(MdiIcons.delete), backgroundColor: Colors.red, foregroundColor: Colors.white, mini: true, onPressed: () => _deleteHandle(context)), FloatingActionButton( heroTag: 'add_todo_button', child: const Icon(MdiIcons.contentSave), backgroundColor: Colors.green, foregroundColor: Colors.white, onPressed: () => _saveHandle(context)), ], ), ), const SizedBox(height: 10), TextField( controller: TextEditingController(text: _todo.content), autofocus: true, cursorColor: Colors.deepOrange, onChanged: (value) { _todo.content = value; }, onEditingComplete: () async { // todo : save() }, decoration: const InputDecoration( focusedBorder: OutlineInputBorder( borderSide: BorderSide(color: Colors.deepOrange)), prefixIcon: Icon( MdiIcons.rocketLaunch, color: Colors.deepOrange, ), hintText: 'do...'), ), SingleChildScrollView( scrollDirection: Axis.horizontal, child: MaterialColorPicker( shrinkWrap: true, allowShades: false, circleSize: 32, colors: const [ Colors.red, Colors.orange, Colors.teal, Colors.pink, Colors.blueGrey, Colors.blue, Colors.purple, ], onMainColorChange: (color) { _todo.color = color!.toMyColor(); }, selectedColor: _todo.color!.toColor()), ), CategoryPicker( initialCategory: _todo.category ?? TodoCategory.personal, categoryOnChanged: (category) { _todo.category = category; }, ), ], ), ), ); } Future<void> _saveHandle(BuildContext context) async { _todo.time = DateTime.now(); BlocProvider.of<TodosBloc>(context).add( TodoAdded(_todo), ); // // if (widget.todo.isInBox) { // final key = widget.todo.key; // Hive.box<TodoModel>('todos').put(key, _todo); // } else { // await Hive.box<TodoModel>('todos').add(_todo); // } // Navigator.pop(context); } Future<void> _deleteHandle(BuildContext context) async { if (widget.todo.isInBox) await widget.todo.delete(); Navigator.pop(context); } } class CategoryPicker extends StatefulWidget { const CategoryPicker({Key? key, required this.categoryOnChanged, required this.initialCategory}) : super(key: key); final ValueChanged<TodoCategory> categoryOnChanged; final TodoCategory initialCategory; @override _CategoryPickerState createState() => _CategoryPickerState(); } class _CategoryPickerState extends State<CategoryPicker> { var _isSelected = List.generate(3, (index) => false); @override void initState() { super.initState(); final index = TodoCategory.values.indexOf(widget.initialCategory); _isSelected[index] = true; } @override Widget build(BuildContext context) { return ToggleButtons( fillColor: Colors.white.withOpacity(0.1), onPressed: (index) { setState(() { _isSelected = _isSelected.map((e) => e = false).toList(); _isSelected[index] = true; }); final category = TodoCategory.values[index]; widget.categoryOnChanged(category); }, children: const [ _ToggleButtonContainer( icon: Icon(Icons.person), color: Colors.red, label: '暫定'), _ToggleButtonContainer( icon: Icon(Icons.work), color: Colors.blue, label: '工作'), _ToggleButtonContainer( icon: Icon(MdiIcons.shopping), color: Colors.yellow, label: '購物') ], isSelected: _isSelected); } } class _ToggleButtonContainer extends StatelessWidget { const _ToggleButtonContainer({Key? key, required this.color, required this.icon, required this.label}) : super(key: key); final Color color; final Icon icon; final String label; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8), child: Row( mainAxisSize: MainAxisSize.min, children: [ IconTheme(data: IconThemeData(color: color), child: icon), const SizedBox(width: 4), Text( label, style: TextStyle(color: color), ), ], ), ); } }
todo_item.dart則是每一個待辦的子項
todo_item:
import 'package:flutter/material.dart'; import 'package:todos_repository/todo_repository_core.dart'; class TodoItem extends StatelessWidget { final DismissDirectionCallback onDismissed; final GestureTapCallback onTap; final ValueChanged<bool> onCheckboxChanged; final TodoModel todo; const TodoItem({ Key? key, required this.onDismissed, required this.onTap, required this.onCheckboxChanged, required this.todo, }) : super(key: key); @override Widget build(BuildContext context) { return Dismissible( onDismissed: onDismissed, key: Key("__TodoItem__"), child: ListTile( onTap: onTap, leading: Checkbox( value: todo.done, onChanged: (bool? value) { }, // onChanged: onCheckboxChanged, ), title: Hero( tag: '${todo.category}__heroTag', child: Container( width: MediaQuery.of(context).size.width, child: Text( todo.content??"空", style: Theme.of(context).textTheme.headline6, ), ), ), subtitle: Text( "${todo.time}", maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.subtitle1, ), ), ); } }
todo_home就是todo顯示的界面了,需要調用todo_item,所以先貼出item的代碼
import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc_hive_todo/blocs/todos/todos.dart'; import 'package:flutter_bloc_hive_todo/view/test.dart'; import 'package:flutter_bloc_hive_todo/view/todo_editor_dialog.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; import 'package:todos_repository/todo_repository_core.dart'; class TodoHome extends StatefulWidget { const TodoHome({Key? key}) : super(key: key); @override _TodoHomeState createState() => _TodoHomeState(); } class _TodoHomeState extends State<TodoHome> { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("待辦事項"), actions: [ IconButton( onPressed: () { //路由跳轉 固定寫法 PageA 為目標頁面類名 Navigator.of(context) .push(MaterialPageRoute(builder: (context) => TestPage())); }, icon: const Icon(Icons.add)) ], ), floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, floatingActionButton: FloatingActionButton( foregroundColor: Colors.white, backgroundColor: Colors.red, heroTag: 'add_todo_button', onPressed: () => showTodoEditorDialog(context), child: const Icon(MdiIcons.rocketLaunch), ), body: BlocBuilder<TodosBloc, TodosState>( builder: (context, state) { if (state is TodosLoadInProgress) { return const Text("加載中"); } else if (state is TodosLoadSuccess) { return CustomScrollView( slivers: [ SliverList( delegate: SliverChildListDelegate([ const SizedBox(height: 10), ...state.todos.map( (todo) => InkWell( onTap: () { showTodoEditorDialog(context, todo: todo); }, child: Slidable( startActionPane: ActionPane( motion: const ScrollMotion(), dismissible: DismissiblePane(onDismissed: () {}), children: [ SlidableAction( onPressed: (BuildContext context) {}, flex: 2, backgroundColor: const Color(0xFFFE4A49), foregroundColor: Colors.white, icon: Icons.delete, label: '刪除', ), ], ), endActionPane: ActionPane( motion: const ScrollMotion(), children: [ SlidableAction( flex: 2, onPressed: (BuildContext context) async {}, backgroundColor: const Color(0xFF7BC043), foregroundColor: Colors.white, icon: Icons.radio_button_unchecked, label: '完成', ), ], ), child: Card( elevation: 0, child: SizedBox( height: 60, child: Row( children: [ Padding( padding: const EdgeInsets.symmetric( horizontal: 8.0), child: todo.done! ? Stack( alignment: Alignment.center, children: const [ Icon( MdiIcons.check, color: Colors.amberAccent, size: 18, ), Icon(MdiIcons.circleOutline, color: Colors.red), ], ) : const Icon(MdiIcons.circleOutline, color: Colors.cyan), ), const SizedBox(width: 10), Expanded( child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Opacity( opacity: todo.done! ? 0.4 : 1, child: Text( todo.content ?? "空", style: TextStyle( fontSize: 17, fontWeight: todo.done! ? FontWeight.w100 : FontWeight.normal, decoration: TextDecoration.lineThrough), ), ), const SizedBox(height: 6), Opacity( opacity: 0.4, child: Row( children: const [ Icon( Icons.date_range, size: 12, ), SizedBox(width: 4), Text("'MM-dd-yyyy HH:mm'", style: TextStyle( fontSize: 12, )) ], ), ) ], ), ), Padding( padding: const EdgeInsets.all(8.0) + const EdgeInsets.symmetric(horizontal: 8), child: const Icon(Icons.person, color: Colors.red), ) ], ), ), ), ), ), ) ].toList())) ], ); } return const Text("加載錯誤"); }, ), ); } } void showTodoEditorDialog(BuildContext context, {TodoModel? todo}) { final _todo = todo ?? TodoModel( color: MyColor.red, category: TodoCategory.personal, content: '', time: DateTime.now(), done: false); Navigator.push( context, PageRouteBuilder( fullscreenDialog: true, opaque: false, barrierDismissible: true, transitionsBuilder: (context, animation, secondaryAnimation, child) { if (animation.status == AnimationStatus.reverse) { return SlideTransition( position: Tween<Offset>( begin: const Offset(0, 1.0), end: Offset.zero, ).animate(animation), child: child); } else { return SlideTransition( position: Tween<Offset>( begin: const Offset(0, 1.0), end: Offset.zero, ).animate(animation), child: child); } }, pageBuilder: (context, _, __) => TodoEditorDialog(todo: _todo))); }
好了最后一個main就完事了
Main:
import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc_hive_todo/view/todo_home.dart'; import 'package:hive/hive.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:path_provider/path_provider.dart'; import 'package:todo_repository_simple/todo_repository_simple.dart'; import 'package:todos_repository/todo_repository_core.dart'; import 'blocs/todos/todos_bloc.dart'; void main() async{ await _hiveSetup(); runApp( BlocProvider( create: (context) { return TodosBloc( const TodosRepositoryFlutter( fileStorage: FileStorage( '__flutter_bloc_app__', getApplicationDocumentsDirectory, ), ), )..add(TodosLoaded()); }, child: App(), ), ); } class App extends StatelessWidget { const App({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return const MaterialApp(title: "待辦事項", home: TodoHome()); } } Future<void> _hiveSetup() async { await Hive.initFlutter(); Hive.registerAdapter(TodoCategoryAdapter()); Hive.registerAdapter(MyColorAdapter()); Hive.registerAdapter(TodoModelAdapter()); await Hive.openBox<TodoModel>('todos'); }
嗯,先這樣,乏了,看心情更新吧