Flutter初学者がRiverpodを用いてMemoアプリ作ってみた

はじめまして! 今年の6月からエキサイトのアプリエンジニアとして内定者インターンをしております、藤崎裕樹( @ex_naty44 )と申します!

今回はインターンのトレーニングでMemoアプリを作ってみたので、 その過程をまとめてみようと思います。

エキサイトでインターンをする前は他社でweb開発のインターンをしていましたが、モバイルアプリに関する知識はほとんどない状態でした。

そんな僕でもこのメモアプリを作ることが出来たので、今回の記事が初学者の方の参考になれば幸いです。

よろしくお願いします!

目次

  • メモアプリを作成した理由
  • 完成品
  • 要件
  • Riverpodでリプレイス
  • 振り返り

メモアプリを作った理由

Flutterを触ったことがなかったので、まずはUIと基本的な機能についての理解を深めることが主な目的でした。 ゆくゆくはこのエキサイトに入ってくるアプリエンジニアのための参考にしていただければと思い、完成させてみようと考えました。

完成品

メモアプリ完成品1メモアプリ完成品2
メモアプリ完成品

UIとしてはこんな感じです。 右下の+ボタンを押すとメモが投稿出来ます。 右上の選択を押すと、チェックボックスが表示され、 選択したメモをボタンで削除できます。検索フィールドからはメモを検索することができます。

完成品の全機能については以下のリンクから動画で確認できます。

要件

  • 投稿機能
  • 一覧表示
  • 削除機能
  • 投稿日時の保存
  • ローカルストレージでの永続化
  • 編集機能
  • 編集日時の保存
  • 編集日時でのソート
  • 投稿日時でのソート
  • 選択したメモの一括削除
  • 検索機能

これらの機能初めはStatefulwidgetで実装しました。 その後、今後関わりうるプロジェクトでRiverpodが採用されるとのことで、 メモアプリでも導入してみようと思いました。

Riverpodでリプレイス

Riverpodは状態管理パッケージです。 Riverpodではrefオブジェクトを使ってグローバルに定義した様々な種類のProviderを利用することが可能になります。

refオブジェクトを用いるために、 StatefulwidgetとStateをConsumerStateful widgetとConsumerStateに切り替えて、 ProviderとStateNotifireProviderを用いてリプレイスしました(例ではStateProvideを使用しています)。

Stateの変更を検知してUIを再描画する際、今までsetStateで囲んでいましたが、 Providerをref.watchで監視し、変更を検知できるようにしました。 Providerが変更を感知すると、関わっているUIを再描画してくれます。

class MyHomePage extends StatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  // テキストフィールドの値を受け取る
  final _myController = TextEditingController();
  // データ格納用リスト
  final List<String> _items = [];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Memo App'),
      ),
      body: Column(
        children: [
          // テキストフィールド
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: TextField(
              controller: _myController,
            ),
          ),
          // リストビュー
          Expanded(
            child: ListView.builder(
              itemCount: _items.length,
              itemBuilder: (_, int index) {
                return Card(
                  child: ListTile(
                    title: Text(_items[index]),
                  ),
                );
              },
            ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          setState(() {
            _items.add(_myController.text);
          });
          // テキストフィールドの値をクリア
          _myController.clear();
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

上記を以下のように書き変えました。

// ConsumerStatefulWidgetに変更
class MyHomePage extends ConsumerStatefulWidget {
  const MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  ConsumerState<MyHomePage> createState() => _MyHomePageState();
}

// StateProviderを定義
final memosProvider = StateProvider.autoDispose((ref) => []);

// ConsumerStateに変更
class _MyHomePageState extends ConsumerState<MyHomePage> {
  final _myController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    // Providerを監視
    final memoList = ref.watch(memosProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Memo App'),
      ),
      body: Column(
        children: [
          // テキストフィールド
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: TextField(
              controller: _myController,
            ),
          ),
          Expanded(
            child: ListView.builder(
              itemCount: memoList.length,
              itemBuilder: (_, int index) {
                return Card(
                  child: ListTile(
                    title: Text(memoList[index]),
                  ),
                );
              },
            ),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          memoList.add(_myController.text);
          // memoListにすると参照渡しになり、変更が検知されない。値渡しにして変更を検知させる。
          ref.read(memosProvider.notifier).state = [...memoList];
          _myController.clear();
        },
        child: const Icon(Icons.add),
      ),
    );
  }
}

Riverpodを用いるとグローバルにProviderを利用できるので、大規模なアプリでは非常に便利なので、 これからも色々なプロジェクトでの導入が増えていきそうです!

振り返り

最後に、今回のトレーニングでRiverpodの利点と書き方を学ぶことができました! ですが、まだProviderを全て使い切れた訳ではありません。 このメモアプリ自体もUI、アーキテクチャ等の改善の余地があるので、 今後もアップデートしていきたいと思います!

最後に

エキサイトではフロントエンジニア、バックエンドエンジニア、アプリエンジニアを随時募集しております。長期インターンも歓迎していますので、興味があれば連絡いただければと思います。今回の記事を読んで少しでも興味が湧きましたら是非応募お願い致します!!

就業型インターンの募集情報です! www.wantedly.com

募集職種一覧はこちらになります!カジュアル面談からでも構いません。 www.wantedly.com