もやもやエンジニア

IT系のネタで思ったことや技術系のネタを備忘録的に綴っていきます。フロント率高め。

KotlinとDataBindingとMVVMとか

※ こちらもどうぞ 2018/7/30 追記 AACのViewModel使ってFluxする - もやもやエンジニア

今までDateBindingをButterKnifeの代わりのような使い方しかしてなかったので、ちゃんとMVVMっぽい作りもやってみようということで前に作ったアプリをごそっと書き換えてみました。アプリはKotlin製です。

rei19.hatenablog.com

そもそものMVVM

  • 以下の記事・スライドを参考にしてます。こういうアーキテクチャ系の話の原点って何を探ればいいのかちょっとわからなかったので印象に残った記事を読みこんでます。

ugaya40.hateblo.jp

qiita.com

techblog.reraku.co.jp

techblog.reraku.co.jp

speakerdeck.com

  • 自分の中で噛み砕いたポイントとしては以下の点
    • MVVMのコンテキストでのModelはプロパティの公開と戻り値のないメソッドだけを実装している。ここでいうModelは例えばDDDのコンテキストでのDomainModelのようなものを指しているわけではないので切り離して考えること。
    • ViewModelはModelを監視している。メソッドの呼び出しによりModelの状態が変更された場合はその変化を翻訳してViewに伝える。
    • Modelは内部の状態が更新されたらViewModelに更新通知を送る
  • ※ 勘違いしてたらコメントください。。。

とりあえず作ってみよう

  • このアプリでははてぶの情報を取得するために、対象のユーザーのはてなのIDをSharedPreferencesに持っていていつでも更新することができます。なのでできることはユーザーIDの公開、ユーザーIDの更新です。

Model

  • ModelはシングルトンでViewModelにDIしてます。このアプリではユーザーIDを設定するダイアログとその呼び元のActivityのViewModelそれぞれでUserModelの更新イベントを監視しています。なのでユーザーIDが更新されたら呼び元のActivityも同時に更新されます。

UserModel.kt

    var user: UserEntity = getUserFromPreferences()
        private set(value) {
            field = value
            storeUserToPreferences(value)
            userUpdatedEventSubject.onNext(value)
        }

    // イベントを流す用のPublishSubject
    private val userUpdatedEventSubject = PublishSubject.create<UserEntity>()
    private val unauthorisedEventSubject = PublishSubject.create<Unit>()
    private val errorSubject = PublishSubject.create<Unit>()

    // PublishSubjectをObservableに変換して公開。PublishSubjectのままだとonNextが呼び元で使えてしまう
    val userUpdatedEvent: Observable<UserEntity> = userUpdatedEventSubject
    val unauthorisedEvent: Observable<Unit> = unauthorisedEventSubject
    val error: Observable<Unit> = errorSubject

    fun setUpUserId(userId: String) {

        // ここは http://b.hatena.ne.jp/[UserId]/ を叩いて200が返って来るかを確認して存在するユーザーかチェックしている
        hatenaBookmarkService.userCheck(userId).map {
            // 特定の記号が入っている場合にTopページを取得しているケースがあるのでトップページが返ってきたら 存在しないユーザー = 404として扱う
            return@map !it.contains("<title>はてなブックマーク</title>")
        }.onErrorResumeNext {
            if (it is HttpException) {
                when (it.code()) {
                    HttpURLConnection.HTTP_NOT_FOUND -> {
                        Single.just(false)
                    }
                    else -> {
                        Single.error(it)
                    }
                }
            } else {
                Single.error(it)
            }
        }.subscribeAsync({ isValidId ->
            // Model内でスレッドを指定して配信。subscribeAsyncは拡張関数
            if (isValidId) {
                // 有効なIDだったら渡されたIDを内部に保存
                user = UserEntity(userId)
            } else {
                // 無効なIDだったら認証できなかったイベントを発行
                unauthorisedEventSubject.onNext(Unit)
            }
        }, {
            // 通信エラーなどの場合は一律エラーのイベントを発行
            errorSubject.onNext(Unit)
        })
    }
  • ここでModel内のUserが更新されたらSharedPreferecesに保存して、userUpdatedEventを発行してます。
    var user: UserEntity = getUserFromPreferences()
        private set(value) {
            field = value
            storeUserToPreferences(value)
            userUpdatedEventSubject.onNext(value)
        }

