Flutterで日記アプリを作ろう
はじめに
会話メモアプリを作ってて、少し作り方がまとまってきたから、かんたんなものを最初から作り直してみよう、そんな動機です。
やっぱり、かんたん、といえば日記アプリでしょう?
こんなアプリ
日記の一覧、編集画面があって…
日記には画像を一枚添付できる感じ。
データの保存方法
-
日記はSQLiteに保存サンプル通りにDBを初期化しようとしてうまくいかなかった…。不安定な部分もありそうなので、やめる…。ObjectBoxにしてみようかな…。 - 添付画像はアプリケーションのドキュメントディレクトリに年/月/日.png のように保存
リポジトリ作成
CI
flutterはバージョンが頻繁に上がるし、自分も大きな変更をしたときに何処かが壊れることに気づけると助かるので、やっぱりCIは入れておきたい。
いくつかサービスがありそうだけど、flutter公式が採用しているCirrus CIがとりあえず安心そう。
- flutter向けドキュメント https://cirrus-ci.org/examples/#flutter
- 設定例でコンテナのバージョンがlatestになってるけど…アップデートが頻繁で面倒なので、ベストプラクティスかも。
- 困ったらバージョン指定して、直したら戻す運用でよさそう。
- 例の通りに記述してもダメだった…。
静的解析
初心者の自分がイマイチな書き方で放置しないようにするのにとても有用だと思っています。
main.dartからwidget部分を分割
一個のファイルに全部書くのは、一度に考える範囲がめちゃくちゃ広がってしまってしんどいので、分割します。
アプリケーションのベースになる画面を作成
- 将来的に日記エントリの一覧になる予定
- タイトルとアクションボタンがついてる。
タイトルの変更
アイコンの変更
カウントアップ削除とステートレス化
ボタンを押すとカウントアップするようになってますが、当然不要なので取り払います。
それによって、状態が不要になるのでステートレスウィジェットに変更します。
アクションボタンから編集画面を起動
ペンボタンで記事画面を開く
sqfliteを使おうとしたら、なぜかデータベースのオープンもできなかった…。
疲れ切ったので、別のを探していました。
ObjectBox
ObjectBoxはモバイルデバイスやIoT機器のためのNoSQLデータベース。
ObjectBox Syncでサーバとの同期もあるようなので、案外よさそうな雰囲気。Firebaseの対抗馬?
Dart/Flutter以外にもJava、Kotolin、Swift用のライブラリもある模様。
会社化されているっぽい。
ああ…うまくできてなかった、sqfliteのほうもWidgetsFlutterBinding.ensureInitialized();
を入れたらうまくいった…。
main()
はflutterの実行開始位置ではなくて、dartのものなんだというのが唐突にわかった。そりゃ動かないよなぁ…。
sqfliteに戻ろうかなぁ…悩む…。
sqfliteで実装中…。
しかし、hivedbがラクすぎて、そっちに逃げたくなる…。
Navigatorで戻るやったら、リスト更新してくれなくて泣いてる><
Navigator.pushをawaitで待って、setStatus()で更新するとよいと聞いて、やったらうまくいきました。
参照
…シンプルに作りたいと思っているのに、ついリポジトリパターンをみてしまう。
ObjectBoxはそれ自体がリポジトリパターンっぽい。
クエリビルダも付いてるし、そっちにしてみるべか…。
仕事で使うならfirebaseが一番よさそうだけど…
いっぱいサンプルあるし、自分が書かなくてもよさそう。
だいたい、自分が作りたいのは小さくて可愛い、サーバの不要な個人的なアプリなので…ちょっとなぁ…。
テストが動かない…。
ローカルマシンで動くには別の設定が要りそう。
bash <(curl -s https://raw.githubusercontent.com/objectbox/objectbox-dart/main/install.sh)
↑をやると、失敗しなくなるけど、終わらない。
どこにあるんだろう、ローカルマシンで動かすための設定…。
インストールできてるかわからんので、サンプルコードがローカルで動くか確認中…。
pubspec.yamlのSDKバージョンを2.12以上を指定しなかったので、最初使えず四苦八苦…。
flutterだとpubspec.yaml準備してくれるから、考えたことなかった…。
environment:
sdk: '>=2.12.0 <3.0.0'
インストールできてるっぽい。
ちゃんと保存して読めた…!
自分のテストコードにバグがあるはず!!
お… getApplicationDocumentsDirectory
が失敗してる。
どっかでみたな…。
~/src/f_diary[Add_objectbox(;´▽`A]
22:45 $ flutter test test/widgets/my_home_page_test.dart
00:02 +0 -1: (setUpAll) [E]
MissingPluginException(No implementation found for method getApplicationDocumentsDirectory on channel plugins.flutter.io/path_provider)
package:flutter/src/services/platform_channel.dart 154:7 MethodChannel._invokeMethod
00:02 +0 -1: Some tests failed.
~/src/f_diary[Add_objectbox(;´▽`A]
これを読んだらなにかしらヒントがあるかしら…。
…なかった><
なんか違う…unit testじゃない…。
ファイル書き込みをするwidgetテストの下準備について書かれてる。
小さく必要なところだけやっているので読みやすく、ヒントになった。
ファイル書き込みのテストの中に書いてあった、MethodChannelが気になった。
FlutterはUIのフレームワークなので、UIでないプラットフォームの機能を使おうとすると、プラットフォーム側に実装が必要になる。その実装とのやり取りで使うもの…のよう。
ファイル書き込みはモロにそれにあたってるからなのね。
hivedbのテストコードに書いてあったヤツはテスト用プロバイダを作って登録する系だったのか(ちゃんと読んでなかった)。
MethodChannelのほうは、必要になったメソッドだけモックする形で、アプローチ方法
あー…どうしてもうまくいかない…。
単体でテストを動かすと通るのに、まとめて動かすと、なぜか変なレコードがうまれちゃっててダメ…。
このブランチ、破棄して、また作り直そう…。
サンプルアプリにテストない…。
テストの書き方もチュートリアルにない…。結構イバラの道っぽい…。
作り始めた。
モデルの定義のあとにgeneratorを動かしたが、エラーで動かず…。(サンプルにimportがないので想定してたけど…。)
$ flutter pub run build_runner build
[INFO] Generating build script...
[INFO] Generating build script completed, took 594ms
# 省略
line 1, column 1 of package:f_diary/models/article.dart: Could not resolve annotation for `class Article`.
╷
1 │ @Collection()
│ ^^^^^^^^^^^^^
╵
[INFO] Running build completed, took 4.0s
[INFO] Caching finalized dependency graph...
[INFO] Caching finalized dependency graph completed, took 33ms
[SEVERE] Failed after 4.0s
pub finished with exit code 1
サンプルを見て解決。
hivedbと同じ。
import 'package:isar/isar.dart';
part 'article.g.dart';
()
class Article {
()
int? id;
late String title;
late String body;
late DateTime createdAt;
late DateTime updatedAt;
}
isar inspectorがDart/Flutter製のデスクトップアプリだと知ってびっくりした…。
あっ!テストある…?
使い方とか細かく書いてないけど…気になる…。
…マイナーバージョンアップされてる!
何が変わったのかな…?
isar_connectに統合されましたisar
isar_connectがなくなってる…。バージョンアップでいきなり変わる、そういう時期のライブラリだよね。
機能強化
- 古い生成ファイルのチェックを追加
- 分離全体で変更されたスキーマのチェックを追加
- 追加したIsar.openSync()
同期的にオープンするの、入ってくれた!
- 追加col.importJsonRawSync()、、、、col.importJsonSync()_ query.exportJsonRawSync()_query.exportJsonSync()
- クエリのパフォーマンスが向上
- ffiメモリの処理の改善
- その他のテスト
書き込みの確認。
とくに問題なし。
isarをアップグレードしなければ…。
$ flutter pub outdated
Showing outdated packages.
[*] indicates versions that are not the latest available.
Package Name Current Upgradable Resolvable Latest
direct dependencies:
isar *2.0.0 *2.0.0 *2.0.0 2.1.0
isar_flutter_libs *2.0.0 *2.0.0 *2.0.0 2.1.0
dev_dependencies:
isar_generator *2.0.0 *2.0.0 *2.0.0 2.1.0
transitive dependencies:
path *1.8.0 *1.8.0 *1.8.0 1.8.1
transitive dev_dependencies:
js *0.6.3 *0.6.3 *0.6.3 0.6.4
source_span *1.8.1 *1.8.1 *1.8.1 1.8.2
test_api *0.4.3 *0.4.3 *0.4.3 0.4.9
You are already using the newest resolvable versions listed in the 'Resolvable' column.
Newer versions, listed in 'Latest', may not be mutually compatible.
アップグレードしました。
次はホーム画面にとりあえず保存したレコードを表示できるようにしよう。
あと、これはテストも書く。
激しいな、またバージョン上がってる…!
テストでisarのライブラリが読み込めない…。
どうやってやるんだ…?
:
Invalid argument(s): Failed to load dynamic library 'libisar.dylib': dlopen(libisar.dylib, 1): image not found
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following IsarError object was thrown running a test:
IsarError: Could not initialize IsarCore library. If you create a Flutter app, make sure to add
isar_flutter_libs to your dependencies.
:
このあたりかな…。
# 省略
- name: Download binaries
run: bash tool/download_binaries.sh
working-directory: packages/isar_flutter_libs
# 省略
これかな…。
↑のsetup_tests.shを動かすと、 .dart_tool
以下に各OS用のライブラリが置かれた。
Failed to load dynamic library 'libisar.dylib': dlopen(libisar.dylib, 1)
というエラーだったので、プロジェクトのルートにlibisar.dylib
という名前でコピーして、テストを実行。
ライブラリは読んでるようだけども…。
$ flutter test test/widgets/my_home_page_test.dart
00:02 +0: エディットボタンを押したときに記事ページが表示されること 00:03 +0: エディットボタンを押したときに記事ページが表示されること 00:03 +0: エディットボタンを押したときに記事ページが表示されること
══╡ EXCEPTION CAUGHT BY FLUTTER TEST FRAMEWORK ╞════════════════════════════════════════════════════
The following ArgumentError was thrown running a test:
Invalid argument(s): Failed to lookup symbol 'isar_get_instance': dlsym(0x7fc5e5522ea0,
isar_get_instance): symbol not found
(省略)
ここを見る限り、他のブツが足りないことはなさそう…。
ふと思い立って、androidエミュレータ上でテストを実行してみた。
同じエラーになったので、テスト環境のセットアップのどこかが悪そう…。
調べてるとよくdart ffi
なるものが引っかかるんだけど…なんだろう?と思ったら、外部関数インターフェースだそう。
このエラー、ライブラリ内にisar_get_instance
という名前の関数がみつからねーぞっていう意味だと思うんだが…
The following ArgumentError was thrown running a test:
Invalid argument(s): Failed to lookup symbol 'isar_get_instance': dlsym(0x7fd7a7507870,
isar_get_instance): symbol not found
↓でバージョンを修正したら直った…。
あー…2.1.0だったのに、2.1.4のライブラリを入れてたのかも…。
Isarのテスト用データベースのオープン/クリア処理をテスト本体の前後でできるようにした。
あー…RSpecがほしい…。
次は手元の開発環境のセットアップ方法をまとめるのと、CI上でテストを実行できるようにしなきゃ。
isarのセットアップ方法をまとめて…
ciで実行できるようにした。
linuxのsoについてるサフィックスのx86でなくてx64と気づくのに手間取った…。
テストのラッパーのsetUpAllでisarの初期化、tearDownでisarを空にするようにした。
いちいちクリア処理などを書かずに済むはず…。
CIのセッティングを兼ねた、記事のリスト表示、ようやくマージ!
いい感じにテストを書き進めてたんだけど…。
あれー…なぜ落ちる><
Rustで書かれてる中で出ているような…。
自動でリトライする方法がありそうなんだけどな…。
こう書けばよかったけど…。
ダメだった><
テストの順番を入れ替えても最初に実行されるテストで失敗する…。
なんか変。
article_page_test と my_home_page_test が同時に動こうとしている雰囲気を感じる。
成功した…!
えー…勝手に並列実行していたのか…。
テストの並列実行をやめたら通った…!
「簡単な日記アプリ」としてあとほしいものは…
- 日付
- 一枚画像添付
- アイコン
- 起動画面(スプラッシュ)
日付を選べるようにしました。
showDatePicker()を呼ぶだけで、↓みたいなカレンダーが出てくれて楽ちんでした。
画像の追加をやろうとしてます。
やめてしまったアプリで四苦八苦してたことをもう一度なぞっていきたいと思います。
ImagePickerで画像を取得し、表示するようにしました。
最初、Future<File?> _imageFrom〜()
のように画像を返すメソッドにしたら、setState()
にasyncな無名関数を書くなと言われ…
void setImageFrom〜()
として、インスタンス変数のimageFileにセットするだけの関数にし、これをsetState()
でくるんだら…画像が最初表示されず、何かしらほかの操作で更新が入ってから表示されるようになってしまい…
今のようになりました。
むずい…。
画像の保存、タイプコンバーターで楽になるかな…。
isarへの出入りで処理を挟み込めるから、案外…。
Fileに対してコンバータを書いたのに、機能しない…。
Fileを継承して作ってみるか…。
ふと思い立って、File?
の?
を取り去ったら、うまくいきそう…。
article.g.dartにimageFileが記載された。
コンバーターにちゃんとFile?を指定しなきゃアカンのでした…。
うまくいった。
File? <-> String はダメ。
File? <-> String? で、fromIsar()でNULLではなく、空文字を返すようにしたらうまくいった。
あと、アイコンとスプラッシュかな。
アイコンはこれで作る。
動かしてたら画像が消えちゃったりしてました…。
さきに、保存の仕方を修正しました。
え…こんなにインデックスもそろってるの?
タグを扱うのに、マルチエントリインデックスが使えそう…。
投稿日の逆順にソートするようにしました。
起動画面(スプラッシュスクリーン)はこれで行けそうです。
スプラッシュを追加した!
…だいたいやりたいことが終わったかも?!
ずっと面倒だったテストが並列でできない問題、プロセスIDごとに作業ディレクトリを作ることで回避しました。