visible true

技術的なメモを書く

facebook/Sonarで大体Stethoを置き換える

Stetho便利ですよね〜。 最近Stethoを置き換えるfacebook/Sonarというライブラリが登場しました。 メトリクスツールのSonarとかDAWCakewalk SONARとかとかぶってて名前紛らわしいっすね〜

facebook/Sonarでできること

StethoはAndroid向けのライブラリであったのに対し、SonarAndroidiOSの両方の環境をサポートしています。 また独立したElectronアプリを提供していて、Chrome Dev Toolsをポチポチする必要がなくなりました。やったね。

f:id:sys1yagi:20180718112432p:plain

SonarにはBuilt-inのプラグインがいくつかあります。とりあえず現状あるやつを紹介しときます。

Logcat

Logs · Sonar

SonarのElectronアプリを起動するととりあえずLogcatが見れます。アプリ側で特に対応とか入れなくても適当につなぐみたいです。 あとなんか画面キャプチャとか動画撮影もやれるみたい。adb~

Layout Inspector

Layout Inspector · Sonar

Layoutの樹海に潜っていじれるやつです。

f:id:sys1yagi:20180718113315p:plain f:id:sys1yagi:20180718113318p:plain

Stethoでも出来た気がするな。どうだったかな。

Network Inspector

Network · Sonar

こちらもStethoで出来たやつ。概ねできることは同じはず。後ほど書くけどOkHttp3しかサポートしてないので、まだOkHttpを使ってる場合は使えないので注意。

f:id:sys1yagi:20180718113546p:plain

Shared Preference Viewer

Shared Preferences · Sonar

Shared Preferenceの中身を見たり編集したり。 ただ、特定のShared Preferenceを指定する必要があるので、Stethoよりpoorになった印象。

https://fbsonar.com/docs/assets/shared-preferences.png

Sandbox

Sandbox · Sonar

よくわかってないけど、key-valueを設定しておくと、Sonarアプリ側からいじれるようになる感じっぽい。 アプリの環境変数を動的にいじれるぜ的なsomethingという理解でよさそう。

facebook/Sonarでまだできないこと

できないことというか、plug-inがまだないというか..

  • SQLite database viewer
    • いずれBuilt-inでサポートされるとは思うけどまだない
  • Realm database viewer
    • 欲しい
  • no-op dependenciesがない
    • get startedではdebugImplementationで書けといいつつリリースビルド時に死にがち (プラグイン使うとガッツリコードで依存する。build-typeで分離はできる範囲だとは思う)

ちょ〜っと厳しいよねまだ..

導入手順

まあGetting Startedに書いてあるんですが、 dependenciesを追加して...

repositories {
  jcenter()
}

dependencies {
  debugImplementation 'com.facebook.sonar:sonar:0.6.11'
}

Applicationなどで初期化します。

fun initSonar() {
  SoLoader.init(this, false)
  if (BuildConfig.DEBUG && SonarUtils.shouldEnableSonar(this)) {
    AndroidSonarClient.getInstance(this).start()
  }
}

pluginを導入する

さてそのまま実行しても特になにも起こりません。pluginを入れないと。 とりあえずLayout InspectorとShared Preference Viewerは次のように設定します。

fun initSonar() {
  SoLoader.init(this, false)
  if (BuildConfig.DEBUG && SonarUtils.shouldEnableSonar(this)) {
    AndroidSonarClient.getInstance(this).apply {
      // layout inspector
      addPlugin(InspectorSonarPlugin(this@Application, DescriptorMapping.withDefaults()))

      // shared preference
      // 第二引数にpreference名を指定できる
      // 引数渡さない場合はデフォルトのpreferenceを参照する
      addPlugin(SharedPreferencesSonarPlugin(this@Application)) 
    }.start()
  }
}

Network Inspectorについてはもうちょい複雑で、pluginの初期化のほかにOkHttpClientのNetworkInterceptorにもpluginインスタンスをセットしないといけないのでちょっと大変。

val networkSonarPlugin = NetworkSonarPlugin()

fun initSonar() {
  SoLoader.init(this, false)
  if (BuildConfig.DEBUG && SonarUtils.shouldEnableSonar(this)) {
    AndroidSonarClient.getInstance(this).apply {
      addPlugin(networkSonarPlugin)
    }.start()
  }
}

OkHttpClientを作る時にNetworkSonarPluginインスタンスをどっから引っ張ってくるか..