ViewModel

  • ViewModelはonStartでModelのイベントの監視を開始、onStopで解除という感じにしてます。
  • DataBinding周りでProgressDialogとかSnackBarとか表示したい時はEventBus経由でActivityに通知しています。。。が、もっといいやり方がある気がします。
    • TwitterでアドバイスをもらってProgressDialogはViewModelにDIしたり、Activity/Fragmentにどうしても何かさせたい場合はViewModelがEventを公開してActivity/Fragment側でそれを監視するようにしました。EventBusは見通しが悪くなるのでこちらの方が追いやすいですね。

EditUserIdDialogFragmentViewModel.kt

    override fun onStart() {
        super.onStart()
        // Modelから通知されるイベントを翻訳してViewに伝える
        registerDisposable(userModel.userUpdatedEvent.subscribe {
            progressDialog.dismiss()
            idErrorMessage.set("")
            userId.set(it.id)
            dismissDialogEventSubject.onNext(Unit)
        }, userModel.unauthorisedEvent.subscribe {
            progressDialog.dismiss()
            idErrorMessage.set(userIdErrorMessage)
        }, userModel.error.subscribe {
            progressDialog.dismiss()
            snackbarFactory?.create(R.string.message_error_network)?.show()
        })
    }

    override fun onResume() {
        super.onResume()
        userId.set(userModel.user.id)
    }

    fun onClickSetUp(view: View) {
        progressDialog.show()
        userModel.setUpUserId(userId.get())
    }

おわり

  • Modelのメソッド呼び出しの結果をイベントベースで受け取るのはメソッドでObservableを返すよりわかりやすいかなと思いました。その場合はエラーハンドリングでどんなエラーが流れてくるか内部の実装を知っていなければならないので発生しうるイベントとして公開されてる方がModelを呼ぶ側は使いやすいです。

  • コードはこちら。主要な画面だけDataBindingとViewModelで書き直してます(わりと前に書いたものがベースなのでちょっと変なところがあるかも)。

続編のようなもの

rei19.hatenablog.com

百人一首暗記するアプリ作ってからの振り返り

rei19.hatenablog.com

  • 上の記事から3ヶ月たったので、数字とかやったこととかの振り返りメモです。

3ヶ月たった時点の数字

項目 数値 取得元
インストール 13,500(うちアクティブは5,500) play store
平均評価 4.382 play store
レビュー/評価数 31/76 play store
DAU 400 ~ 600 (うち新規は100 ~ 150くらい) Fabric
Crash率 99.50% Fabric
AdMobからの収益 飲み会1回分くらい/month AdMob

リリースしてからやったことなど

Bug-Fixの対応

  • 4.1系だけ札が真っ黒になる。
  • カスタムフォントの使い方がまずくてOOM出てた。

機能追加

  • レビューでもらった要望や自分でこれはあってもいいかなと思ったものは追加していった。
  • 読み上げの要望が何個か来ていたけどアプリのコンセプトには合わないかなと思ってるのでその辺は対応してない。シンプルが一番。

ASO

  • 百人一首"、"百人一首 暗記"で上位に来るようにアプリ名やら紹介文やら変えて様子を見た。後半はPlay StoreでABテストもしてみたけど有為な差が見られなかった。多分、僕の対抗馬の出し方が悪い。
  • 今のとこ"百人一首"は2位、"百人一首 暗記"は1位を取れてる。

レビューの返信

  • Googleの中の人のアドバイスに従ってレビューの返信をできるだけ頑張ってやってみた。

気づき

  • ニッチなジャンルでそんなに使われないかなと思ったけど、思ったよりインストールされたしDAUも安定してる。それなりに競合のアプリもあったがあまり更新されてないものも多いので、丁寧に作り込んで検索で引っかかるようにすればちゃんと使ってもらえる。
  • 上述のバグが原因で1点のレビューしてた人がいたけど、直した連絡をレビューの返信でしたら5点に変えてくれた。少なくとも低い評価のレビューしてる人にはちゃんとケアしてあげるとよさそう。
  • Firebaseを入れてみたけど、想定してた数字が取れていないので使い方の認識がズレてるとこがある。ドキュメント読み直して手を入れたい。

RecyclerViewにインクリメンタルサーチをくっつけたライブラリを作った

こんな感じでRecyclerViewにインクリメンタルサーチくっつけたやつを作りました。ライブラリを公開するのは初めてだったりします。

https://github.com/rei-m/filter-recyclerview/blob/master/images/demo.gif?raw=true

