visible true

技術的なメモを書く

Android Architecture ComponentsのViewModelとHolderFragmentとActivity-Fragment間通信と。

Android Architecture ComponentsのViewModel周りの実装を読んでいくとふーんってなったのでActivity-Fragment間通信やれそうだしやってみたらいけたなーそりゃそうだねみたいな話

Android Architecture ComponentsのViewModelとViewModelProviders

Android Architecture ComponentsのViewModelは次のような抽象クラスである。なーんにもない。

public abstract class ViewModel {
    @SuppressWarnings("WeakerAccess")
    protected void onCleared() {
    }
}

もう一つApplicationを安全に保持したAndroidViewModelがある。

public class AndroidViewModel extends ViewModel {
    private Application mApplication;
    public AndroidViewModel(Application application) {
        mApplication = application;
    }
    public <T extends Application> T getApplication() {
        return (T) mApplication;
    }
}

ViewModelかAndroidViewModelを継承した上で、ViewModelProvidersを通してインスタンスを作る。

val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)

ViewModelProvidersにはFactoryをセットできる。

val viewModel = ViewModelProviders.of(this, factory).get(MainViewModel::class.java)

デフォルトではDefaultFactoryが使われる。DefaultFactoryではAndroidViewModelかそれ以外かを判定してインスタンスを作ってる。 立て込んだViewModelを作るときはFactoryを実装することになる。

ViewModelとHolderFragment

抽象クラスであるViewModelはなーんにもしてないからわざわざインスタンスを作るためにViewModelProvidersを通す意味がわからないと思うが、 Configuration ChangeでのActivity再生成に備えてViewModelProvidersはガンバってViewModelの保持機能を備えている。

内部を追っかけるとActivityやFragmentをkeyとしてViewModelを保持するViewModelStoresというクラスが見つかる。

public static ViewModelProvider of(@NonNull FragmentActivity activity) {
        initializeFactoryIfNeeded(activity.getApplication());
        return new ViewModelProvider(ViewModelStores.of(activity), sDefaultFactory);
}

ViewModelStoresはさらにholderFragmentFor関数でHolderFragmentというFragmentを取り出している。

public static ViewModelStore of(FragmentActivity activity) {
  return holderFragmentFor(activity).getViewModelStore();
}

HolderFragmentはふつーのFragmentである。メンバにViewModelStoreを持っている。 で、コンストラクタでsetRetainInstance(true)してる。

public class HolderFragment extends Fragment {
  private ViewModelStore mViewModelStore = new ViewModelStore();
  public HolderFragment() {
    setRetainInstance(true);
  }
  // ...
}

ViewModelStoreはHashMapでViewModelを保持している。

public class ViewModelStore {
  private final HashMap<String, ViewModel> mMap = new HashMap<>();
  // ...
}

ようするにUIなしFragmentじゃねーの

Activity-Fragment間通信

ActivityをkeyにViewModelインスタンスを取り出せるので、Activity-Fragment間で通信ができる。

たとえば2タブで子Fragmentからunread countをもらってタブに出すやつとか。 次のように更新の通知を受けたい値をLiveDataで用意する。

class MainViewModel : ViewModel() {
    val left: MutableLiveData<Int> = MutableLiveData()
    val right: MutableLiveData<Int> = MutableLiveData()
}

で、こういう感じでobserveしておいて、

// MainActivity
val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
viewModel.left.observe(this, Observer {
  tab.count.text = it.toString()
})

Fragment側で取り出して、更新すると

// Fragment
val viewModel = ViewModelProviders.of(activity).get(MainViewModel::class.java)
viewModel.left.value = 10

シュッ

f:id:sys1yagi:20170822221233p:plain

Source Code

詳細はソースを見てください。

GitHub - sys1yagi/aac-viewmodel-with

雑感

へーって思った

Androidのアーキテクチャ本をクラウドファンディングで執筆します - 共著者3人の紹介 #peaks_cc

peaks.cc Android アプリ設計パターン入門

ある日

f:id:sys1yagi:20170703130312p:plain

こんなこと書きます

個人的な思想としてアーキテクチャはチームのためにあると思っています。なのでそういう感じの章を書きます。章の紹介文を引用しておきます。

