Chapter 07

Hands-on 4

takumma
takumma
2021.09.13に更新

Todo クラスの作成

まずは TODO のクラスを作成します。

my_home_page.dart
  class MyHomePage extends StatefulWidget {
    // 略
  }

  class _MyHomePageState extends State<MyHomePage> {
    // 略
  }

+ class Todo {
+   String title;
+   IconData icon;
+   
+   Todo(this.title, this.icon);
+ }

Todo(this.title, this.icon); はコンストラクタです。
Flutter ではアイコンは IconData クラスとして持ち、Icon(IconData icon) というふうに UI で表示できます。マテリアルデザインのアイコンは Icons クラスで定義されています。

https://api.flutter.dev/flutter/material/Icons-class.html

_todoItems の変更

_todoItems を文字列のリストから、先ほど作成した Todo のリストに変更します。

my_home_page.dart
  class _MyHomePageState extends State<MyHomePage> {
    
-   List<String> _todoItems = [
-     "英語の課題",
-     "牛乳を買う",
-   ];
+   List<Todo> _todoItems = [
+     Todo("英語の課題", Icons.description),
+     Todo("牛乳を買う", Icons.local_grocery_store),
+   ];

それに伴って、いくつかの箇所を修正していきます。

my_home_page.dart
  class _MyHomePageState extends State<MyHomePage> {

    List<Todo> _todoItems = [
      Todo("英語の課題", Icons.description),
      Todo("牛乳を買う", Icons.local_grocery_store),
    ];

-   void _addTodo(String title) {
+   void _addTodo(Todo todo) {
      setState(() {
-       _todoItems.add(title);
+       _todoItems.add(todo);
      });
    }
my_home_page.dart
  body: ListView.builder(
    itemCount: _todoItems.length,
    itemBuilder: (BuildContext context, int index) {
      return Card(
        child: ListTile(
-         title: Text(_todoItems[index]),
+         title: Text(_todoItems[index].title),
          trailing: IconButton(
            icon: const Icon(Icons.more_vert),
            onPressed: () => showDialog(
              context: context,
              builder: (BuildContext context) => AlertDialog(
-               title: Text(_todoItems[index]),
+               title: Text(_todoItems[index].title),
                actions: [
                  // 略
                ],
              ),
            ),
          ),
        ),
      );
    },
  ),

Todo を追加する部分では、とりあえず Icons.add(FloatingActionButton のアイコンと同じ)を追加するようにします。

my_home_page.dart
  floatingActionButton: FloatingActionButton(
    onPressed: () async {
      final String? title = await Navigator.of(context)
        .push(MaterialPageRoute(builder: (context) => CreatePage()));
-     if (title != null && title != "") _addTodo(title);
+     if (title != null && title != "") _addTodo(Todo(title, Icons.add));
    },
    child: const Icon(Icons.add),
  ),

これでひとまず _todoItems を変更する前の状態と同じになりました。
次はここにアイコンを表示させてみましょう。

アイコンを表示する

ListView を変更してアイコンを表示しましょう。
ListTile の leading を指定します。

my_home_page.dart
  itemBuilder: (BuildContext context, int index) {
    return Card(
      child: ListTile(
+       leading: Icon(
+         _todoItems[index].icon,
+         size: 35.0,
+       ),
        title: Text(_todoItems[index].title),
        trailing: IconButton(
          // 略
        ),
      ),
    );
  }

アイコンが表示できました。

ですが、まだ追加する TODO のアイコンは指定できないので、次は TODO を入力するときにアイコンも選べるようにしていきましょう。

flutter_iconpicker パッケージをインストールする

アイコンを選ぶのに、今回は flutter_iconpicker という外部パッケージを利用します。

https://pub.dev/packages/flutter_iconpicker

まずは pubspec.yaml で以下のように追加します。

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  
  cupertino_icons: ^1.0.2

  flutter_iconpicker: ^3.0.4  # 追加

そしてパッケージをインストールします。Android Studio なら「pub get」というところをクリックすればインストールが開始されます。

また、以下のようなコマンドでもインストールできます。

$ flutter pub get

flutter_iconpicker を使う

インストールした flutter_iconpicker を create_page で使ってみましょう。
外部パッケージを使うときはインポートする必要があります。

create_page.dart
  import 'package:flutter/material.dart';
+ import 'package:flutter_iconpicker/flutter_iconpicker.dart';

_pickIcon() では、FlutterIconPicker.showIconPicker(context) でアイコンを選択するダイアログを表示し、選択したアイコンのデータを _icon に入れています。

create_page.dart
  class _CreatePageState extends State<CreatePage> {
    String _title = "";
    
+   IconData? _icon;

+   void _pickIcon() async {
+     IconData icon = await FlutterIconPicker.showIconPicker(context);
+     setState(() {
+       _icon = icon;
+     });
+   }

flutter_iconpicker ダイアログを表示して選択したアイコンを表示できるように、ボタンとアイコンを追加します。

create_page.dart
  body: Center(
    child: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        const Text("TODOを入力してください"),
        TextField(
          onChanged: (String text) => _title = text,
        ),
+       Icon(
+         _icon,
+         size: 45.0,
+       ),
+       ElevatedButton(
+         child: const Text("Pick Icon"),
+         onPressed: () => _pickIcon(),
+       ),
        ElevatedButton(
          child: const Text("Add"),
          onPressed: () => Navigator.pop(context, _title),
        ),
      ],
    ),
  ),

ボタンを押すと、ダイアログが表示されます。

そしてアイコンを選択できます。

my_home_page に選択したアイコンを渡す

テキストと一緒にアイコンも my_Home_page に渡すように変更します。先ほど作成した Todo クラスを用いましょう。

my_home_page にある Todo クラスを使うので、インポートを忘れずに。

create_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_iconpicker/flutter_iconpicker.dart';

+ import 'my_home_page.dart';
create_page.dart
  body: Center(
    child: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        // 略
        ElevatedButton(
          child: const Text("Add"),
-         onPressed: () => Navigator.pop(context, _title),
+         onPressed: () => Navigator.pop(context, Todo(_title, _icon)),
        ),
      ],
    ),
  ),

my_home_page のほうも、受け取った Todo をリストに追加するように修正します。

my_home_page.dart
  floatingActionButton: FloatingActionButton(
    onPressed: () async {
-     final String? title = await Navigator.of(context)
-       .push(MaterialPageRoute(builder: (context) => CreatePage()));
+     final Todo? todo = await Navigator.of(context)
+       .push(MaterialPageRoute(builder: (context) => CreatePage()));
-     if (title != null && title != "") _addTodo(Todo(title, Icons.add));
+     if (todo != null) _addTodo(todo);
    },
    child: Icon(Icons.add),
  ),

これで選択したアイコンを TODO リストへ追加できるようになりました。

細かい修正

まずは create_page の Widget ツリー全体を Container ウィジェットで覆い、外側の padding を追加します。

create_page.dart
- body: Center(
+ body: Container(
+   padding: const EdgeInsets.all(40.0),
+   child: Center(

  // 略
  
    ),
+ ),

create_page の UI を以下のように修正します。

  • TextField の上のテキストを消して、TextField にラベルテキストを追加
  • アイコンと Pick Icon ボタンを RowWidget を使って横並びに
  • アイコンが選択されていないときに none という文字を表示
create_page.dart
  body: Container(
    padding: const EdgeInsets.all(40.0),
    child: Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
-         const Text("TODOを入力してください"),
          TextField(
+           decoration: const InputDecoration(
+             labelText: "TODO title",
+           ),
            onChanged: (String text) => _title = text,
          ),
-         Icon(
-           _icon,
-           size: 45.0,
-         )
-         ElevatedButton(
-           child: const Text("Pick Icon"),
-           onPressed: () => _pickIcon(),
-         ),
+         Container(
+           padding: const EdgeInsets.only(top: 20.0, bottom: 30.0),
+           child: Row(
+             mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+             children: [
+               _icon != null
+                   ? Icon(
+                       _icon,
+                       size: 45.0,
+                     )
+                   : const Text("none"),
+               ElevatedButton(
+                 child: const Text("Pick Icon"),
+                 onPressed: () => _pickIcon(),
+               ),
+             ],
+           ),
+         ),
          ElevatedButton(
            child: const Text("Add"),
            onPressed: () => Navigator.pop(context, Todo(_title, _icon)),
          ),
        ],
      ),
    ),
  ),

あと、Add ボタンが押されたときに _title_icon のどちらかが入力されていないとメッセージが出るようにしておきます。

create_page.dart
  class _CreatePageState extends State<CreatePage> {
    String _title = "";
    
    IconData? _icon;

+   bool _isError = false;
create_page.dart
  body: Container(
    padding: const EdgeInsets.all(40.0),
    child: Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          // 略
          ElevatedButton(
            child: ElevatedButton(
              child: const Text("Add"),
-             onPressed: () => Navigator.pop(context, Todo(_title, _icon)),
+             onPressed: () {
+               if (_title == "" || _icon == null) {
+                 setState(() {
+                   _isError = true;
+                 });
+                 return;
+               }
+               Navigator.pop(context, Todo(_title, _icon));
+             },
            ),
          ),
+         if (_isError)
+           const Text(
+             "全ての項目を埋めてください",
+             style: TextStyle(color: Colors.red),
+           ),
        ],
      ),
    ),

このようになります。

これで完成です!
Flutter で TODO アプリを作ることができました!