github.com

僕はAndroidアプリの開発を仕事として関わり始めたのが Lolipop が出たくらいのときでわりと後発です。特に凝ったことをしない限りはやりたいことを実現するライブラリはだいたい用意されていて、自作のライブラリを作る機会もなかったのですが、上で貼ったキャプチャのViewを作る要件に対応するライブラリはさくっと見つからず。。。

MsvSearchというライブラリがそれっぽい動きをしたけどViewやイベントの拡張ができなくて入れられなかったので、動きの部分だけ参考にして自前で書きました。で、せっかくなのでたいしたコードでもないけどお試しでライブラリ作るにはちょうどいい機会かなと思って、共通化できる部分を切り出して公開してみました。

だいたいREADMEに導入の仕方は書いておきましたが、導入側はフィルターの条件とRecyclerViewの表示部分だけを実装すればいいように作ってあります。

公開はJitPack.ioを使ってGitHub経由で落とせるようにしたのでandroid-mavenプラグインを突っ込むだけでした。お手軽ですな。BintrayにアップロードしてさらにJCenterに・・・というのは今回はやらなかったです。

TextViewでカスタムフォント使ったら InflateException が出るようになった

先日、下のアプリをリリースしました。で、ありがたいことに正月の暇つぶしに使ってくれてるのか、ちょこちょこインストールされてるのですが、CrashlyticsにInflateExceptionが結構な頻度で飛んでくるのに気付きました。

rei19.hatenablog.com

開発中の実機確認は自分の持ってるNexus5xを使っていて問題なかったのですが、試しに嫁が使っている割と古めのXperiaで動作確認してみると確かに落ちました。。。しかも落ちるタイミングが不定期なのでOOMっぽい。↓の画面のViewを作成しようとしたタイミングです。

f:id:Rei19:20170104231035p:plain:w150

原因と対応

  • このViewの札の部分はカスタムViewにしてありました。できるだけ本物の札っぽくみせたかったので縦書きにしてフォントもIPAフォントをカスタムしたものを使っていたのですが、そこの内部がよくありませんでした。
Resources resources = context.getResources();
Typeface typeface = Typeface.createFromAsset(context.getAssets(), resources.getString(R.string.app_font_file));     
setTypeface(typeface);

まあ見ての通りですが、Viewを作るたびにフォントファイルを読み込んでたので、表示のたびにめちゃくちゃメモリを使っていたというオチでした。Typefaceをシングルトンにして一回しか読み込まないようにして解決できました。

  • ↓の画像はFabricのクラッシュ数のグラフです。インストール数の増加に応じてクラッシュ数が増えていくのがわかりますが、対応後は激減しましたね。今回はちょっと確認が甘かったので反省。

f:id:Rei19:20170104234545p:plain:w300

ちはやふるにはまったので百人一首を暗記するアプリ作った

今年もぼちぼち終わりですね。さて、毎年1個くらいプライベートで何かWebサービスなりアプリなりリリースすることを目標にしてるんですが、今年はちはやふるにはまったのがトピックとしてあって、百人一首に興味が湧いたので暗記用のアプリを作りました。

※ちなみに ちはやふるはこちら。勧められて観る前は恋愛ものかなと思ってたらスポ根青春もので展開が熱かったです。開発合宿で聖地巡礼したりしましたね。

アプリはこちら (Androidのみ)

play.google.com

技術的な話

  • 仕事ではkotlinでアプリ書いてるので、今回は一周回ってJavaで書きました。Jack & Jillとか新しいツールも出てきてるし、メインストリームに乗っかったアプリを手元に置いておきたいなあという気持ちからJavaにしてます。
  • 技術的には目新しいことはしてないのですが、個人的に今回初めて使ったのは以下のもの
    • Orma
    • RxJava2
    • BottomNavigation
  • Ormaを使った理由も、仕事でRealm使ってるのでDB使うならSQLiteがいいなあってことで選択しました。一長一短あるなと思ったのですが、Realm使うとRealmに依存する設計になるので、個人的にはOrmaの方が自分の好みには合ってるかなと感じました。あとAuto Migrationが圧倒的に楽ですね。(まあアプリ自体は別にDBなくても作ることができる規模なのでこの辺は使いたいから使ったという感じ)
  • RxJava2はだいぶインターフェース変わったので、思ったより学習コスト高かった印象です。まだMaybeの使い所がよくわかってない。。。
  • そんな感じで技術的なチャレンジはあまりできてないのですが、目標のアウトプットはできたかなという感じで、仕事も気分良く納まることができました。来年は何作ろうかな。