@Provides
@Singleton
fun provideOkHttp3(): OkHttpClient {
  return OkHttpClient.Builder().apply {
    // initSonar()の時と同じインスタンスが必要
    addNetworkInterceptor(SonarOkhttpInterceptor(Application.networkSonarPlugin))
  }.build()
}

Daggerでinjectすんのがいいだろな〜。ちょっとめんどくさい。 SonarOkhttpInterceptorはOkHttp3向けにしか存在しないぽいので注意してください。

ここがいいよねSonar

アプリ起動したら自動的にNetwork Inspectorがキャプチャ開始してくれるので、 Chrome Dev Toolsで毎度立ち上げ直していた手間がなくなってその点はめっちゃ良い。 今の所それくらいかな...

hyperionの様子

SQLite database viewerが無いのはかなり厳しい気がしているので、せめて代替えなんかできないかということで、 Hyperion Androidhyperion-sqliteあったなーって思ったら、 not yet released だった hyperion-sqlite not yet released? · Issue #137 · willowtreeapps/Hyperion-Android · GitHub 諦め。

おわりに

自分は導入してしまったけど、まだ様子見な気がする。絶賛開発中なので導入よりいろいろcontributeを考えるほうがいいのかもなと思うなどした。 SQLite database viewerはちょっと試しに作りたい。

Android Studio自体のヒープサイズを変更する

Android Studioでしばらく開発していると、一文字打ったり、行移動するだけで一瞬固まるといった現象に見舞われる。CPUが定期的にスパイクしていて、待てど暮らせど改善しない。再起動すると直るがしばらく触っているとまた遅くなる。

どうもメモリが足りなくなってGCしまくっているため起こるらしい。

どうにかならんものかと調べたら公式に設定方法があったのでメモ

Android Studio の設定 | Android Studio

[Help] > [Edit Custom VM Options] で studio.vmoptions ファイルを開き( or 作り )、 -Xmx4gとか書いて再起動するといける。

やったね。

nativechainをKotlinで実装する

nativechainとは

nativechainはブロックチェーンの基本的な概念を200行のjavascriptで実装したものです。 https://github.com/lhartikk/naivechain

nativechainは次の実装および制約を持っています

  • ノードをコントロールするHttpインタフェース
  • Websocketを使ったP2Pノード
  • データ(ブロックチェーン)を永続化していない
  • Proof of WorkやProof of Stakeはない。無条件でチェーンにブロックを追加できる

Kotlinで実装する

ブロックチェーンの基本的な概念を学ぶには実装するのが手っ取り早いよね〜という事でnativechainを写経してもいいんだけど、どうせならKotlinで書いてみたいよね〜という事でKotlinで実装しました。

GitHub - sys1yagi/nativechain-kotlin: https://github.com/lhartikk/naivechain by kotlin

ブロックの実装

ブロックのデータ構造は極めてシンプルなのでdata classでシュッ

data class Block(
    val index: Long,
    val previousHash: String,
    val timestamp: Long,
    val data: String,
    val hash: String
)

ハッシュはSHA-256を使っている。JavaだとMessageDigestでやれるので楽。バイト配列をhex stringに変換するのがKotlinだと少し面倒だった。

private fun calculateHash(
  index: Long,
  previousHash: String,
  timestamp: Long,
  data: String): String {
  return MessageDigest.getInstance("SHA-256")
    .digest("$index$previousHash$timestamp$data".toByteArray(StandardCharsets.UTF_8))
    .joinToString(separator = "") { "%02X".format(it) }
}

Httpサーバの実装

なにを使うか迷ったけどJetBrains製のktorを使うことにした。

compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
compile "io.ktor:ktor-server-core:$ktor_version"
compile "io.ktor:ktor-server-netty:$ktor_version"
compile "io.ktor:ktor-websockets:$ktor_version"
compile "ch.qos.logback:logback-classic:1.2.1"

次のようにシュッとサーバを立てられる。

embeddedServer(Netty, httpPort) {
  routing {
    get("/blocks") {
      val blockchain = jsonConverter.toJson(nativeChain.blockchain)
      call.respondText(blockchain, ContentType.Application.Json)
    }

    post("/mineBlock") {
      val data = call.request.receiveContent().inputStream().bufferedReader().readText()
      val mineBlock = jsonConverter.fromJson(data, MineBlock::class.java)
      nativeChain.addBlock(nativeChain.generateNextBlock(mineBlock.data))
      webSocketServer.broadcastLatestMessage()
      call.respond(HttpStatusCode.OK)
    }

    get("/peers") {
      call.respondText(webSocketServer.sockets().joinToString(separator = "\n") { it.peer() })
    }

    post("/addPeer") {
      val data = call.request.receiveContent().inputStream().bufferedReader().readText()
      val peer = jsonConverter.fromJson(data, Peer::class.java)
      webSocketServer.connectToPeers(listOf(peer))
      call.respond(HttpStatusCode.OK)
    }
  }
}.start(wait = true)

