Flutter製メモアプリ Writybitを公開しました

みなさんお久しぶりです。vorotamorozです。
公開から少し経っちゃいましたが、Writybitというアプリを公開しました。
基本的には前作アプリのQuickLogbookから、あまり使わなくなった機能を減らしてミニマムにしたものです。

どんなアプリかと申しますと、是非一度使ってみて頂きたいのですが、短文を現在地の住所と共にひたすら記録するアプリになります。
ふと思いついたこととか、全部書いておくとめちゃくちゃ便利で。
全文検索を使って拾い出せるので「あれはいつだっけ?」「前に確かポストに入れようと家から持って出たけど、結局あの日は何してた?」1とか、そういうのめちゃくちゃ減ります。
この形のアプリで2015年頃から日記をつけ続けていますが、今では記憶を半分ぐらいこれに預けてます。

今回はFlutterでアプリを作ったので、その知見を少し共有したいと思います。

全体構想

まず、僕はAndroidケータイを普段2台持ち歩いていて、両方のスマホで日記を書いています。そのため、普通にスタンドアロンなアプリにすると、日記がばらばらになってしまいます。
かつてはWriteNote等でEvernoteに日記を取っていたのですが、だんだんと日記の分量が増えてしまいこれも邪魔に。
なので、前作アプリQuickLogbookでは、OneDriveで同期を行っていました。
さすがに日記という超個人的なコンテンツを預かるのは適切で無いと思ったので、保存場所はユーザに確保して貰おうという考えです。

その際にOneDriveを使用したのは、RESTなAPIで楽だったという単にそれだけです。その当時、Xamarinにクラウドストレージを簡単に使えるNuGetパッケージがあったのかが定かではないのですが、自作したということは何らかの問題があったっぽいですね。
ただ、この選択はそこそこ失敗で、Googleアカウントは持っているけど、Microsoftアカウントは持ってないという声を結構聞きました。
そして、結構遅い。.NETでのDBファイルを圧縮してアップロードしていたのですが、最近は何故かなかなか同期に時間が掛かるようになってきてしまっていました。

そんないきさつがあったので、実は、今回、改めてアプリを作り直すにあたり、当初は日記を完全に暗号化した上で、同期サーバを立てようかと思っていました。
そうすると、データのバックアップや同期が一瞬で終わるに違いない、と。
簡単な登録フローで登録できて、いつでも自分のデータを全削除できるような形にしたらどうだろう? なんてのを構想したのですが、リスクを改めて分析したところ、確実に危険で。
データを預かってしまうと、例えば僕が車に轢かれて死んだ結果、誰もサーバを止めないのにデータの流出可能性だけが上がっていくなんていう最悪なケースも考えられます。

最終的に、ソロである事がそもそも問題であるという結論に落着き、自サーバー案は廃案になりました。

で。

今回はどうしたか

まぁ、FlutterってGoogle製だし、Googleアカウントだとシュッと行くのでは? まずAndroidユーザは確実に持ってるし、iPhoneユーザもGmailとか持ってるんじゃない? という大雑把な考えで、GoogleDriveを使用する方針に。
ということで確認したところ公式の説明あり、最終的には

  • firebase_auth
  • google_sign_in
  • googleapis
  • googleapis_auth

あたりでめちゃくちゃ簡単に解決できました。
バックアップのファイル形式も、できるだけ今後活用したいので、jsonをgzipで圧縮したファイルに。
できるだけシンプルに考えるようにしました。
GoogleDriveを使用しなくてもいいように、直接アプリからデータのインポート&エクスポート機能もつけてあります。

  • share
  • file_picker

このあたりで万全でした。

データベースはHiveを使用してます。SQLiteを使ってもよかったのですが、ORMとか色々考えると結局こういうののほうが便利だよなって。
全文検索にあたっては、パフォーマンス的に不安がありそうだったのですが、めちゃくちゃ余裕で検索してくれてます。
ついでに無限スクロールするようにしたので、QuickLogbookだと数十件ごとにアクションが必要だったのが、スルスルーっと全部出てきます。爽快。

GPSや逆ジオコーディングは下記のライブラリでサクッと。

  • geolocator
  • geocoding

このあたりまで、画面を一切作らずに、ボタンを押したら機能検証が走るという状況でテストしてました。

で、いよいよ画面を作るとなって、最初はsetStateでテキトーに出来る範囲かな? と思って雑に書いてたのですが、依存値の依存値みたいなのが思ったより煩雑になってきました。

そこで使ったのが、RiverpodFlutter Hooksです。
こいつらがめちゃくちゃ便利で、あるStateに値が入ったら、こっちのFutureを解決しにかかる、みたいな機能がめちゃくちゃ書きやすかったですね。

final listDateProvider = StateProvider((ref) => DateTime.now());
final listProvider = FutureProvider<List<RecordModel>>((ref) async {
  final selectedDate = ref.watch(listDateProvider);
  final models = await getList(selectedDate.state);
  return models;
});

こんな感じで、listDateが変更されたらlistが変更される、というのが、処理をほとんど書かずに実現出来てしまいます。
上記みたいなProviderを用意しておくと、画面側は、ReactのHooksと同じように、

class ListPage extends HookWidget {
  @override
  Widget build(BuildContext context) {
    final listDate = useProvider(listDateProvider).state;

    final items = useProvider(listProvider);
        //::
    return Column(children: [
        OutlinedButton(
            onPressed: () async {
                final selected = await showDatePicker(
                    context: context,
                    initialDate: listDate,
                    firstDate: DateTime(2015),
                    lastDate: DateTime.now(),
                );
                if (selected != null) {
                    context.read(listDateProvider).state = selected;
                }
            },
            child: Text(
                DateFormat('yyyy/MM/dd').format(listDate),
            ),
        ),
        Expanded(
            child: items.when(
                data: (data) {
                return data.isEmpty
                    ? const Text('No Data')
                    : ListView.builder(
                        itemBuilder: (BuildContext context, int index) {
                            final item = data[index];
                            return EntryView(
                            maxwidth: maxwidth,
                            item: item,
                            );
                        },
                        itemCount: data.length,
                        );
                },
                loading: () =>
                    const Center(child: CircularProgressIndicator()),
                error: (err, stack) => Text('Error: $err'))),
        ],
    );
  }
}

とふわっと書くだけで良い感じに制御されます。
結局、現在地座標やジオコーディングもProviderで制御してます。

画面は画面で、実際のところMaterial UIのガイドラインに沿って作ったら概ね間違いのないデザインになりましたし、今回は、ライブラリとフレームワークに面倒なことを全部任せて、やりたいことを最短で実現できた気がしてます。

総工数37コミット。1時間ずつぐらい使って、1ヶ月でできあがってます。
Flutter、おすすめですよ。

なお、今回のアイコンはExcelで書きました。凄いもんだね、Excel。

是非是非、一度使ってみて下さい。
もう一度宣伝しておきます。
Writybitです。


  1. 全文検索して挙がってきた単票を含む日、という画面遷移ができます。 ↩︎