コードはこちら

  • セキュアな情報は含まれてないのでローカルでビルドするには自分で足りないものを埋める必要があります。

github.com

MacBook Pro 15インチが入るいい感じのリュック買った

買ったのはこれ!

  • MacBook Pro 15インチを普段愛用していて、それを持ち運べてかつ機能性に優れたリュックを探していたのですが、色々見た結果、このAer FIT PACKというリュックを買いました。

実物の写真

  • 丸っこいデザイン

f:id:Rei19:20161207231410j:plain:w300

  • 中の収納は豊富

f:id:Rei19:20161207231628j:plain:w300

f:id:Rei19:20161207231840j:plain:w300

使用感

  • 少し小さいかな?と思ったのですが、条件通り15インチのMacBookが入るポケットがちゃんと付いていて、機能性も十分で気に入りました。旅行のお供にはこれだけだときついですが、普段使いにはこれで全く問題なさそうです。
  • 欠点は縦に置くとちょっとバランス悪く自立できないくらいですけど、立てかけて置けば問題無いですね。
  • という感じでAer FIT PACK おすすめです。もっと容量の大きいタイプもあるみたいなので、こちらも紹介しておきます。

君の名は。展に行ってきた。良さがあった。

君の名は。展に行ってきたので簡単なレポートなど。基本的に中は撮影NGなので雰囲気だけ。

f:id:Rei19:20161120133135j:plain:w300

場所

  • 長野県は小海町というところにある高原美術館で開催されています。

  • 小海は新海監督の出身地で、以前にも新海誠展が開催されたところです。僕はその時に初めて行って、この場所を気に入ったので、たまにぷらっと遊びに行くようになりました。

東京からの行き方

  • 電車とバスで行きました。東京から北陸新幹線佐久平へ行き、そこからJR小海線で小海駅または松原湖駅まで行きます。駅に着いたらバスかタクシーで美術館へ向かいます。

  • 電車とバスの時間は事前に調べてからのほうがよいです。小海線は1時間に1本、バスも1時間に1本あればいいという感じなので、タイミングが悪いと新幹線で佐久平についたけど小海線が来ない、とか小海駅についたけどバスがないとか、ありえます。

  • 個人的には、小海駅からの美術館までは、行きは割り切ってタクシーを使って、帰りはバスを使うのがオススメです。今回は君の名は。が予想以上にヒットしたので、いつもは適当につかまえていたタクシーが全然捕まらず、タクシーを呼んだっぽい二人組に声をかけて相乗りさせてもらいました。

  • 昼時に着いたら駐車場は満車だったので車で行く場合は早めに行ったほうがよいでしょう。

見所

  • 美術館自体はそこまで大きくないものの、貴重な絵コンテや君の名は。の原案など、ここでしか見られないものがあり、ファンの人は行って損はないと思います。

  • 美術館自体も山に囲まれた珍しい立地で、隣にはレストランと温泉が併設されています。君の名は。展を見て、レストランでランチを食べて、山々を見ながら露天風呂に入ってさっぱりして帰る、という贅沢な時間をすごすことができます。

  • 来館者一万人達成!

f:id:Rei19:20161120133018j:plain:w300

  • まわりはこんな感じでどこを見渡しても山ですね。自然が気持ちいい。

f:id:Rei19:20161120133541j:plain:w300

  • レストランでのランチ。信州は飯がうまい!

f:id:Rei19:20161120130842j:plain:w300

  • ちなみにレストランに君の名は。に出てくるパンケーキを再現したメニューというのがあるんですが、数量限定で予約制だったので、食べたい人は着いたら、先にレストランに行って予約できるか確認するといいかもしれません。(僕は知らずにいったのでありつけなかった。。。)

おわり

  • 新海作品は秒速5センチメートル以来のファンですけど、今回はめちゃくちゃヒットしたのでびっくりですね。前回の新海誠展に行った時は、僕が行った時間に来ていたお客さんは3組くらいだった記憶があるのですが、今回は駐車場は満車、レストランも満席という感じだったので人気のほどを伺えました。

  • 君の名は。展は12/25日までやっているので、今回でファンになった人も昔からファンの人も、行ってみるとより君の名は。を好きになるのではないでしょうか。