LoginSignup
5
3

More than 3 years have passed since last update.

FlutterでTodoアプリを作成 ~第一話 あなたが今日する事を教えて下さい。~

Last updated at Posted at 2020-10-05

1. Flutter学習歴

Flutter/Dartは触り始めてまだ一か月くらいです。
早くもFlutterを愛し始めております。

2. 完成したアプリ 

todo.gif

3. ファイルの構成

スクリーンショット 2020-10-05 22.00.29.png

4. 構成説明

  • domainフォルダにはロジックを詰め込む用にtodo_domain.dartを用意しました。
  • entityフォルダはinitialize用にファイルを用意しましたが、今回は使用しなかったので放置で。。(削除しておけばよかった)
  • ui/todoフォルダには、以下のファイルを用意しました。
    • list.dart(todo一覧表示用)
    • complete.dart(todo完了表示用)
    • incomplete.dart (todo未完了表示用)

5. 各ファイルの説明

  • main.dart
main.dart
import 'package:flutter/material.dart';

import 'ui/todo/list.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Todoアプリ',
      home: TodoList(),
    );
  }
}

  • pubspec.yaml

今回のtodoアプリ作成では状態管理でproviderを使用しているので、pubspec.yamlにproviderを使用できるようにinstallします。

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  provider: ^4.3.2+2

追加したらターミナルで以下のコマンドを打って使用できるようになります!

$ flutter pub get
  • list.dart

この画面でCRUD処理とtodoを完了状態に更新ができます。
完了状態にするには、todo(ListTile)を長押しします!
AppBarの左上にdrawerを設置し、そこから完了ページと未完了ページに遷移できるように設定しています。

list.dart
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

import 'package:todo_app/domain/todo_domain.dart';
import 'package:todo_app/ui/todo/complete.dart';
import 'package:todo_app/ui/todo/incomplete.dart';

class TodoList extends StatelessWidget {
  final _formKey = GlobalKey<FormState>();

