どくぴーの備忘録

真面目なことを書こうとするクソメガネのブログ。いつ投げ捨てられるのかは不明

kotlinx.coroutinesでAndroid用画像ローダを実現する

まずこの記事について

この記事は #kosen10s Advent Calendar の6日目の記事です。

<- 前日の記事(@do_su_0805) 翌日の記事(@kogepan159)->

Kotlinについて何かを書きたい

ちょくちょくAndroidについて書いたりしている(最近イベントレポートしかしてない。まずい)んですが、最近業務でKotlinを書いてた(今はJavaに戻ったりしている)り、趣味コードや勉強会に出るときのサンプルを全部Kotlinにしたりしています。KotlinかわいいよKotlin。

というわけで今年も終わるということでちょっと趣味で書いているAndroid向けの画像ローダについて話してみます。ちゃんと欲しい機能ができればOSS化するつもりですが時間がなかなか取れなくてこいつにコミットする時間がなくてア

作りたい物

Androidで画像ローダといったら square/picassobumptech/glide が有名ですが、これらと似たような使用感の小さいライブラリを用意したいという感じです。usageとしてはこんな感じ

Katsushika.with(context)
        .load(url)
        .into(imageView)

トップクラスの命名は適当です。これを思いついたときはSplatoon2にドハマリしてたので思考回路がわかる人にはわかるはず。

何が必要か

ネットワークを介して画像をロードするので、Androidでは当然非同期処理を扱います(メインスレッドで通信はできない)。非同期で扱いたい処理を挙げると

  • 画像のネットワークからのダウンロード
  • 画像の縮小
  • etc.(キャッシュとか。今回は考えない)

そしてこれらの処理を終えた後にImageViewに対して画像を反映させるわけです。

まずは画像取得以外を作ってみる

とりあえずネットワーク周りはめんどくさいので今のところは square/okhttp を頼ります。

とりあえず最初のスニペットで動かせるように考えて実際にBitmapの取得以外をKotlinで実装するとこんなコードになるはず。 @JvmStatic をcompanion object内のwith関数につけるとJavaコードからも読めるようになるけど今回は省略。

class Katsushika private constructor(private val context: Context) {

    private var url: String? = null

    companion object {
        fun with(context: Context): Katsushika {
            return Katsushika(context)
        }
    }

    fun load(url: String): Katsushika {
        this.url = url
        return this
    }

    fun into(target: ImageView) {
        url ?: return

        // ここで画像ロード
    }

}

実際にpicassoやglideみたいに機能をバシバシつけたいときはこの部分を拡充していくことになるのだけど、キリがないので今回はここまでにします。

画像ロードをする

というわけでinto関数の中身を実装していきましょう。まずは愚直にCall.enqueueのCallbackを使って実装してみます。

fun into(target: ImageView) {

    OkHttpClient().newCall(request).enqueue(object: Callback {
        override fun onResponse(call: Call?, response: Response?) {
            val body = response?.body()
            body ?: return

            // 取得した画像バイト配列から縮小画像生成
            val byteArray = body.bytes()
            val options = byteArray.getBitmapOptions()
            val bitmap = byteArray.decodeByteArray(options, byteArray.size)

            // 対象のImageViewに画像を表示
            (context as Activity).runOnUiThread {
                target.setImageBitmap(bitmap)
            }
        }
    })

}

ちなみにここでのバイト配列からの縮小画像生成はByteArrayにKotlinの拡張関数を生やしたりして手抜きをしています

// Bitmapのサイズ情報を取得する
fun ByteArray.getBitmapOptions(): BitmapFactory.Options {
    val imageOptions = BitmapFactory.Options()
    imageOptions.inJustDecodeBounds = true
    BitmapFactory.decodeByteArray(this, 0, this.size, imageOptions)
    return imageOptions
}

// 取得したBitmapFactory.Optionsを元に2のべき乗のサンプルサイズで縮小する
fun ByteArray.getScaledBitmap(target: ImageView, options: BitmapFactory.Options): Bitmap {
    val widthScale = options.outWidth/target.width
    val bitmap : Bitmap
    if (widthScale > 2) {
        val imageOptions = BitmapFactory.Options()

        var i = 2
        while (i <= widthScale) {
            imageOptions.inSampleSize = i
            i *= 2
        }

        bitmap = BitmapFactory.decodeByteArray(this, 0, this.size, imageOptions)
    } else {
        bitmap = BitmapFactory.decodeByteArray(this, 0, this.size)
    }
    return bitmap
}

ここでinto関数を見ると、 Callbackの中で runOnUiThread ブロックを実行したりしている都合でネストが深めになってしまっています。しかもこんな感じでバシバシCallbackを使う処理を使っているとコールバック地獄になったりしてかなり怖い感じです。どうせならPromiseとか使って上から流れるように書いておきたい…

kotlinx.coroutinesを投入する

Coroutines - Kotlin Programming Language