WebSocketサーバの実装

WebSocketサーバもktorを利用。embeddedServer関数でガチャガチャ書いてると動くので便利。接続が確立されるとWebSocketSessionが渡ってくる。WebSocketSessionはincoming(ReceiveChannel)とoutgoing(SendChannel)を持っている。コルーチンなので待機とかその辺がいい感じでいい。

embeddedServer(Netty, port) {
  install(DefaultHeaders)
  install(CallLogging)
  install(WebSockets) {
    pingPeriod = Duration.ofMinutes(1)
  }
  routing {
    webSocket {
      logger.debug("receive websocket connection")
      val socket = KtorWebSocket(this)
      initConnection(socket)
    }
  }
}.start(wait = false)

WebSocketクライアントの実装

なんとktorにはWebSocketクライアントの実装がない(ぽい)。ので、P2P的な実装ができない。うへー。そこでTyrusを使うことにした。

compile "org.glassfish.tyrus.bundles:tyrus-standalone-client-jdk:$tyrusVersion"
compile "org.glassfish.tyrus:tyrus-container-grizzly-server:$tyrusVersion"

TyrusはJavaライブラリなのでわりと古めかしいインタフェースを持っている。後述するとおりサーバとクライアントで異なるWebSocketのスタックを使っているのでコネクション全体を管理するために抽象化が必要となる。ktorのWebSocketSessionはChannelでI/Oするので、こちらもChannelを持つ形で実装をした。

nativechain-kotlin/TryusWebSocket.kt at master · sys1yagi/nativechain-kotlin · GitHub

WebSocketセッションの抽象化

WebSocketサーバで接続を受けると、io.ktor.websocket.WebSocketSessionでやりとりを行う。WebSocketクライアントで接続をするとjavax.websocket.Sessionでやりとりを行う。nativechainはブロックの更新があった時などにネットワーク全体にブロードキャストを行う。このためサーバで受けた接続とクライアントとして張った接続の両方を同じように取り扱いたい。nativechainというかjavascriptはWebSocketのサーバクライアントが両方同じように作れるので複雑なことはしてないのだがktorがWebSocketサーバしか持たないのでややこしくなってしまっている。サーバもTyrusにしようか考えたけどなんかアノテーションベースでDIを前提とした設計なので諦めた。

とりあえずWebSocketInterfaceを作って、必要な関数を宣言し、

interface WebSocketInterface {
    fun send(message: String)
    fun receiveChannel(): ReceiveChannel<String>
    fun peer(): String
}

KtorWebSocketTryusWebSocketを実装した。ktorでWebSocketクライアント実装して( ゚д゚)ホスィ…。

動かす

あとは二個サーバ起動してcurlとかでどっちかを叩くとシュッとなる

./gradlew jar
java -jar build/libs/nativechain-kotlin-1.0.jar 3001 6001
java -jar build/libs/nativechain-kotlin-1.0.jar 3002 6002 ws://localhost:6001
curl -H "Content-type:application/json" --data '{"data" : "Some data to the first block"}' http://localhost:3001/mineBlock

おわり

ブロックチェーンの仕組みはなんとなくわかった。ただProof of Workとかはやってないしその他の様々なことにはまだ触れてないので実用的な何かを作るのは当然まだ無理ぽい気配。けど面白かった。分散データベースとヒステリシス署名の組み合わせだというのをどこかで見たけどたしかにその通りだとおもった。改ざんを心配しなくていいことで得られることって色々ありそうだけど具体的にここに使えそうだってのがパッと思いつかないのでもっと学習が必要だ。WebSocket周りはなんか統一したいけどめんどくさいのでまぁいいやという気持ち。

とりあえず今はこれを読んでいる。

で、この辺も読もうと考えている。

Kotlin 1.2.0 言語機能の感想

Kotlin 1.2.0出ましたね。 主に新しい言語機能についてざっくり感想を述べます。

via What's New in Kotlin 1.2

言語機能

アノテーション上でArrayリテラルをサポート

