FlutterでTwitterクライアントを作ってみた
FlutterでTwitterクライアントを作ってみた。 レイバン製造機になる未来しか見えないので公開したりはしないけれど。 とりあえずマルチアカウント対応とタイムラインの取得を実装して力尽きた。 twitter_loginなどのライブラリがflutter(dart)でも存在しているようだが、そういうのは利用せずに直接APIとやり取りする形で実装した。
- crypto Twitter APIを利用する署名計算のため
- cryptoutils 同上
- url_launcher ブラウザ立ち上げのため
- flutter_redux
- redux_persist_flutter
- mockito モックテスト
Access Tokenの取得はこの記事を参考にして実行した。
Androidネイティブから入門した身からすると、dartではなくKotlinで書きたいという気持ちでいっぱい。
はじめはセミコロンのつけ忘れが多発し、その次はインスタンスを生成する際にnewを付け忘れるが多発して困ったからという理由が大きかった。newのつけ忘れは、なくても動く場合と、つけないと動かない場合があって、その違いがいまいち分かっていない。
しかも付け忘れて動かないのは、実行しないとわからないのが更に困りものだった。
Kotlinで書きたい理由は、今だとこの2種類の理由から。
- data classが使いたい
- Null許容・非許容を型で表現したい
data classについてはdartのissueに上がっているので、そのうち実装されるかもしれない。
Javaで書くことに比べたらdartでのデータオブジェクトの記述はスッキリしている(getter/setterを定義する必要はない)のだが、data classならequalsメソッドをいちいちオーバーライドしなくても済むとか、そういうところが便利だと思うので、早くdartにもdata class来てほしい。
まあそれは来たらいいな程度の気持ちだけど、null許容・非許容を型で表現したいのは結構切実な問題である。
dartはカジュアルにnullが飛んできすぎな気がする。
dartではあらゆるものがオブジェクトなので、intだろうと初期化されていなければnullとなるので、nullと格闘することが多かった。
このあたりは私の実装の仕方が悪いという面も大きいかもしれない。
が、それにしてもnull許容・非許容が型で表現できるKotlinを知っていると、nullで消耗するのがつらい。
Textにnullが渡ってアプリがクラッシュする→どこでnullが渡ったのか把握しきれないとかいうのが多くてね・・・。
asyncなメソッドを定義してやればそれだけで非同期処理が記述できてしまうのは非常に便利だと思った。
awaitと組み合わせて複数のリクエストを同期的に書けるのはめちゃくちゃ楽だった。
Androidネイティブだと、RxJavaを使って連結させてやるような処理がシンプルに書けるのはよい。
ネットワークを使った処理がシンプルに書けるのはすごい楽だった。
アプリ内の状態を管理するのにreduxを使った。
最初はflutter_reduxから導入の仕方を学んだのだが、redux初見の私にとってはこれは悪手だった。
なぜシングルトンで管理しているはずの状態を、わざわざ_ViewModelに移し替えているのか最初は理解できなくて混乱した。
flutter_reduxはredux.dartとflutterの橋渡しをするライブラリなので、そこを切り離して考えないといけないだろう。
アプリケーションの状態を1つのクラスに集約する(名前は別になんでもいい。以下利便性のためにAppStateと記述するけど、名前はなんでもいい)
状態クラスはイミュータブルで変更不可にする(重要)
AppStateはアプリケーション内で唯一の存在となるStoreが保持する
状態を変更するのはActionで、ActionはStore経由でdispatchする。dartには型があるので、○○Actionみたいなクラスを作っていく。
Actionと状態の変更を対応付けるのがReducer。
Reducerはネストというか、AppStateが持つ各フィールド(個々のアプリケーションの状態。カウンタの値とか、Twitterクライアントならツイートの一覧とか)とReducerを個別に対応させることもできる。
この場合、AppStateの特定のフィールドの値と、それを変更するActionの対だけをそのReducerで管理すれば良くなるので、見通しが良くなる。
Reducerでは変更する状態とActionの組み合わせを使って新たな状態を返す。
AppStateの変化はStoreからStreamとして流れてくる。このStreamはbroadcast streamなので、状態変更に関心を持つやつが、このStreamをlisten()すれば状態変更をキャッチできる。
MiddlewareはStoreにActionがdispatchされて、そのActionがReducerに渡される前に処理を挟むことができるやつ。例えば流れてくるActionをログに保存するとか、状態をファイルに保存するとか、1つのActionを複数のActionに分割するとか。
今回のTwitterクライアントでは、サインインのアクションをMiddlewareで処理して、その結果からAccess Tokenを更新するActionを発行してアプリ内のAccess Tokenを更新するような処理を行った。
dart.reduxのおさらい
- アプリケーションの全状態を保持する`AppState`を用意する
- `Store`が`AppState`を保持する
- `Store`経由で`Action`を投げる
- `Reducer`が`Action`を元に`AppState`を更新する
- `Store`の`onChange`を`listen()`することで`AppState`の変更を監視する
flutter_reduxが担うのは、各WidgetからStoreへアクセスするしくみを提供すること。
- `Store`の保持
- 保持した`Store`へのアクセス
- `Store`からの変更の`listen()`の隠蔽
アプリケーションのルートWidgetをStoreProviderにして、Storeを保持する。
子のWidgetではStoreConnectorを通じてStoreにアクセスする。
StoreConnectorはconverterを経由してAppStateをViewModelに変換する。
ViewModelというのは、該当のWidgetを構築するのに必要なだけの状態を別途切り出したやつというイメージ。
状態変更の度にWidgetの更新が走らないようにする役目もある。
状態を持ってるのにStatelessWidgetになるというのがいまいち理解できなかったが、状態を管理しているのはStoreであってこのWidgetではないからだろうか。
redux_persist_flutterは、reduxで管理するアプリの状態をファイルに書き出して永続化するライブラリ。 今回Twitterクライアントをflutterで作っていて、データの永続化をどうすればいいのかいまいち分からなくてとりあえずこれを使った感じである。 別にシリアライズ方法はなんでもいいのだろうけれど、JSONにパースするようになっていたのでそのようにした。 しかしStoreで管理する状態が増えれば増えるほど、JSONへのパース処理を追加するのが大変になって辛くなっていく。
簡単なKey/Valueのデータであれば、shared_preferencesを使えばいいのだろうが、アプリのデータを保存するのには向かない。
自前でファイルに保存するか、Databaseのライブラリを導入して保存するかといったところなのだろうか。
どっちにしろデータオブジェクトとのマッピング処理を自前で実装しないといけないのが面倒くさいところ。
またデータの暗号化もどうしたらいいのやらという感じでよくわからない。
エラーメッセージがわかりにくいところがツライ。エラーの内容は分かるものの、そのエラーが自分の書いたコードのどの部分で発生しているのかを把握するのに大変労力を必要とする。
UIの記述がツライ。 ネストが深くなりすぎてツライ。 これ絶対後からいじれなくなるやつだと思う。
IDEのサポートが受けられるのはめちゃくちゃメリットだと思う。 IntelliJは偉大だ。 無料で使えるCommunity Edditionが使えるのは偉い。 React NativeだとWebStormになると思うので(IntelliJ系IDE使うなら)、そこはメリットだろう。
テストコードも書きやすいしいい感じだと思う。
ネストしたテストコードがすっきりと書けるのが非常に良い。
パラメタライズテストはちょっとやり方がよくわからなかった。
Mockitoも使えるし、使い方も普通にMockitoなので特に新しく学習する必要が無いのも良い。
Mockクラスを用意するやり方がちょっと変わってると思ったけども(class MockClass extends Mock implements RealClass{}と定義してやる)。
UIのテストは実際には書かなかったが、サンプル見る限りEspressoでUIテストを記述するより簡単にできそうな印象。 async/awaitがあるので待ち合わせとか特に考えなくても普通にコードを書いていくだけで実現できるのが良いと思う。