使用 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