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があるので待ち合わせとか特に考えなくても普通にコードを書いていくだけで実現できるのが良いと思う。