【Flutter】Todoアプリを作ってみた

2022/01/04に公開

はじめに

Flutterを用いてシンプルなTodoアプリを作成してみた。

完成形を以下のようなる。

使用パッケージ

hooks_riverpod: ^1.0.3

https://pub.dev/packages/hooks_riverpod

実装

Todoリストの状態を保持するクラスの作成

Todoリストの状態を保持するクラスを作成。

class Todo {
  Todo(this.id, this.title, this.check, this.focusNode);
  int id;
  String title;
  bool check;
  FocusNode focusNode;
}

「check」はチェックボックスの値の管理の為、「focusNode」はテキスト入力時のフォーカスを管理する為に使用。

ViewModelの作成

todoリストの管理を行うViewModelを作成。

class TodoViewModel extends ChangeNotifier {
  List<Todo> _todoList = [];
  UnmodifiableListView<Todo> get todoList => UnmodifiableListView(_todoList);

  void createTodo(String title) {
    final id = _todoList.length + 1;
    _todoList = [...todoList, Todo(id, title, false, FocusNode())];
    notifyListeners();
  }

  void updateTodo(int id, String title) {
    todoList.asMap().forEach((int index, Todo todo) {
      if (todo.id == id) {
        _todoList[index].title = title;
      }
    });
    notifyListeners();
  }

  void updateCheck(int id, bool check) {
    todoList.asMap().forEach((int index, Todo todo) {
      if (todo.id == id) {
        _todoList[index].check = check;
      }
    });
    notifyListeners();
  }

  void deleteTodo(int id) {
    _todoList = _todoList.where((todo) => todo.id != id).toList();
    _todoList.asMap().forEach((int index, Todo todo) {
      _todoList[index].id = index + 1;
    });
    notifyListeners();
  }
}

Providerの作成

mainでTodoViewModelのProviderを作成

final todoProvider = ChangeNotifierProvider((ref) => TodoViewModel());

また、ProviderScopeでmainのWigetをくくる

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

画面の作成

最後にToodリストを表示する為の画面を作成

class TodoScreen extends HookConsumerWidget {
  TodoScreen({Key? key}) : super(key: key);
  final GlobalKey<AnimatedListState> _listKey = GlobalKey();
  final focusNode = FocusNode();

  
  Widget build(BuildContext context, WidgetRef ref) {
    final todoModel = ref.watch(todoProvider);
    return Scaffold(
      resizeToAvoidBottomInset: true,
      appBar: AppBar(
        elevation: 0,
      ),
      drawer: Drawer(
        child: ListView(
          children: [
            DrawerHeader(
                decoration:
                    BoxDecoration(color: Theme.of(context).primaryColor),
                child: const Text("Menu")),
            ListTile(
              leading: const Icon(Icons.check_box),
              title: const Text("ToDo List"),
              onTap: () {
                Navigator.pop(context);
              },
            ),
          ],
        ),
      ),
      body: Container(
          width: double.infinity,
          decoration: BoxDecoration(
            color: Theme.of(context).primaryColor,
          ),
          child: Column(
            children: [
              Container(
                margin: const EdgeInsets.only(left: 20),
                height: 80,
                width: double.infinity,
                alignment: Alignment.centerLeft,
                child: Text("ToDo List",
                    style: Theme.of(context).textTheme.headline4),
              ),
              Expanded(
                  child: Container(
                      decoration: const BoxDecoration(
                          color: Colors.white,
                          borderRadius: BorderRadius.only(
                              topLeft: Radius.circular(20),
                              topRight: Radius.circular(20))),
                      child: Padding(
                        padding: const EdgeInsets.all(10),
                        child: AnimatedList(
                            key: _listKey,
                            initialItemCount: todoModel.todoList.length,
                            itemBuilder:
                                (BuildContext context, int index, animation) {
                              return _buildItem(
                                ref,
                                index,
                                Theme.of(context).secondaryHeaderColor,
                                animation,
                              );
                            }),
                      )))
            ],
          )),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          int insertIndex = todoModel.todoList.length;
          todoModel.createTodo("");
          _listKey.currentState?.insertItem(insertIndex,
              duration: const Duration(milliseconds: 300));

          todoModel.todoList[todoModel.todoList.length - 1].focusNode
              .requestFocus();
        },
        tooltip: 'Add Todo',
        child: const Icon(Icons.add),
      ),
    );
  }

  Widget _buildItem(
    WidgetRef ref,
    int index,
    Color dismissColor,
    Animation<double> animation,
  ) {
    final todoModel = ref.watch(todoProvider);
    final id = todoModel.todoList[index].id;
    final title = todoModel.todoList[index].title;
    final check = todoModel.todoList[index].check;
    final focusNode = todoModel.todoList[index].focusNode;
    return SizeTransition(
        sizeFactor: animation,
        child: Dismissible(
          key: Key('${id.hashCode}'),
          background: Container(color: dismissColor),
          confirmDismiss: (direction) async {
            return true;
          },
          onDismissed: (direction) {
            _listKey.currentState?.removeItem(index,
                (context, animation) => const SizedBox(width: 0, height: 0));
            todoModel.deleteTodo(id);
          },
          child: ListTile(
              leading: Checkbox(
                onChanged: (e) {
                  todoModel.updateCheck(id, !check);
                },
                value: check,
              ),
              title: TextFormField(
                focusNode: focusNode,
                autofocus: false,
                initialValue: title,
                decoration: const InputDecoration(
                  border: InputBorder.none,
                ),
                onChanged: (value) {
                  todoModel.updateTodo(id, value);
                },
                onFieldSubmitted: (value) {
                  if (value == "") {
                    _listKey.currentState?.removeItem(
                        index,
                        (context, animation) =>
                            const SizedBox(width: 0, height: 0));
                    todoModel.deleteTodo(id);
                  }
                },
              )),
        ));
  }
}

完成プロジェクト

https://github.com/syi2021/simple_todo

参考

https://zuma-lab.com/posts/flutter-todo-list-riverpod-use-provider-change-notifier

Discussion