アーキテクチャはチームのために存在しています。チームとは人です。すなわち人がプロダクトを正しく作るためにアーキテクチャは存在しています。人を支えないアーキテクチャは意味がありません。Androidが出た当初は、限られたリソースの中でうまく動作させるために、組み込みアプリケーションの方法論が重要でした。 時代は進み、いまやAndroid端末のスペックは一昔前のPCと遜色がありません。提供する機能は複雑化し、画面も増加し、品質の要求は上がり、開発規模が増大して関係者も増えました。今こそアーキテクチャの出番がやってきたというわけです。 本章では中長期的にチームでAndroidアプリケーションを開発するにあたって直面した課題と、解決するために考えたこと、実際に行ったことを紹介します。取り扱うソースコードGitHubで公開しているMastodonAndroidクライアントアプリケーション「DroiDon」です。DroiDonは個人で開発を進めていますが、これまでのチームでの経験をすべて詰め込んで設計し、VIPERアーキテクチャを採用しています。これに近いアーキテクチャを著者の所属するトクバイのアプリでも採用してます。アーキテクチャ設計の議論のたたき台として活用できることでしょう。

皆さん

共著者を紹介します。皆さんマジはんぱねー人たちです。

日高 正博さん

  • 第1章 Androidアプリの基本構成
  • 第2章 MVPパターンを使ったアプリ構成
  • 第3章 MVVMパターンを使ったアプリ構成
  • 第4章 差分開発にみる設計アプローチ

ひつじさん。夏コミとかで2,30人技術者を集めて400P-500P(100Px5冊)書いたりするテクブの主催者。DroidKaigiとか技術書典とかの発起人でもある。peaks自体のアイデアにも確か関与していたような?とにかく彼が書くと言ったら書くんです。

今回は本書の方向づけをする序盤を担当しています。わかる!ドメイン駆動設計 ~もちこちゃんの大冒険~【C91新刊】などを見てもわかる通り、何をどのようにどの順番で伝えるべきかについての分解力は半端ないのですごくいい感じになるのではと今から期待しています。

共著者としては、原稿のビルド環境や、校正、編集作業にも精通しているので、安心して初稿をぶつけられます。

小西 裕介さん

  • 第5章 OSSにおける設計者の役割

こにふぁーさん。DroidKaigiアプリを始めた人!ブログでは技術的な話のほかにチーム課題とか技術者としてのふるまいについてなどもアウトプットしていらっしゃいます。さらにandroid-material-design-icon-generator-pluginなど1k超えのプラグインを公開してたりして色々すごい方です。

本書ではDroidKaigiアプリの設計の「なぜ」について解説します。OSSでは不特定多数の人が参加します。この時に気をつけるべきことは何か?暗黙知をいかに減らすかとか妥協点とか色々あるんじゃないかと思います。小西さんが当時何を考えていたか!めっちゃ気になります。

藤原 聖さん

ふじわらさん。普段はそこまで接点はなくて、DroidKaigi 2017辺りからちょくちょく絡んでいます。 実は同い年だという事がわかったんですが、貫禄が違う。落ち着きとでもいうんだろうか。見習いたい。

サイバーエージェントAndroid開発といえばfluxをAndroidに取り入れている事でわりと有名ではないかと思います。 QiitaでもRxJava + Flux (+ Kotlin)によるAndroidアプリ設計 - Qiitaとかを書かれてますね。 fluxのAndroidでの利用周りは「ふーん」くらいしか理解していなかったのがガッツリ解説めっちゃ楽しみです。

まとめ

なにこれめっちゃ読みたいんやけど。

Clean Architectureを理解するための補助的なコンポーネント図のようなもの

Clean Architectureを雰囲気でしか理解していなかったんだけど、なんでだろうな〜って考えるとあの図とか説明文がややこしいからだな〜と思った。 抽象的なやつはええねん、具体をくれ具体を〜、と思ったので、Android-CleanArchitectureのサンプルコードをコンポーネント図のようなものにおこした。

Android-CleanArchitecture Sample Code