kotlinx.coroutines はkotlin 1.1にてExperimentalとして実装された言語機能で、コルーチンを扱う低レベルAPIkotlin.coroutines.experimental を扱う高レベルAPIとなっています(開発者がコルーチンを扱うために触るAPIはこっち)。公式リファレンスのコルーチンに関するページを雑に読んでみると

  • コルーチンは軽量のスレッドのようなもの
  • 非同期処理の呼び出しでコルーチンを中断・再開することができる

みたいなことが書いています。kotlinx.coroutinesは外部ライブラリとして提供されているので、使用する際にはbuild.gradleのdependenciesに宣言してあげます。

dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:0.20"
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:0.20"
}

詳しく日本語で読みたい場合は以下の記事とかがいいかも…

qiita.com

コルーチンの中断・再開というのがとても重要で、コルーチン内から呼ぶことができるsuspend関数と言うものを使うようになります

suspend fun foo(): Bar { // 何かしらの処理... }

kolinx.coroutinesではES7やC# 5.0以降で提供されるようなasync/awaitが一機能として提供されており、非同期処理を同期処理っぽく書くことが出来ます。

fun requestItems() {
    itemApi.getItems(object: Callback {
        override fun onSuccess(item: Item) {
            textView.text = item.name
        }
    })
}

という風に書いていたものがkotlinx.coroutinesのasync/awaitを使うことで

fun requestItems() {
    val job = launch(UI) {
        // 一旦ここで関数の実行がitemApi.getItems()が完了するまで止まる
        val item = async { itemApi.getItems() }.await()
        textView.text = item.name
    }
}

こんな風にかけるようになります。 launch(UI) のブロックはコルーチンビルダーといい、引数のCoroutineContextにUIを与えることでUIスレッドで動くコルーチンを作成することになります。async のブロックでは新しく別のスレッドで動くコルーチンを作成し、await()を呼ぶことでブロック内の処理が完了するまで停止することができ、ブロック内の返り値を処理の完了後に返すことが出来ます。asyncの引数としてこちらもCoroutineContextを渡したりでき、これによって実行するスレッドを制御したりすることが可能です。

すると最初に書いたinto関数もいい感じに書けそうだ、という感じがしてきます。とはいうもののまずはCall.enqueueをsuspend関数として呼べるようにCallに拡張関数を用意してあげる必要がありそう。

Call.enqueueをsuspendCancellableCoroutineで包んであげて、その結果に応じてcontinuationの発火をさせるメソッドを変更するようにしてあげます(理解不足で説明が謎なことになっている…)

suspend fun Call.do(): ResponseBody {
    return suspendCancellableCoroutine { continuation ->
        enqueue(object : Callback {
            override fun onResponse(call: Call, response: Response) {
                val responseBody = response.body()
                if (responseBody == null) {
                    // responseBodyが空なら失敗として扱う
                    continuation.resumeWithException(Exception("ResponseBody is null")) 
                } else {
                    // このresumeがawait()した結果の返り値となる
                    continuation.resume(responseBody) 
                }
            }

            override fun onFailure(call: Call, e: IOException) {
                if (continuation.isCancelled) return
                continuation.resumeWithException(e)
            }
        })
    }
}

最後に、これを扱うようにinto関数を書き換えて完了です。

fun into(target: ImageView) {
    url ?: return

    val job = launch(UI) {
        val byteArray = async(CommonPool) { // Responseを待ってからバイト配列を取得
            OkHttpClient().newCall(request).do().bytes()
        }.await()
        
        val options = async(CommonPool) { // 結果からBitmapFactory.Optionsを取得
            byteArray.getBitmapOptions()
        }.await()

        val bitmap = async(CommonPool) { // 上2つが終わったらBitmapを縮小
            byteArray.decodeByteArray(options, byteArray.size)
        }.await()

        target.setImageBitmap(bitmap.await()) // bitmapが出力されたらtargetに表示
    }
}

まとめ

こんな感じでkotlinx.coroutinesを利用することで動作スレッド・処理の完了待機を明示的に宣言でき、非同期処理を同期処理っぽい羅列で書くことが出来ます。やはりこういう機能やPromiseなりで非同期処理を完結にかけると嬉しいですね。

実際はpicasso/glideが強すぎるのでこういう完全な車輪の再発明はうーんうーんって感じになりがちですが、新しい機能なりで試すと結構勉強になったりでいいなぁと思いました。

追記

今回はコルーチン内のasyncを途中でキャンセルさせることをあまり考えていませんでしたが、実際にコルーチンをキャンセルするとその中のasyncもキャンセル出来ないケースがあるそうで、そういうケースにはasyncのCoroutineContextにContext(こっちはAndroidの方)を与えることで親のジョブの子のような扱いになり、ジョブをキャンセルしたときに子もキャンセルされるようになるらしいです。そして async(context + CommomPool) のように和で指定することでUIスレッドでない状態でかつジョブのキャンセルもされるようになるらしいです。

↓参考↓

medium.com

Bonfire Android #2に参加してきた

お詫び

色々あって投稿が本当に遅くなりました。申し訳ありません…

はじめに

yj-meetup.connpass.com

11/6にYahoo! JapanのLODGEで開催されたBonfire Android #2に参加してきました。テーマとしては「Kotlin × サービス」ということで、Yahoo! Japan内外問わず様々なプロダクトでのKotlin採用に関するトークがありました。Toggeterはこちらになります。

togetter.com

まとめ

スライドのまとめ等はconnpassのページから見ることができるので、トークを聞いていて個人的に思ったトピックを幾つか…

導入・JavaからKotlinへのコンバート

やはりサービスでKotlinを導入するとなると新規開発はともかく、既存で動いているアプリケーションのJavaコードをKotlinコードへと置き換えるというケースが多く、その点に関してのお話をたくさん伺うことが出来ました。

  • とりあえずAndroid Studioのconvert機能で1ファイルずつKotlin化する
    • コンバートした後にCompanion Object内のvalconst valにしたりNonNull/Nullableの考慮をしたり
  • 既存クラス置き換えは良い勉強になる。とりあえず「習うより慣れろ」でKotlinの学習を進めるのもあり
    • もちろんKotlin Koansもとっても有効
    • よく言われているように自由度が高いのでチームとしてのベストプラクティスを持つのが大事だと思いました
  • 成長のためのKotlin導入
    • 「使う技術・言語は自分で決める。いい道具で仕事をしよう」という言葉が印象的でした
    • 成長したら/勉強したらやるから… -> いつ成長するの?いつ勉強するの?というジレンマ

エンジニア外から見たKotlin採用

特に興味深かったのが、スクラムマスター、ビジネスサイドと言ったメインでコードを書くようなエンジニアではない視点からの言及がいくつかあったことでした

  • Kotlin導入にビジネスインパクトはあるのか
    • 一応品質向上や広報効果といったインパクトはある
      • Javaと比較してNull安全であること等による品質向上
      • 最新技術に挑戦しているという技術広報的なメリット
  • そもそもリプレースをやるべきであるか?という問に明確に答えが出せないなら踏みとどまるという策もある
    • 置換え中のサービスのグロースは?
    • 新機能開発は?
    • ビジはどう説得するの?
  • 業務の一部を使ってインパクトのないところで検証して新規モジュールからじわじわKotlinにというケース
    • 既存モジュールへの影響を廃していつでもやめられる状態を作り出す
  • 置き換えを頑張るくらいならユーザさんに新しい価値を届けることに時間を使いましょう
  • スクラムマスター的には…?
    • タイミングがあればチームのモチベーション・成長観念的にKotlin導入は全然アリ
    • Kotlinの時点でコストがかかる、けどポジティブな結果を呼べるので取り組みたい…!
      • 導入に対するネガティブチェック・ロードマップの作成で取り組みへの説得力をもたせたい
    • ここまで動けるスクラムマスター、素晴らしいし大変だしで尊敬する…

まとめ

KotlinがAndroid開発の1st languageになったことでみんな導入したいという思いがある中でこういうモデルケースをたくさん伺うことが出来たのはとても良かったなと思いました。ただ、その中でもAndroid FrameworkはJavaで書かれているという事実もあって、Kotlinを導入してもJavaからは逃げることが出来ないということや、プロタクトのインパクト的な観点から導入に際してどう付き合っていくのか、チームとしてどうKotlinを導入して実装を進めていくのかなど、課題と言うか考えるべき案件もまだまだあるなと…。でもそれを乗り越えて得られるものもたくさんあると思いました。

主催のYahoo! Japanさん、発表者の皆様、ありがとうございました :bow:

第50回 情報科学若手の会に参加してきた #wakate2017

通算3回目の参加だそうです。社会人になってからは初めての参加っぽいです。

というわけで、今年も伊東市は山喜旅館にて開催された情報科学若手の会に参加してきました。なんと50回目。アニバーサリーって感じです。おまけに伊東市はその日は秋祭りで市政70周年だったそうで、こちらもアニバーサリーって感じでした。

前回の参加ブログはこちらです。

e10dokup.hateblo.jp

雑なまとめ

  • 相も変わらず話題が豊富
  • 50周年記念なので記念セッションがあった
    • 国立国会図書館に眠る記録が掘り出されていた
    • 案外昔想像されていたことが今実現されてたりして心温まる感じ
    • 手書きの報告書の書き手が村井純先生だったり
  • 交流イベント
    • 今年は何をやらされるかと思ったらQRコード陣取りゲーム
      • QRコードが読めなくなるまでお互いに付箋を貼って読めなくなった時点で一番多い枚数を貼った人が勝ちとか
    • 第1問の回答が各位絞り出したエモさで強かった
      • 「相互投票」になると思っていなかったので幹事の方のサンプルと100%一致する答えをしてしまって選外になりました :bow:
    • 強いQRコードリーダーは強い
      • スマホのリーダーは全滅してもなお読めたりする
  • ナイトセッション
    • 相変わらずエモい話で盛り上がる
    • ピングー
    • 今年のLT用プロジェクタの配置が神がかっていて一杯の人が聴ける感じでとても良かった
    • 結局毎日3-4時就寝
    • 突発的スプラトゥーンバイト若手の会
      • オフラインよりオンラインプラベのほうが落ちないっぽい
      • 評価が155%まで上がりました :bow:
  • お祭り
    • 二日目にお祭りがあったのでみんな外に出てみた
    • ついに旅館から出ることの許されなかった若手の会の参加者が外へと足を踏み出す奇跡の瞬間
      • こう書くとシャバの空気を吸うみたいな話になる
  • ネットワーク
    • 今年は明らかにオーバーキルだった
    • 幹事/野生の幹事の方々ありがとうございます
    • 快適にも程がある

ところで来年は…

幹事を勤めさせていただくことになりました

噂によると「初参加の人が突然肩を叩かれて気付いたら幹事になっている」とのことでしたが3年目にして突然肩を叩かれました。 今年幹事を引退される方々に負けないように幹事業をやっていきたいと思います。とにかくやっていくぞという気持ちです。

ちなみに幹事の杯として50年物のワインをみんなで頂きました。ワイン苦手なのですがなんかドチャクソ美味しくてすごかったです。

来年の情報科学若手の会もよろしくお願いいたします :bow:

ところで何かあなた発表したの?

ちゃんとした学術・技術的な発表はしませんでした。ただ幹事の方からご希望を頂いたので新宿御苑をひたすら勧めるLTをしていました。新宿御苑はリビングであると同時にコワーキングスペースなのでみんな行きましょう。寝ましょう。コーディングもしましょう。

ちなみに新宿御苑というと映画「言の葉の庭」で雪野先生が東屋で金麦片手にチョコレートを食べているのが印象にある方も多いと思いますが新宿御苑は禁酒ですのでお気をつけください。一体何を気をつけるのか…

potatotips #41 に参加してきました

potatotips.connpass.com

Yahoo! JapanさんのLODGEで開催されたpotatotips #41にAndroid ブログまとめ枠として参加してきました。 (2日ほど遅れてしまっていますが…)

当日の様子はToggeterからどうぞ

togetter.com

Android Tips まとめ

Physics-based Animations (rkowase さん)

qiita.com

Google I/O 2017中のAndroid Animations Spring to Life で発表された Physics-based Animationsに関するLTでした。

  • 物理法則や関数に従ったAnimationが簡単に実現できるようになった
    • 自然な見た目になり、視覚的違和感が軽減できる
    • Interpolator等を使っても関数のチューニングが必要だったりしたのでありがたい…
  • 途中で移動先の座標が変わっても自然に軌跡を修正してくれる
    • ちゃんと連続関数的になるように座標移動を修正している感じがすごい
  • 係数調整も自由度が高くて良さそう
    • SplingAnimationならバネ係数や剛性、FingAnimationなら摩擦係数など、ちゃんと係数レベルから調整できるのは細かい人には嬉しそう

Icon Fonts in XML (anikaido さん)

SupportLibrary 26で追加されたFonts in XMLに関するLTでした。

  • Fonts in XMLでレイアウトXMLでFont指定できるようになった
      • 従来ならCalligraphyでやっていたことがSupportLibraryでできるようになった、という感じ
    • これを利用してMaterial Icon Fontを導入したい
  • res/font/material_icon.ttfに配置して
  • android:fontFamilyで指定するとandroid:textで対応するASCIIコードを指定することでアイコンフォントが表示されるように
    • ASCIIコードを直打ちするの大変そう…、strings.xmlとかで対応付けしたい…

AsyncLayoutInflater vs Litho (KeithYokoma さん)

speakerdeck.com

非同期でLayoutのInflateを行うAsyncLayoutInflatorと宣言型のUIフレームワークである facebook/Litho の非同期UI読み込みを比較したLTでした

  • まだLithoはベータだけど非同期UI読み込みではAsyncLayoutInflatorのほうが速い
    • 相対的な大きさのせいか、AsyncLayoutInflatorの時間のブレが大きく感じる…
  • 1万のTextViewが並んだLayout XMLを見た後にLithoのforループで宣言できるのを見ると場合によってはとても良さそう

Android Font Updates (uecchi さん)

speakerdeck.com

本日二件目のフォントに関するLT。内容がかぶっていないDownloadable FontsとEmojiCompatに関するお話がメインでした

  • Downloadble Fonts
    • Google Fontsであれば端末内に入れることなく使えるようになった
      • FontとかにこだわってもAPKサイズを下げることができるので良さそう
      • CJK対応はまだなので日本で使われるようになるのは…
      • M以下で使おうとするとNPE… (´・ω・`)
    • 同じフォントはアプリ間で共有してリソース削減を狙う仕様は細かい配慮が効いていていいなぁと思った
  • EmojiCompat
    • Downloadable Fonts/APK Bundleで最新Emojiを古い端末にも適用させる機能
      • 豆腐削減の1手
    • Emoji/EmojiAppCompatHogeViewがある
      • TextView/EditText/Buttonがあるみたい
  • Android OからEmojiが丸くなるらしいです

swagger-codegenpojo生成 (kgmyshin さん)

swagger.jsonからPOJOを生成するswagger-codegenに関するLTでした。

  • リクエストやレスポンスがネストが深いときや複雑な構造になっているときにミスの可能性を減らせそう
  • 新規や後発で開発が始まったときにこういうことができると幸せが強い
    • swagger.jsonを使ってきっちりサーバサイドの仕様を決めている前提はあるが… ()

      Instant Apps (bina1204 さん)

www.slideshare.net

正式に使えるようになったInstant Appsの紹介LTでした。

  • Instant Apps 正式リリース :tada:
    • インストールいらずになる
    • どこからでもアクセスできて
    • 5.0移行でも動くようになるはずの
    • 既存のアプリにちょちょいと手を加えて動く様になる機能
  • Module細分化してAndroidManifestにエントリーポイントを貼るとok
  • デバッグ仕様だとUpdate Error -27なのでInstant Apps版のアプリをアンインストールしてから入れよう
  • 4MB未満が推奨されてるけど10MBでも動くよ!今はとりあえず!
  • Nearbyと組み合わせてイベントとかでNearby範囲内に入ったときにイベント専用アプリをインストールさせる、とか楽しそう

自動テストが無ければDeviceFarmを使おう (としさん)

openSTF/AWS Device Farm/Firebase Test Lab といったAndroidクラウドテスト環境に関するLTでした。

  • いろんな端末で自動テストが走る
  • 最近は自動テストがなくても走る
    • OpenSTF/AWS Device FarmはMonkeyTest
    • Firebase Test LabはRoboTest
    • Firebaseのほうがある程度こちらで制御できる部分があったりして賢くできそう
  • AWS DeviceFarm/Firebase Test Lab
    • AWS、Firebaseも無料枠が出来た
  • まだ発展途上だけど、やれることレベル的にFirebase Test Labが一番強そう

感想

前回(#21だったはず)に出たときもブログまとめ枠でした。今回も濃い内容で充実した時間でした。毎回こういう勉強会に出ると「次こそは発表したい…!」ってなるのでモチベーションを維持して行きたいです。

会場・お寿司・お酒を提供してくださったヤフーさん、どうもありがとうございました!

次回参加するときがあればよろしくお願いいたします!

DataBinding + RxJavaでMVVMパターンな設計を考える

今更感がすごいが、DataBindingを使うことによってAndroidアプリケーションの実装でMVVMパターンな設計を考えやすくなったし、DroidKaigi 2017のアプリがMVVMで実装されていたりするので、自分なりに設計をまとめてみる。

全体図

他で実装されている記事を見るとDDDなりと混ぜ合わせた感じの設計がちらほら見えて、一番シンプル(かつ集合知的な知見が溜まっている)と感じたDroidKaigi/conference-app-2017のアーキテクチャを丸パクリする形になった。

f:id:e10dokup:20170507160545g:plain

github.com

何をしているかざっと書くと

  • View
    • Activity/Fragment/Adapter ItemといったViewは1対1で対応するViewModelを持つ
    • 各Layout XMLには対応するViewModelをDataBindingでbindする
  • ViewModel
    • Viewの要素をクリックしたときの処理の定義やAPI/DBとのModelのやり取り、Viewへの反映を行う
    • ViewModelが取り扱うRepositoryはDagger2のDIを用いてインジェクトして利用する
  • Repository
    • ModelのCRUD操作を提供する
    • 各Modelのクラスに対応するRepositoryが存在する
    • RepositoryはDataSourceを持ち、DataSourceを利用してDBやAPIからデータを取得するが、ViewModel以下には隠蔽する。
  • Local/RemoteDataSource
    • 実際にModelのCRUD操作を行う
  • DataSourceからViewModelまでの処理はRxJavaでストリーミングに扱う

という感じ。

View

Viewは対応するViewModelをInjectして、Layout XMLにbindする。

public class MainActivity extends BaseActivity {

    @Inject
    MainActivityViewModel viewModel;

    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        getComponent().inject(this);
        bindViewModel(viewModel);

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        binding.setViewModel(viewModel);
    }

    // 以下省略
}

ちなみにBaseActivityの中は次のような感じ。ViewModelにもライフサイクル系メソッドを用意して、ActivityやFragmentのライフサイクルと同期して呼び出すようにしている。

public abstract class BaseActivity extends AppCompatActivity {

    private ActivityComponent component;
    private ActivityViewModel viewModel;

    @NonNull
    public ActivityComponent getComponent() {
        if (component == null) {
            MyApplication application = (MyApplication) getApplication();
            component = application.getComponent().plus(new ActivityModule(this));
        }
        return component;
    }

    protected void bindViewModel(ActivityViewModel viewModel) {
        this.viewModel = viewModel;
    }

    @Override
    protected void onStart() {
        super.onStart();
        checkViewModel();
        viewModel.onStart(this);
    }
    
    // 以下省略
}

ViewModel

ViewModelの実装は次のようにする。画面遷移は、Navigatorという画面遷移を取り扱うクラスを用意して、それをViewModelにinjectして行うようにしてみた。今回は直面していないがContextが必要な処理があるときはEventBusを使ってActivityにイベントとして流したほうがいい気がする…。

public class MainActivityViewModel extends ActivityViewModel {

    private final Navigator navigator;
    private final TaskRepository taskRepository;

    private ObservableList<TaskViewModel> taskViewModels;


    @Inject
    public MainActivityViewModel(Navigator navigator, TaskRepository taskRepository) {
        this.navigator = navigator;
        this.taskRepository = taskRepository;

        this.taskViewModels = new ObservableArrayList<>();
    }

    @Override
    public void onStart() {

    }

    @Override
    public void onResume() {
        taskRepository.findAll()
                .map(tasks -> Stream.of(tasks)
                        .sorted((o1, o2) -> (int)(o1.deadlineEpoch - o2.deadlineEpoch))
                        .toList())
                .map(tasks -> convertToViewModel(tasks))
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(taskViewModels1 -> {
                    this.taskViewModels.clear();
                    this.taskViewModels.addAll(taskViewModels1);
                });
    }

    // 以下省略
}

Navigatorの実装はこんな感じ。

@ActivityScope
public class Navigator {

    private final Activity activity;

    @Inject
    public Navigator(AppCompatActivity activity) {
        this.activity = activity;
    }

    public void navigateToCreateTask() {
        activity.startActivity(CreateTaskActivity.createIntent(activity));
    }

    public void navigateToTaskDetail(int taskId) {
        activity.startActivity(TaskDetailActivity.createIntent(activity, taskId));
    }

    public void closeActivity() {
        activity.finish();
    }
}

Repository

RepositoryにはLocal/RemoteDataSourceを持たせて、それらDataSourceにてCRUD操作を行う。例はローカルDBしかおいてないのでほぼ効果はないが、「ローカルDBにデータが有るときはローカルDBから、そうでないときはAPIから」という風に処理を分ける際にはここで分岐させる。

@Singleton
public class TaskRepository {

    private final TaskLocalDataSource taskLocalDataSource;

    @Inject
    public TaskRepository(TaskLocalDataSource taskLocalDataSource) {
        this.taskLocalDataSource = taskLocalDataSource;
    }

    public Single<List<Task>> findAll() {
        return taskLocalDataSource.findAll();
    }

    // 以下、省略
}

DataSource

DataSourceからの返り値はRxJavaのSingleで包んでストリーミングに流すようにする。例はLocalDataSourceだけだが、Retrofit等を用いてRemoteDataSourceを作るときはRetrofitのClientをinjectしてClientのアクセス結果をSingleで包む形になるはす。

public class TaskLocalDataSource {

    private final OrmaDatabase ormaDatabase;

    @Inject
    public TaskLocalDataSource(OrmaDatabase ormaDatabase) {
        this.ormaDatabase = ormaDatabase;
    }

    public Single<List<Task>> findAll() {
        return ormaDatabase.relationOfTask()
                .selector()
                .executeAsObservable()
                .toList()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }
   
   // 以下省略
}

終わりに

とりあえずこれを踏襲してサンプルでToDoアプリを作ってみた。

github.com

先人の知識に頼りっぱなしで実装したが、いざ組んで見てわかること、記事にして思い違いだったと気づくことがたくさんあるのでとても重要だと思った(感想)

ObservableList#OnListChangedCallbackでListViewやRecyclerViewのAdapterを更新する

TL;DR

  • ObservableList#OnListChangedCallbackでObservableListに格納されているデータの変更を通知できる
  • 変更を通知できるので「通知のあったitemのみViewを更新する」と言った処理も可能
    • RecyclerViewなら各コールバックメソッドに対応した変更通知が使える
    • ListViewには変更通知として notifyDataSetChanged() しかないので同様のことをしたい場合、getViewを独自に叩くことになるのでActivity/FragmentにObservableList#OnListChangedCallbackを配置するのが早そう
    • チャットUI等の実装にはListViewではなくてRecyclerViewを使ったほうがViewの更新回数が減りそう

ObservableList

DataBindingで実装されているObservableなコレクションの一種で、イメージ的には「データ変更を通知する機能を備えたList」。DataBindingにおける各データの通知方法(BaseObservable、ObservableField等)はこの辺を参照

developer.android.com

ObservableListは、コールバックにOnListChangedCallbackを持っており、(RecyclerViewについてきた)SortedListよろしくリスト内の要素の変更を通知することができる。というわけで、ListViewやRecyclerViewのAdapterに持たせるListにコールバックを実装したObservableListを与えることで、リストの要素が変更されたときや追加されたときなどにその変更をUIに反映させることができる。

Listの変更をListViewやRecyclerViewに通知したい

ListViewの場合

色々見ているとAdapterのコンストラクタ内でObservableList#OnListChangedCallbackをセットしていることが多そう。Adapterの変更通知は notifyDataSetChanged() しかないので、基本的にOnListChangedCallbackのどのコールバックメソッドが走っても(表示されている)全部の要素に対して getView() が実行され、描画されることになる。

public class ContentsAdapter extends ArrayAdapter<Content> {

    public ContentsAdapter(Context context, ObservableArrayList<Content> objects) {
        super(context, 0, objects);

        objects.addOnListChangedCallback(new ObservableList.OnListChangedCallback<ObservableList<Content>>() {
            @Override
            public void onChanged(ObservableList<Content> contents) {
                // リストそのものが変更されたとき
                notifyDataSetChanged();
            }

            @Override
            public void onItemRangeChanged(ObservableList<Content> contents, int i, int i1) {
                // iを始点としてi1までの範囲の要素が変更されたとき
                notifyDataSetChanged();
            }

            @Override
            public void onItemRangeInserted(ObservableList<Content> contents, int i, int i1) {
                // iを始点としてi1までの範囲に要素が挿入されたとき
                notifyDataSetChanged();
            }

            @Override
            public void onItemRangeMoved(ObservableList<Content> contents, int i, int i1, int i2) {
                // iからi1へi2の数だけの要素が移動したとき
                notifyDataSetChanged();
            }

            @Override
            public void onItemRangeRemoved(ObservableList<Content> contents, int i, int i1) {
                // iを始点としてi1までの範囲の要素が削除されたとき
                notifyDataSetChanged();
            }
        });
    }

    // 以下省略...
}

変更が通知された要素のみViewを再描画したい場合、 notifyDataSetChanged() せずに、直接AdapterのgetViewを叩くことで指定のViewのみを更新できる。しかしCallbackをAdapterのコンストラクタ内で定義するとうまくいかないのでListViewが表示されるActivity/FragmentでCallbackを定義してあげる必要があるし、実行するにしても getFirstVisiblePosition()getLastVisiblePosition() で表示されている範囲内の要素か判定する必要がありそう。 「ListViewの中に更にListViewやRecyclerViewがあって、再描画するとListView/RecyclerViewの表示が崩れたり一瞬消えたりする」場合に使うと言ったケースはあるかもしれない。

stackoverflow.com

RecyclerViewの場合

ObservableList#OnListChangedCallbackのセットはListViewのAdapterと同様、Adapterのコンストラクタ内でやっているのが多い感じ。ただ、RecyclerViewのAdapterはListViewのAdapterとは違って、notify系メソッドが用途別に揃っているっぽい。

  • notifyDataSetChanged()
    • データセットが変更されたことを登録されているすべてのObserverに通知する。表示されているItem全体を更新する
    • この場合のデータセットの変更は「既存の要素が全て有効ではなくなる」ことを想定させるもの。なのでこれを実行するとLayoutManagerは表示される範囲のViewの再描画を行う。
  • notifyItemChanged(int position)
    • 指定したpositionのitemが変更されたことを登録されているすべてのobserverに通知する。そのpositionのItemのみを更新する
  • notifyItemInserted(int position)
    • 指定したpositionのitemが挿入されたことを登録されているすべてのobserverに通知する。そのpositionのItemのみを更新する
  • notifyItemRemoved(int position)
    • 指定したpositionのitemが削除されたことを登録されているすべてのobserverに通知する。そのpositionのItemのみを削除する
  • notifyItemMoved(int fromPosition, int toPosition)
    • fromPositionのitemがtoPositionに移動したことを登録されているすべてのobserverに通知する。移動に関係のあるItemのみを更新する
  • notifyItemRangeChanged(int positionStart, int itemCount)
    • positionStartからitemCountの範囲だけのitemが変更されたことを登録されているすべてのobserverに通知する。該当範囲のItemのみを更新する
  • notifyItemRangeInserted(int positionStart, int itemCount)
    • positionStartからitemCountの範囲だけのitemが挿入されたことを登録されているすべてのobserverに通知する。該当範囲のItemのみを更新する
  • notifyItemRangeRemoved(int positionStart, int itemCount)
    • positionStartからitemCountの範囲だけのitemが削除されたことを登録されているすべてのobserverに通知する。該当範囲のItemのみを削除する

(引数にObject payloadを与えるものもあるが今回は特に使う機会がないので見なかったことにする)

RecyclerView.Adapter | Android Developers

qiita.com

この中からObservableList#OnListChangedCallbackのそれぞれのコールバックメソッドに相当するものを選ぶと、

  • onChanged - notifyDataSetChanged
  • onItemRangeChanged - notifyItemRangeChanged
  • onItemRangeInserted - notifyItemRangeInserted
  • onItemRangeMoved - notifyItemRangeRemoved

になる。onItemRangeRemoved(T sender, int positionStart, int itemCount)だけは対応するnotify系メソッドがないというか、notifyItemRangeMovedが存在しないので困ったところだが、

for (int i = 0; i < itemCount; i++) { 
        notifyItemMoved(fromPosition + i, toPosition + i); 
} 

でforでちまちま回すか notifyItemRangeChanged(fromPosition, toPosition + itemCount) で範囲変更として扱うかで対応できそう。(ここに関しては試していないので自信がない)

public class ContentsRecyclerAdapter extends RecyclerView.Adapter<ContentsRecyclerAdapter.BindingViewHolder> {

    private ObservableArrayList<Content> objects;

    public ContentsRecyclerAdapter(ObservableArrayList<Content> objects) {
        this.objects = objects;
        this.objects.addOnListChangedCallback(new ObservableList.OnListChangedCallback<ObservableList<Content>>() {
            @Override
            public void onChanged(ObservableList<Content> contents) {
                notifyDataSetChanged();
            }

            @Override
            public void onItemRangeChanged(ObservableList<Content> contents, int i, int i1) {
                notifyItemRangeChanged(i, i1);
            }

            @Override
            public void onItemRangeInserted(ObservableList<Content> contents, int i, int i1) {
                notifyItemRangeInserted(i, i1);
            }

            @Override
            public void onItemRangeMoved(ObservableList<Content> contents, int i, int i1, int i2) {
                for (int j = 0; j < i2; i++) {
                    notifyItemMoved(i + j, i1 + j);
                }

            }

            @Override
            public void onItemRangeRemoved(ObservableList<Content> contents, int i, int i1) {
                notifyItemRangeRemoved(i, i1);
            }
        });
    }

    // 以下省略...
}

このようにしてあげることでListViewのAdapterのnotifyDataSetChanged()ではできなかった「変更があった要素のみViewを更新する」ことが可能になる。ついでに何かアニメーションもしてくれる。このようにすると、単純にListViewに比べてもViewの描画回数が減るので、ポーリングでリストの内容を更新したり、チャットUIの実装を行うときにDataBindingを扱う場合、RecyclerViewを使ったほうが良さそう。

東京に来ました(就職)

やべぇブログ書いてねぇ

広告に目をやられました。ブログに広告が出っぱなしなのはつらいのでとにかく書いていこうかなと思います。

そういえば大学を卒業していました

いわゆる「17新卒社会人」になりました。明石高専から編入した神戸大学を学部で卒業し、就職して東京に住むことになりました。今は渋谷駅へのアクセスがし易いところ(曖昧)に住んでいます。今後もずっとプログラムを書いていく事になりそうなので当面の目標は 渋谷のイケイケパリピプログラマ です。嘘です。ちゃんと真面目にプログラミングしたいです。

高専から大学に編入した人だと、だいたい大学院への進学が多いような印象があるのですが、僕は色々考えた上で院への進学を諦めました。学力面とかではない…はずです。究極的に言うと

「研究室のM1の先輩方が大学院の授業関連でめちゃくちゃ忙しそうにされていた」

のが決め手でした。大学院に進んだ編入生の同期もなんだかんだ忙しそう。

東京来て変わったこと

  • イベントとかに参加するのに全く苦労しなくなった
    • 地方でイベント、あまりないしやるにしても大変というのはある
  • 人がいっぱいいる
  • 自己管理が大変
    • 体調崩しても頼れる人がいない…
    • 自堕落になると戻れない
  • 食費がやばい
    • ランチで1000円とか飛んで行くのでぐるぐる目になってきた
    • 自炊しないと財布も胃も持たない
  • 休日が家事で溶ける
    • 今日も買い出しと掃除と洗濯と料理してたら一日溶けた
    • ありきたりだけど親のありがたみがすごい

ネタがなくなった

技術的な話をしようと思ったけどここ一ヶ月とても忙しかったせいかまとめられるようなスタックがないのでGW中に何か技術的な記事書きます。

  1. なんでこんな時期にこんなエントリを書いたんだ
  2. 一ヶ月経って落ち着いたのもあるけど一番の理由は 「4月直後だとみんな書いてるし便乗感が出るから」

おまけ

ひとりぐらしはたいへんなので助けてください

http://amzn.asia/7chMyfR