今までは次のようにarrayOfって書いていたけど、

@Singleton
@Component( modules = arrayOf(
    AppModule::class,
    DataModule::class,
    ...
  )
)
interface AppComponent : AndroidInjector<HogeApplication> {

次のように書けるようになる。

@Singleton
@Component( modules = [
    AppModule::class,
    DataModule::class,
    ...
  ]
)
interface AppComponent : AndroidInjector<HogeApplication> {

アノテーションでのみ利用可能なので、大体Dagger周りとかPermissionDispatcherとかRobolectricとか周りで活躍しそう。

lateinit が top-levelプロパティとローカル変数で利用可能に

nullableじゃないけど宣言時はまだ値が決まらない時に使えそう。 とはいえlateinitは初期化されてない状態で触るとクラッシュするので、使い所が難しい。 Activity, Fragment等のライフサイクルがあるコンポーネントのプロパティとして使う事はあるが(やむを得ず)、 top-levelやローカル変数でのユースケースってそんなにないのではないかという気はする。

公式のコード例が次。特殊〜

// A cycle of three nodes:
lateinit var third: Node<Int>

val second = Node(2, next = { third })
val first = Node(1, next = { second })

third = Node(3, next = { first })

同じことをDelegateでもできる。

var third by Delegates.notNull<Node<Int>>()

val second = Node(2, next = { third })
val first = Node(1, next = { second })

third = Node(3, next = { first })

lateinit varが初期化済みかどうか検査できるようになった

こんな感じでプロパティリファレンスで検査できる。

class A {
  lateinit var lateinitVar: String
  fun initialize () {
    if(!this::lateinitVar.isInitialized){
       lateinitVar = "initialized"
    }
  }
}

ローカル変数では使えないので注意っぽい。lateinitは宣言時には値は決まらないけどあとで必ずnon-nullになるので型宣言はnon-nullで頼む :pray: みたいな機能なので、初期化されてなければ何か処理する、みたいな使い方はあんまり本質的ではない気がする。再代入を防ぐのに使う、というのがありそうではあるが...

インライン関数でデフォルト引数が可能に

今までなかったのか〜。便利。

inline fun <E> Iterable<E>.strings(transform: (E) -> String = { it.toString() }) = 
map { transform(it) }

val defaultStrings = listOf(1, 2, 3).strings()
val customStrings = listOf(1, 2, 3).strings { "($it)" } 

明示的なキャストの情報を使った型推論

Android API Level 26でfindViewByIdfindViewById<T>になったので次のコードは動かなくなったんだけど、明示的キャストを型推論に使うから既存コードいじらなくてもいけるぜ的なやつ。助かる。

// findViewById<T>のTがどこにもないのでエラーになってたけど、
// as ButtonのButtonをTとして認識してくれるようになった。
val button = findViewById(R.id.button) as Button

スマートキャストがより賢く

よくわかってないけど、キャストが成功したと解釈できるときはキャスト対象をその後の文脈でキャスト後の型として扱えるようになったみたいな感じ。複雑な検証もいい感じに拾うぜ的なやつ。

val firstChar = (s as? CharSequence)?.firstOrNull()
if (firstChar != null)
  return s.count { it == firstChar } // s: Any is smart cast to CharSequence

val firstItem = (s as? Iterable<*>)?.firstOrNull()
if (firstItem != null)
  return s.count { it == firstItem } // s: Any is smart cast to Iterable<*>

もうひとつ、クロージャでキャプチャする変数を、クロージャより手前でいじる場合のみ、クロージャ内でスマートキャストが利くようになった。

var x: String? = null
if (flag) x = "Yahoo!"

run {
    if (x != null) {
        println(x.length) // x is smart cast to String
    }
}

明示的にこれによさそうみたいなコード例が思いつかないけど多分どっかしら恩恵を受けそうな気はする。

Bound Callable Referenceの省略表記

クラスの関数やプロパティの参照は次のように書く。

val showMessage = Hoge::showMessage
val finish = this::finish

今回、this::::に省略して書けるようになった。

val finish = ::finish

はい。

破壊的変更: sound smart casts after try blocks

https://youtrack.jetbrains.com/issue/KT-17929で報告された不具合で、次のコードはコンパイルが通り実行時にクラッシュする。

var s: String?
s = "Test"
try {
  s = null
} catch (ex: Exception) {}
s.hashCode() // スマートキャストで非null扱いになる

1.2.0ではこのコードのコンパイルは通らなくなる。プロダクトで1.2.0にしてもエラーにならなかったのでこういうコードってほぼ書かないよな普通と思いつつ安全になってよかった。

非推奨: スーパークラスのcopy関数をオーバーライドするdata classes

そもそもdata classがなにかを継承するケースってあんまりないと思うけど、もしもcopy関数を持ったクラスを継承したdata classを作ると腐ってしまうので非推奨になった。

例えば次のような感じ。

open class A {
  fun copy(): A {
    return A()
  }
}
data class B(val a: Int) : A()

次のコードの変数bはBクラスが返って欲しいのだけど、実際にはAが返ってしまう。

val b = B(1).copy()
println(b.javaClass.simpleName) // A

data classで継承したいケースがほとんどない気がするのとさらにスーパークラスがcopy関数を持ってるケースってほとんどなさそうだけど、コンパイラがエラーにしてくれると安心ではある。1.2.0では警告で、1.3.0からコンパイルエラーになるとのこと。

非推奨: enumクラスでのネストしたクラスの宣言

次のコードで、A.Xにアクセスすると死ぬ。

enum class A {
    X {
        val x = 1
        class Nested {
            val y = x // It's allowed and seems to be fine, because a Nested instance can use A.X
        }

        val z = Nested() // There wasn't PUTSTATIC A.X yet, fails with NPE
    }
}

これはXのコンストラクタでval zを初期化するときにNestedのコンストラクタでX.xにアクセスするから。nest classはデフォルトでは静的なクラスになるので、まだコンストラクタが走りきってないXに対してアクセスして死んでいる。inner classで宣言するとX.this.xでアクセスしにいくので死なない。1.2.0では警告で、1.3.0からコンパイルエラーになるとのこと。

こんなコード書かねーよ〜〜〜〜とは思う。

非推奨: 名前付き引数で、varargを単一の値で渡す

次のような関数があったとする。

fun print(vararg values: Int) {
  values.forEach {
    println(it)
  }
}

次のような呼び出し方に警告が出るようになった。1.3.0でエラー。アノテーションの配列リテラルのスタイルとの一貫性のためらしい。

print(values = 5)  // warning

次のように書くようになる。

print(values = *intArrayOf(5))

あんまり感想がない。

Deprecation: Throwableを継承するジェネリック型の内部クラス

なんか型安全じゃなくなるらしい。よくわからないけど大変そう。こちらも1.2.0警告、1.3.0エラー。

Deprecation: valプロパティでバッキングフィールドを更新する

こういうのはダメよと。

val a: Int = 0
  get() {
    field++
    return field
  }

valなのに値変わっとるやんけ!と。1.2.0警告、1.3.0エラー。

まとめ

コルーチンのexperimental外れるのまだかな。

Kotlin in Action はとてもいいのでとりあえずAndroidエンジニアは全員読むのがいいと思います

献本頂きました。ありがとうございます。

Kotlin in Actionとは

Kotlin in ActionはJetBrains社でKotlinの開発に参加しているDmitry Jemerov氏とSvetlana Isakova氏がJava開発者向けに書いたKotlin1.0の解説書です。

Kotlinが生まれた経緯、Kotlinの言語機能に関する網羅的な解説、高度な利用法の解説がどっさり400Pくらいに渡って書かれています。

原著が2017年2月発売なので、ちょうどKotlin 1.1がリリースされる直前です。ですのでKotlin 1.1以降に関する内容は含まれていません。 1.0と1.1や1.2はそこまで差分はないので、本書で十分Kotlinを学べると思います。

Kotlin in Actionのここがいい

本書のいいところはKotlinのリード設計者のAndrey Breslav氏が寄せた前書きと、序文(これはだれが書いたか明記されてないので不明)に詰まってるなぁと感じます。 以下引用します。

前書き

2010年の春、私が初めてJetBrainsを訪れたときには、世界はもう汎用目的のプログラミング言語を必要としていないだろうということを全く疑っていませんでした。 既存のJVM言語は十分優れていると考えており、それなのに誰かが責任を持って新しい言語を作るというのでしょうか?しかし、1時間ほど大規模なコードベースにおける生産上の課題について議論したあと、私はそうではないと確信しました。そして、のちにKotlinの一部になる最初のアイデアがホワイトボードに書かれました。私はこの言語の設計をリードするため、そしてコンパイラの開発に取り組むために、すぐにJetBrainsに入社しました。・・・

序文

Kotlinのアイデアは、2010年にJetBrainsで考案されました。その頃のJetBrainsは、JavaC#JavaScriptPythonRubyPHPなどの多くの言語に対応した開発ツールのベンダーとして地位を確立していました。「IntelliJ IDEA」(我々の主力製品であるJavaIDE)は、GroovyやScalaプラグインも含んでいます。 このような多種多様な言語のツールを開発した経験は、言語設計の空間の全体の見方とユニークな理解を我々に与えてくれました。とはいえ、IntelliJ IDEAを含むIntelliJプラットフォームベースのIDEは、まだJavaで開発されていました。モダンで強力で素早い進化をする言語であるC#で開発をしている.NETチームの同僚に対して、若干のうらやましさがありました。しかし、我々はJavaの代わりに使用できる言語を知りませんでした。 そのような言語の要件はなんだったでしょうか?まず最初の、そして最もわかりやすい要件は、静的型付けです。発狂せずに長年に渡って数百万行のコードベースを開発するには、これ以外の方法は考えられません。第二に、既存のJavaコードとの完全な互換性が必要でした。JavaのコードベースはJetBrainsにとって非常に貴重な資産であり、これを失うこと、あるいは相互運用性の困難さによって価値を下げることは回避しなければなりません。第三に、道具としての品質の面で、いかなる妥協も受け入れたくありませんでした。開発者の生産性は、JetBrainsという会社にとって最も重要な価値を持っています。そして、優れた道具は高い生産性の達成に不可欠です。最後に学びやすく論理的に考えやすい言語が必要でした。我々の会社にとっていまだに満たされていないニーズがあるならば、それは他社でも同様でしょう。この問題の解決策が、JetBrains以外でも多くのユーザーを満足させるはずです。この点を考慮して、新しい言語であるKotlinの開発プロジェクトに踏み切る決心をしました。・・・

現実の課題を解決するために言語を作っていることが伝わってきます。 Kotlinを気に入った人たちが口にする「簡潔に書けて強力で最高」とか「導入も簡単で相互運用も難しくなくて良い」とか、そもそもそれを狙ってたんやで!という事がわかります。完全に計画通りですね。そんな本書はどこを食べてもおいしく役立ちます。

感想

これからも便利な道具として色んなアイデアを取り込みながら進化していくんだろうな〜と思うと楽しみですね。 1人1冊買おう。読もう。書こう。Have a nice Kotlin!

Android Architecture ComponentsのViewModelとDialogFragment

Android Architecture ComponentsのViewModelとHolderFragmentとActivity-Fragment間通信と。 - visible trueを書いたあと、

ということで試したら行けました。

Source

https://github.com/sys1yagi/aac-viewmodel-with/tree/master/fragment-dialog

MainViewModel

コールバック的な値をLiveDataで定義する。今回はUnitにしてるけどなんでもよさそう。

class MainViewModel : ViewModel() {
    val dialogOk = MutableLiveData<Unit>()
    val dialogCancel = MutableLiveData<Unit>()
}

HelloDialog

DialogでMainViewModelを取り出して対応するアクションの値を更新する。

class HelloDialog : DialogFragment() {
    companion object {
        fun newInstance() = HelloDialog()
    }
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val builder = AlertDialog.Builder(activity)
        val viewModel = ViewModelProviders.of(activity).get(MainViewModel::class.java)
        builder.setMessage("Hello")
                .setPositiveButton("Yes", { _, _ ->
                    viewModel.dialogOk.value = Unit
                })
                .setNegativeButton("Cancel", { _, _ ->
                    viewModel.dialogCancel.value = Unit
                })
        return builder.create()
    }
}

MainActivity

DialogのためのLiveDataはonCreateでobserveしないと、process killレベルのActivity破棄が起こった時にはずれてしまうので注意。

class MainActivity : AppCompatActivity(), LifecycleRegistryOwner {
    override fun getLifecycle() = registry
    val registry = LifecycleRegistry(this)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        findViewById<Button>(R.id.button).setOnClickListener {
            HelloDialog.newInstance().show(supportFragmentManager, "hello")
        }

        val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
        viewModel.dialogOk.observe(this, Observer {
            Toast.makeText(this, "OK", Toast.LENGTH_SHORT).show()
        })
        viewModel.dialogCancel.observe(this, Observer {
            Toast.makeText(this, "Cancel", Toast.LENGTH_SHORT).show()
        })
    }
}

シュッ

f:id:sys1yagi:20170824125211p:plain

雑感

悪くない。

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

雑感

へーって思った