実際にサンプルのソースを眺めると、レイヤの接続のための実装やAndroid固有(NavigatorなどIntentを処理するようなやつ)の実装などが混ざっていて、概念図とソースだけでは結構分かりづらい。しかもFragmentでActivityをキャストして使ってたり、UserCaseのコールバックをPresenterのインナークラスのObserverでやってたり、DataSourceは呼び出し毎に実装をnewしてたりわりとトリッキーだったり雑だったりするのでノイズが多い。

という事で概念が伝えている要素だけを抽出して関係だけを書いたコンポーネント図のようなもの*1を描いた。

f:id:sys1yagi:20170624213749p:plain

登場人物が少ないのでちょっとレイヤー化っていうイメージはつかみにくいなぁとは思うがこれはこれで理解の助けにはなるんじゃないかなと。

もうちょっと機能を足してみる

層感がないんでちょっと適当に機能を足してみた。新たな要素としてViewModelが加わっている。これは単純にViewのデータ群を管理するための要素として書いている。

f:id:sys1yagi:20170624213755p:plain

こういう図がいいのは実装を考えずにこうして機能を追加できたりする点だな〜とか思った。UML様〜。

それで

こういうのは各レイヤの責務が相互に漏れ出さないってのが重要なので、何かを足す時に気をつけないといけない。 たいていの場合Data層は固まるのが早い。初期のAPIセットを実装したら一旦終わるし、追加変更は大体同じようなことを繰り返すだけだからだ。 で、次にPresentation層。固まるっていうか表示要素とデータ、発生するイベントが決まれば大体OKだからどっちかっていうとレイアウトXMLの実装のほうが大変なくらいだ。 一番むずかしいのがDomain層で、ここが一番変化すると思う。特定のPresentation層に特化しすぎてたり、汎用的にしたけど結局共有できないから分離したり、試行錯誤が発生する。

まぁとりあえず概念の理解の助けになれば〜〜。個人的にはVIPERが好きだ。

*1:これって実際は何図って言うんだろう?

もうAndroidの非同期処理はasync/awaitでいいんじゃないかなぁと思った

Rx Ja Night Vol.2 - connpassで「 Androidの非同期処理をKotlinコルーチンで行う」という話をしてきました。

スライドで使っているコードは次のリポジトリに置いています。

github.com

今回取り扱った非同期処理の範囲

スライドやリポジトリのREADME.mdに大体書いているのですがコチラにも載せときます。 詳細な説明はスライドやリポジトリを参照してください。 次の非同期処理をコルーチンで実現します。

単発の実行
直列の実行
並列の実行
+
エラーハンドリング
キャンセル

環境

すべてKotlinが提供する標準の機能を用います。

implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:1.1.2-4"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:0.16"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:0.16"

単発の実行

コルーチン(async/await)ではこういう風に書けます。

val job = launch(UI) {
    try {
        val shop = async(CommonPool) { shopApi.getShop(10) }.await()
        // success
    } catch (e: Exception) {
        // error
    }
}

launch(UI)async(CommonPool)はコルーチンのビルダー関数と呼び、続いて渡すブロックをコルーチンにしてくれます。 ここではlaunch(UI)はコルーチン内をUIスレッドで実行する、async(CommonPool)はコルーチン内をスレッドプールで実行する、という理解でいいと思います。 毎度書くと冗長なので、次のようなパッケージレベル関数を用意して簡潔に書けるようにしたりできます。

fun <T> async(context: CoroutineContext = CommonPool, start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> T)
        = kotlinx.coroutines.experimental.async(context, start, block)

fun ui(start: CoroutineStart = CoroutineStart.DEFAULT, block: suspend CoroutineScope.() -> Unit)
        = launch(UI, start, block)

スッキリ

val job = ui {
    try {
        val shop = async { shopApi.getShop(10) }.await()
        // success
    } catch (e: Exception) {
        // error
    }
}

エラーについてもtry-catch式なので、様々な例外をよしなに扱えます。 uiの中でawait()して待ち合わせていて大丈夫かって気になりますが、 Kotlinコンパイラがよしなにコルーチン内のコードを状態毎に分解していて、 await()の呼び出しの所で処理を中断しているので大丈夫です。 await()が完了すると結果がdispatchされ、Handler.post()で続きを実行するといった具合です。

