使用 Flutter&&Hive&&Bloc 寫一個待辦小demo


 

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

 

創建兩個抽象方法,分別是

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');

}

嗯,先這樣,乏了,看心情更新吧

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM