ボクダイモリ

Life is like a Game

【Flutter入門】Flutterでメモ帳アプリ作ってみた

そろそろ何かアプリを作ってみたいと思ったので簡単にマルチプラットフォームなアプリを作れるFlutterというフレームワークを触ってみました。 今回は勉強の一環として作成した簡単なメモ帳アプリを公開、解説します。 作成したアプリはGithubにアップしています。

github.com

コードの解説の前にFlutterについてですが、Flutterとはハイブリットアプリを開発するためのフレームワークDartという言語で実装されています。 Flutterの公式にも書いてありますが簡単にMaterial UIを持ったネイティブアプリを開発することができます。 flutter.io

書いててとても便利だったのがホットリロード機能で、コード修正→UI確認が秒速で完了します。

実際にアプリ開発に入る前に公式のチュートリアルは一度やってみた方がいいでしょう。 flutter.io

なお、アプリ開発のための環境構築方法については割愛します。

アプリの機能

今回実装したアプリの機能は次の通りです。

  • メモの一覧表示

  • メモの新規作成

  • メモの編集

  • メモの削除

main.dart(Home画面)

アプリのホーム画面でメモの一覧を表示させています。

アプリの全体はmain.dartに記述されており、下記のように定義しています。

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Note App',
      theme: new ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: new Home(title: 'Note List'),
    );
  }
}

class Home extends StatefulWidget {
  Home({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _HomeState createState() => new _HomeState();
}

アプリののタイトルと_HomeStateというWidgetを定義しています。 このWidgetはStatefulでメモ帳のデータを持たせています。

class _HomeState extends State<Home> {
  List<Memo> memos = new List<Memo>();

...

Home画面のUIは次のコードで定義されています。

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: _list(),
      floatingActionButton: new FloatingActionButton(
        tooltip: 'add note',
        child: new Icon(Icons.add),
        onPressed: () {
          Memo newMemo = new Memo('','');
          memos.add(newMemo);
          Navigator.push(context, new MaterialPageRoute<Null>(
              settings: const RouteSettings(name: "/create"),
              builder: (BuildContext context) => new Create(newMemo)
          ));
        }
      ),
    );
  }

ScaffoldというクラスでUIを構成する各コンポーネントを定義します。 それぞれのコンポーネントについて説明します。

  • appBar

アプリのナビゲーションバーに表示するタイトルです。

  • body

Home画面に表示するコンポーネントの本体。_list()でメモ帳の一覧を表示するためのリストが返されます。

  • floatingActionButton

右下に丸いボタンを出すことができます。押すとメモ帳の新規作成画面に遷移します。

        onPressed: () {
          Memo newMemo = new Memo('','');
          memos.add(newMemo);
          Navigator.push(context, new MaterialPageRoute<Null>(
              settings: const RouteSettings(name: "/create"),
              builder: (BuildContext context) => new Create(newMemo)
          ));

Navigator.pushで遷移先を指定します。また、遷移する際に新しいメモデータを配列に追加しています。 また、メモ帳のデータはMemo型を自作して配列にして保存するようにしています。 Memo型はentity/memo.dartで定義しています。

そしてメモ帳のリストを作成している箇所が次の通り。

  Widget _list() {
    return new ListView.builder(
        padding: const EdgeInsets.all(16.0),
        itemCount: memos.length,
        itemBuilder: (context, i) {
          final item = memos[i];
          return new Dismissible(
            key: new Key(item.title),
            onDismissed: (direction) {
              memos.removeAt(i);

              Scaffold.of(context).showSnackBar(
                  new SnackBar(content: new Text("Memo dismissed")));
            },
            // Show a red background as the item is swiped away
            background: new Container(color: Colors.red),
            child: new ListTile(
              title: new Text(
                item.title,
                maxLines: 1,
                style: _biggerFont,
              ),
              onTap: () {
                Navigator.push(context, new MaterialPageRoute<Null>(
                    settings: const RouteSettings(name: "/detail"),
                    builder: (BuildContext context) => new Detail(item)
                ));
              },
            ),
          );
        },
    );
  }

ListView.builderを使ってメモ帳のデータをListViewに表示させています。

メモ帳の要素を元にListTileを一つずつ作っています。

ListView内のListTileをタップしたらメモ帳の編集が行えるようにしたいのでonTapというイベントハンドラーを定義して、 押された時に編集画面に遷移するようにしてます。

また、親要素にDismissibleクラスを定義していますがこれはListViewのTileにスワイプ→削除のようなモーションを実装するのに必要になります。onDismissedがメモ帳削除のイベントハンドラーです。

create.dart(メモ帳の新規作成)

前述したメモ帳の新規作成画面はHome画面の右下に出てくる丸いボタンを押すと遷移します。 新規作成画面にはテキストフィールドとボタンを置いています。

@override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Create new note'),
      ),
      body: new ListView(children: <Widget>[
        new TextField(
            decoration: new InputDecoration(
                hintText:"Write title."
            ),
            controller: new TextEditingController(text: _memo.title),
            onChanged: (String newTitle) {
              _memo.title = newTitle;
            }
        ),
        new TextField(
            maxLines: 10,
            decoration: new InputDecoration(
                hintText:"Write memo."
            ),
            controller: new TextEditingController(text: _memo.body),
            onChanged: (String newBody) {
              _memo.body = newBody;
            }
        ),
        Center(
          child: new Container(
            margin: const EdgeInsets.all(10.0),
            height: 75.0,
            width: 150.0,

            child: new FlatButton(
              child: new Text('save memo'),
              color: Colors.lightBlue,
              textColor: Colors.white,
              onPressed: () {
                Navigator.pop(context);
              }
            )
          )
        )
      ])
    );
  }

2つのテキストフィールドはonChangedを実装していて、変更を行ったタイミングでメモ帳の内容を上書きしています。 また、ボタンを押した時のイベントは単に元の画面(Home画面)に戻っているだけです。

detail.dart(メモ帳の編集)

メモ帳の詳細画面は新規作成とほとんど変わらないです。Home画面に戻るためのボタンがないだけ。

メモ帳の削除

メモ帳の削除については前述した通り、main.dartでonDismissedというイベントハンドラーで実装しています。

            onDismissed: (direction) {
              memos.removeAt(i);

              Scaffold.of(context).showSnackBar(
                  new SnackBar(content: new Text("Memo dismissed")));
            },

ListTileをスワイプすると対応するインデックスのメモデータを削除しています。 UIも削除後に自動的に再描画されます。

この勝手に再描画してくれる点がとても楽。これが正しいやり方なのかは分からないけども。

実装してみての感想

公式ドキュメントも分かりやすく情報は十分にあるので問題なく実戦で使えると思う。 文法的な部分ではアプリのUI構造を宣言的に書けるのでその分構造を組みやすいと感じるが、階層が深くなると一覧性を損なってしまう点を強く感じた。

Widgetの記述ををメソッドやクラスを使って階層を減らすように工夫が必要そうだが、別Widgetの変数をどう参照するかとかも考えないといけない。

UIの再描画を伴うデータの更新はStatefulなWidgetがSetState()の中で行うのが正しいと思われるので、今回のようなデータのポインタを渡した先で値変更、というようなやり方は良くないのかもしれない。

このWidget同士でのデータの扱い方とかStatefulとStatelessのWidgetの使い分けは良く分かっていないので後で調べてみる。

ともあれ、やはり開発時のホットリロードはサクサク開発を進められるのですごく気持ちいいので今後もFlutterは触ってると思う。