直列の実行

await()を後続のasyncの前か、その中で呼び出せばOK。

val job = ui {
    try {
        val userJob = async { userApi.me() }
        val subscriptionShopsJob = async { subscriptionShopApi.getSubscriptionShops(userJob.await().id) }
        val subscriptionShops = subscriptionShopsJob.await()
        // success
    } catch (e: Exception) {
        // error
    }
}

並列の実行

並列で実行したいasyncを呼び出したあとにawaitすればよい。

val job = ui {
    try {
        val userJob = async { userApi.me() } // start immediately
        val shopJob = async { shopApi.getShop(10L) } // start immediately
        val user = userJob.await()
        val shop = shopJob.await()
        // success
    } catch (e: Exception) {
        // error
    }
}

キャンセル

launch(UI)から返るJobcancel()を呼び出せばよい。

job.cancel()

するとコルーチンの中でCancellationExceptionが飛ぶ。

job = ui {
    try {
        val shop = async { shopApi.getShop(10) }.await()
        // success
    } catch(e: CancellationException) {
        // cancel
    } catch (e: Exception) {
        // error
    }
}

感想

特にデメリットもなさそうだし圧倒的に簡潔だし、もう非同期処理はasync/awaitでいいやって思った。 なんてったって非同期処理のための実装だからね。 このほかproducerやactorなどを使うと更にUIイベントのストリームとかイベントバスみたいなこともできそうだけど、 この辺はそのために用意されているかでいうと微妙な気もするのでどちらでもいい気もしている。 一応experimentalなんだけど、裏側の実装が変わる事はあっても利用コード側はそこまで影響受けないだろうしいいんじゃないかなとか。 プロダクトでも入れ始めているしそうしようみたいな気持ち。

参考

これ読んだらもう使ってよしだと思う。

KotlinでViewDataBindingをシュッとinflateするやつ

RecyclerViewなどでViewDataBindingを使う時に次のように書くのめんどくさくて。

class ViewHolder(val binding:ListItemCommentBinding)
: RecyclerView.ViewHolder(binding.root)
override fun onCreateViewHolder(parent: ViewGroup, type: Int) = 
  ViewHolder(ListItemCommentBinding
    .inflate(LayoutInflater.from(parent.context), parent, false))

こういう感じにViewGroupに関数生やすと、

ViewExtensions.kt

inline fun <reified T : ViewDataBinding> 
  ViewGroup.inflateBinding(): T {
    return T::class.java
            .getDeclaredMethod(
              "inflate",
              LayoutInflater::class.java, 
              ViewGroup::class.java, 
              Boolean::class.javaPrimitiveType
            )
            .invoke(null, LayoutInflater.from(context), this, false) as T
}

良さそう。

override fun onCreateViewHolder(parent: ViewGroup, type: Int) =
  ViewHolder(parent.inflateBinding()) 

ViewHolderが複数種類のViewDataBindingを取り扱う場合は型引数が要る。

override fun onCreateViewHolder(parent: ViewGroup, type: Int) =
  ViewHolder(parent.inflateBinding<ListItemCommentBinding>()) 

追記 2017/05/23

proguardで死ぬので、使うときは次の設定が必要になります。ひょえ〜

-keep class * extends android.databinding.ViewDataBinding {
    public static ** inflate(android.view.LayoutInflater, android.view.ViewGroup, boolean);
}

DataBindingが原因のビルドエラー時にエラーを抽出するスクリプト

AndroidでDataBinding周りミスるとめっちゃエラー出て辛いですね。辛いのでDataBindingのエラーを抽出するスクリプトを書きました。スクリプトはかなり雑なので適宜いい感じにしてください。

extract_data_binding_error.rb

#! /bin/sh
exec ruby -S -x "$0" "$@"
#! ruby

state = 0
while str = STDIN.gets
  break if str.chomp == "exit"
  case state
  when 0
    state = 1 if str.match(/.*Found data binding errors.*/)
  when 1
    state = 2 if str.match(/.*e: .*/)
    next if state == 2
    print str
  end
end

gradleのコマンドに2>&1を足してextract_data_binding_error.rbに流せばok