  // ignore: missing_return
  Color getColor(bool isCompleted) {
    if (isCompleted) {
      return Colors.amber;
    }
  }

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<TodoDomain>(
      create: (_) => TodoDomain(),
      child: Scaffold(
        appBar: AppBar(
          title: Text('TodoLists'),
          actions: <Widget>[
            Consumer<TodoDomain>(
              builder: (context, model, child) {
                return IconButton(
                  icon: Icon(Icons.add),
                  onPressed: () {
                    // todoの追加画面をダイアログで開く
                    showDialog(
                      context: context,
                      // 下記はダイアログの外側を押すとダイアログを閉じるようにするかの設定
                      // barrierDismissible: false,
                      builder: (BuildContext context) {
                        return AlertDialog(
                          title: Text(
                            '追加したいTodoを教えて!',
                            style: TextStyle(fontSize: 14),
                          ),
                          content: Form(
                            key: _formKey,
                            child: TextFormField(
                              // 複数行入力できるようにしている
                              keyboardType: TextInputType.multiline,
                              maxLines: null,
                              // ignore: missing_return
                              validator: (value) {
                                if (value.isEmpty) {
                                  return '今日する事教えてくれないの。。?';
                                }
                              },
                              onChanged: (String text) {
                                // TodoDomainのtodoフィールドに、TextFormFieldで打った文字を反映させている
                                model.todo = text;
                              },
                            ),
                          ),
                          // AlertDialogではボタン関係はactionsで定義しなくてはいけない
                          actions: <Widget>[
                            FlatButton(
                              child: Text('OK'),
                              onPressed: () {
                                if (_formKey.currentState.validate()) {
                                  _formKey.currentState
                                      .save(); // TextFormFieldのonSavedが呼び出される
                                }
                                try {
                                  // todoの追加
                                  model.addTodo();
                                  // TodoListsページに戻って、dialogを閉じる
                                  Navigator.pop(context);
                                } catch (e) {
                                  print('今日する事が入力されていません');
                                }
                              },
                            ),
                          ],
                        );
                      },
                    );
                  },
                );
              },
            )
          ],
        ),
        body: Consumer<TodoDomain>(
          builder: (context, model, child) {
            return ListView.builder(
              itemCount: model.todos.length,
              // ignore: missing_return
              itemBuilder: (context, index) {
                return Card(
                  child: Container(
                    color: getColor(model.todos[index]['isCompleted']),
                    child: ListTile(
                      title: Text(
                        model.todos[index]['text'],
                        style: TextStyle(fontSize: 14),
                      ),
                      contentPadding: EdgeInsets.all(8),
                      onLongPress: () {
                        if (!model.todos[index]['isCompleted']) {
                          showDialog(
                            context: context,
                            builder: (BuildContext context) {
                              return AlertDialog(
                                title: Align(
                                  alignment: Alignment.center,
                                  child: Text(
                                    '完了しましたか?',
                                    style: TextStyle(fontSize: 14),
                                    textAlign: TextAlign.center,
                                  ),
                                ),
                                // AlertDialogではボタン関係はactionsで定義しなくてはいけない
                                actions: [
                                  FlatButton(
                                    onPressed: () {
                                      model.completeTodo(index);
                                      Navigator.of(context).pop();
                                    },
                                    child: Text('OK'),
                                  ),
                                  FlatButton(
                                    onPressed: () {
                                      Navigator.of(context).pop();
                                    },
                                    child: Text('NG'),
                                  ),
                                ],
                              );
                            },
                          );
                        }
                      },
                      // ListTileのtrailingに2つアイコンを並べたい時はWrapしてあげる
                      trailing: Wrap(
                        children: [
                          IconButton(
                            icon: Icon(Icons.edit),
                            onPressed: () {
                              showDialog(
                                context: context,
                                builder: (BuildContext context) {
                                  return AlertDialog(
                                    title: Text(
                                      '追加したTodo間違えちゃった?',
                                      style: TextStyle(fontSize: 14),
                                    ),
                                    content: Form(
                                      key: _formKey,
                                      child: TextFormField(
                                        keyboardType: TextInputType.multiline,
                                        maxLines: null,
                                        initialValue: model.todos[index]
                                            ['text'],
                                        // ignore: missing_return
                                        validator: (value) {
                                          if (value.isEmpty) {
                                            return '今日する事教えてくれないの。。?';
                                          }
                                        },
                                        onChanged: (String text) {
                                          model.todo = text;
                                        },
                                      ),
                                    ),
                                    // AlertDialogではボタン関係はactionsで定義しなくてはいけない
                                    actions: <Widget>[
                                      FlatButton(
                                        child: Text('OK'),
                                        onPressed: () {
                                          if (_formKey.currentState
                                              .validate()) {
                                            _formKey.currentState
                                                .save(); // TextFormFieldのonSavedが呼び出される
                                          }
                                          try {
                                            // todoの追加
                                            model.editTodo(index);
                                            // TodoListsページに戻って、dialogを閉じる
                                            Navigator.pop(context);
                                          } catch (e) {
                                            print('今日する事が入力されていません');
                                            if (model.todo == null) {
                                              Navigator.of(context).pop();
                                            }
                                          }
                                        },
                                      ),
                                    ],
                                  );
                                },
                              );
                            },
                          ),
                          IconButton(
                            icon: Icon(
                              Icons.delete,
                              color: Colors.redAccent,
                            ),
                            onPressed: () {
                              showDialog(
                                context: context,
                                builder: (BuildContext context) {
                                  return AlertDialog(
                                    title: Align(
                                      alignment: Alignment.center,
                                      child: Text(
                                        '本当に削除しますか?',
                                        style: TextStyle(fontSize: 16),
                                        textAlign: TextAlign.center,
                                      ),
                                    ),
                                    actions: [
                                      FlatButton(
                                        onPressed: () {
                                          model.deleteTodo(index);
                                          Navigator.of(context).pop();
                                        },
                                        child: Text('OK'),
                                      ),
                                      FlatButton(
                                        onPressed: () {
                                          Navigator.of(context).pop();
                                        },
                                        child: Text('NG'),
                                      ),
                                    ],
                                  );
                                },
                              );
                            },
                          ),
                        ],
                      ),
                    ),
                  ),
                );
              },
            );
          },
        ),
        drawer: Consumer<TodoDomain>(
          builder: (context, model, child) {
            return Drawer(
              child: ListView(
                children: <Widget>[
                  SizedBox(
                    height: 80,
                    child: DrawerHeader(
                      child: Text(
                        'Menu',
                        style: TextStyle(fontSize: 36, color: Colors.white),
                      ),
                      decoration: BoxDecoration(color: Colors.blue),
                    ),
                  ),
                  Card(
                    child: ListTile(
                      title: Text('Incomplete'),
                      onTap: () {
                        // 未完了ページに飛ぶ前にDrawerを閉じている
                        Navigator.of(context).pop();
                        List incompleteTodos = model.getIncompleteTodos();
                        Navigator.of(context).push(
                          MaterialPageRoute(
                            builder: (context) =>
                                TodoIncomplete(incompleteTodos),
                          ),
                        );
                      },
                    ),
                  ),
                  Card(
                    child: ListTile(
                      title: Text('Complete'),
                      onTap: () {
                        // 完了ページに飛ぶ前にDrawerを閉じている
                        Navigator.of(context).pop();
                        List completeTodos = model.getCompleteTodo();
                        Navigator.of(context).push(
                          MaterialPageRoute(
                            builder: (context) => TodoComplete(completeTodos),
                          ),
                        );
                      },
                    ),
                  )
                ],
              ),
            );
          },
        ),
      ),
    );
  }
}

  • complete.dart