$ ./gradlew assembleDebug 2>&1 | ./extract_data_binding_error.rb
Identifiers must have user defined types from the XML file. user is missing it
file:///hoge/foo/git/android-app/app/src/main/res/layout/list_item_comment.xml Line:60


    at android.databinding.tool.processing.Scope.assertNoError(Scope.java:110)
    at android.databinding.annotationprocessor.ProcessDataBinding.process(ProcessDataBinding.java:89)
    at com.sun.tools.javac.processing.JavacProcessingEnvironment.callProcessor(JavacProcessingEnvironment.java:794)
    at com.sun.tools.javac.processing.JavacProcessingEnvironment.discoverAndRunProcs(JavacProcessingEnvironment.java:705)
    at com.sun.tools.javac.processing.JavacProcessingEnvironment.access$1800(JavacProcessingEnvironment.java:91)
    at com.sun.tools.javac.processing.JavacProcessingEnvironment$Round.run(JavacProcessingEnvironment.java:1035)
    at com.sun.tools.javac.processing.JavacProcessingEnvironment.doProcessing(JavacProcessingEnvironment.java:1176)
    at com.sun.tools.javac.main.JavaCompiler.processAnnotations(JavaCompiler.java:1170)
    at com.sun.tools.javac.main.JavaCompiler.processAnnotations(JavaCompiler.java:1068)
    at org.jetbrains.kotlin.kapt3.AnnotationProcessingKt.doAnnotationProcessing(annotationProcessing.kt:73)
    ... 50 more

こんな感じ。やったね

mastodon4j v0.0.6 をリリースしました

mastodon4jをリリースしました - visible true では0.0.3でしたが、0.0.6まで出ました。

差分はReleases · sys1yagi/mastodon4j · GitHubに書いてますがここでも軽く書きます。

v0.0.4

Release v0.0.4 · sys1yagi/mastodon4j · GitHub

mastodon4j

  • 各メソッドにContractを追加。PublicとAuthRequiredに分かれている。
    • 認証が必要なものと不要なものを明示するのが目的だったが、interfaceは@JvmOverloadsが使えないため0.0.7でやめる予定
  • Breaking メソッドの各関数にMastodon4jRequestExceptionのチェック例外を付与
  • Breaking booleanのgetter名をisXXXに変更
  • いくつかの関数に@JvmOverloadsを付与

mastodon4j-rx

なし

v0.0.5

Release v0.0.5 · sys1yagi/mastodon4j · GitHub

mastodon4j

  • Timelinesのpublicとtagにlocalパラメータを追加。これをつけるか付けないかでローカルタイムライン、連合タイムラインを切り替えるらしい。それに伴い次の変更を入れた
    • Deprecated Timelines#getPublic()
    • Add Timelines#getLocalPublic()
    • Add Timelines#getFederatedPublic()
    • Deprecated Timelines#getTag()
    • Add Timelines#getLocalTag()
    • Add Timelines#getFederatedTag()

mastodon4j-rx

なし

v0.0.6

Release v0.0.6 · sys1yagi/mastodon4j · GitHub

mastodon4j

  • Timelinesのlocalパラメータは、falseの時はパラメータを付与してはいけないという事で修正
  • Accounts.getStatusesにonly_mediaフラグを追加。メディアを持つ投稿を抽出する時に使うっぽい
  • 次のDeprecated関数を削除
    • Delete Timelines#getTag()
    • Delete Timelines#getPublic()
  • Link Headerをサポート。そのためにList<T>の代わりにPageable<T>Linkを導入

mastodon4j-rx

mastodon4jの変更に追従

その後

  • 0.0.7 Milestone · GitHub
    • raw jsonを取り出す仕組みを入れる
    • Mastodon4jRequestExceptionはResponseを内包しているけど、Mastodon4jRequestException自身にcode()とかを持たせる
    • Contractを捨ててアクセストークンが不要なメソッドはPublicクラスに集約する
    • できればStreaming APIをサポートする

所感

PR投げてくれる人がいたり(fix only_media param by takke · Pull Request #39 · sys1yagi/mastodon4j · GitHub)、

設計周りでアドバイスを頂けたりしてありがたいです。