完了しているtodoのみを表示するファイルになります。

complete.dart
import 'package:flutter/material.dart';

class TodoComplete extends StatelessWidget {
  TodoComplete(this.completeTodo);

  final List completeTodo;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('TodoComplete'),
      ),
      body: ListView.builder(
        itemCount: completeTodo.length,
        // ignore: missing_return
        itemBuilder: (context, index) {
          return Card(
            child: ListTile(
              title: Text(
                completeTodo[index]['text'],
                style: TextStyle(fontSize: 14),
              ),
              contentPadding: EdgeInsets.all(8),
            ),
          );
        },
      ),
    );
  }
}

  • incomplete.dart

未完了のtodoのみを表示するファイルになります。

incomplete.dart
import 'package:flutter/material.dart';

class TodoIncomplete extends StatelessWidget {
  TodoIncomplete(this.incompleteTodo);

  final List incompleteTodo;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('TodoIncomplete'),
      ),
      body: ListView.builder(
        itemCount: incompleteTodo.length,
        // ignore: missing_return
        itemBuilder: (context, index) {
          return Card(
            child: ListTile(
              title: Text(
                incompleteTodo[index]['text'],
                style: TextStyle(fontSize: 14),
              ),
              contentPadding: EdgeInsets.all(8),
            ),
          );
        },
      ),
    );
  }
}


  • todo_domain.dart

todoのCRUD処理等を記述したファイルです。
todoに対しての操作はこのファイルから行うようにしました!

todo_domain.dart
import 'package:flutter/material.dart';

class TodoDomain extends ChangeNotifier {
  String todo;
  List todos = [
    {
      'isCompleted': false,
      'text': '今日はFlutterのお勉強をします。目標はTodoアプリの完成です。頑張ります。応援して下さい。'
    }
  ];
  var completeTodos = [];
  var incompleteTodos = [];

  // todoの追加
  void addTodo() {
    if (todo.isEmpty) {
      return;
    }
    // todosに連想配列を追加するため
    Map todoData = {'isCompleted': false, 'text': todo};
    // todosの配列にt連想配列のtodoDataを追加
    todos.add(todoData);
    // 状態をなくしている 無くさないと追加ボタンとか押した時にtodoに前に入力したデータが残ってちゃう
    todo = null;
    // notifyListeners()でTodoDomainが使用されているChangeNotifyProviderに変更を通知
    notifyListeners();
  }

  // todoの編集
  void editTodo(int index) {
    if (todo.isEmpty) {
      return;
    }
    // 編集されたtodosの中のtodoのindexを引数でもらって、todosの中のindex番号のtodoを編集したtodoに書き換えている
    todos[index]['text'] = todo;
    todo = null;

    notifyListeners();
  }

  // todoの削除
  void deleteTodo(int index) {
    todos.removeAt(index);

    notifyListeners();
  }

  // todoを完了状態に更新
  void completeTodo(int index) {
    todos[index]['isCompleted'] = true;

    notifyListeners();
  }

  // 完了済みのtodoの取得
  List getCompleteTodo() {
    completeTodos.clear();
    for (var i = 0; i < todos.length; i++) {
      var data = todos[i];
      if (data['isCompleted']) {
        completeTodos.add(data);
      }
    }

    return completeTodos;
  }

  // 未完了のtodoの取得
  List getIncompleteTodos() {
    incompleteTodos.clear();
    for (var i = 0; i < todos.length; i++) {
      var data = todos[i];
      if (!data['isCompleted']) {
        incompleteTodos.add(data);
      }
    }

    return incompleteTodos;
  }
}

6. 改善点

  • Firebase使用してtodoを永続的に保存
    • 現時点では作成したtodoをそのまま配列に追加して状態を保持してるだけなので、デバッグし直したら消えちゃいます😇
  • リファクタリング
    • dataって変数名は直します。。
  • list.dart内のコード分割(できると思う)

他にもまだまだ有ると思うので修正していきたいと思います!

7. 作成した感想

状態管理がまだまだ理解し切れていないなぁと感じました。
一番の肝かなと思うんで深掘りしていきます。
あとはwidgetの分割とか出来ないのかなぁと思いました。
drawerとか別ファイルに分割して全画面に共通して表示とかできるのではと思うんですよね。。
今度調べて試してみよ。。

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3