2017年11月19日日曜日

moshi で独自の Adapter は KotlinJsonAdapterFactory より先に add すべし

Retrofit と一緒にmoshi を使っています。
moshi には Kotlin Support 機能があり、別途 moshi-kotlin を追加して、 implementation 'com.squareup.moshi:moshi-kotlin:1.5.0' KotlinJsonAdapterFactory を MoshiBuilder に add します。 val moshi = Moshi.Builder() .add(KotlinJsonAdapterFactory()) .build()

また、moshi には Custom Type Adapters 機能があり、JSON の値とオブジェクトとの変換をカスタマイズすることができます。

この CustomAdapter および、CustomAdapter の Factory は KotlinJsonAdapterFactory より先に add しなければいけません (Kotlin Support のところに書いてあるのに気づかずはまってしまった...)。 Retrofit.Builder() .addConverterFactory(MoshiConverterFactory.create( Moshi.Builder() .add(MyCustomAdapter()) // KotlinJsonAdapterFactory より先なので OK .add(KotlinJsonAdapterFactory()) .build())) ... Adapter を生成するときに、追加された順番で Factory に Adapter が生成できるか問い合わせていくため、KotlinJsonAdapterFactory を先にすると CustomAdapter に来ないので気をつけましょう。


2017年11月12日日曜日

Android Things をやってみよう - Peripheral 操作編

Peripheral の操作は PeripheralManagerService を使って行います。

GPIO の名前の一覧は PeripheralManagerService.getGpioList() で取得できます。 val pioService = PeripheralManagerService() for (name in pioService.gpioList) { Log.d(TAG, "gpio : $name") } 実行結果 gpio : GPIO_10 gpio : GPIO_128 gpio : GPIO_172 gpio : GPIO_173 gpio : GPIO_174 gpio : GPIO_175 gpio : GPIO_32 gpio : GPIO_33 gpio : GPIO_34 gpio : GPIO_35 gpio : GPIO_37 gpio : GPIO_39

PeripheralManagerService.openGpio() で GPIO を開きます。戻り値は Gpio です。引数には getGpioList() で取得した名前を指定します。 val ledPin = pioService.openGpio(name)
GPIO は General-purpose input/output のことで、GPIO pin はソフトウェアからコントロールできる集積回路上の物理的ピンです。GPIO pin は入力として使って電圧値を読んだり、出力として使って電圧値を変えたりできます。

扱えるのは論理値(true / false)のみで、low value(ピンがグラウンドと同じ電圧値)と high value(ピンが IOREF と同じ電圧値)が論理値にマッピングされます。論理値と high/low のマッピング(true が high/low どちらに対応するか)は設定で変えることができます。

GPIO pin を開くということは、システム全体でこのピンに対するオーナシップを取るということです。これにより close() が呼ばれるまで他から GPIO を開いたりアクセスしたりするのを防ぎます。close() を呼ぶのを忘れると、同じプロセス・アプリに関係なくどこからも GPIO が使えなくなるので注意が必要です。


論理値と high/low のマッピングは Gpio.setActiveType() で指定します。
true に high を対応させるときは Gpio.ACTIVE_HIGH を、true に low を対応させるときは Gpio.ACTIVE_LOW を指定します。 ledPin.setActiveType(Gpio.ACTIVE_HIGH)
ピンの方向(入力 or 出力)は Gpio.setDirection() で指定します。
ピンを入力として使うときは Gpio.Gpio.DIRECTION_IN を指定します。出力として使うときは、初期状態を high にするときは Gpio.DIRECTION_OUT_INITIALLY_HIGH を初期状態を low にするときは Gpio.DIRECTION_OUT_INITIALLY_LOW を指定します。 ledPin.setDirection(Gpio.DIRECTION_OUT_INITIALLY_LOW)
値が変わったときのトリガーのタイミングを Gpio.setEdgeTriggerType() で指定することができます。
ピンを出力として使うときはトリガーの必要がないので Gpio.EDGE_NONE を指定します。
ピンを入力として使う場合、立ち上がり時(low から high になったとき)だけトリガーさせるなら Gpio.EDGE_RISING を指定します。立ち下がり時(high から low になったとき)だけトリガーさせるなら Gpio.EDGE_FALLING を指定します。立ち上がり、立ち下がり両方トリガーさせるなら Gpio.EDGE_BOTH を指定します。 ledPin.setEdgeTriggerType(Gpio.EDGE_NONE)
現在の値を変更するときは Gpio.setValue() を使います。このメソッドはピンが出力として設定されているときだけ使うことができます。 ledPin.value = isChecked
現在の値を読むときは Gpio.getValue() を使います。このメソッドはピンが入力として設定されているときだけ使うことができます。 val value = ledPin.value
ピンの値が変わった時にコールバックを受け取るには Gpio.registerGpioCallback()GpioCallback を登録します。登録した GpioCallback は Gpio.unregisterGpioCallback() で登録解除します。 private val callback: GpioCallback = object : GpioCallback() { override fun onGpioEdge(gpio: Gpio?): Boolean { Log.d(TAG, "onGpioEdge: ${gpio?.name}, ${gpio?.value}") return true } override fun onGpioError(gpio: Gpio?, error: Int) { super.onGpioError(gpio, error) /** error : [android.system.OsConstants] */ Log.d(TAG, "onGpioError: ${gpio?.name}, $error") } } override fun onCreate(savedInstanceState: Bundle?) { ... ledPin.registerGpioCallback(callback) } override fun onDestroy() { super.onDestroy() try { ledPin.unregisterGpioCallback(callback) ledPin.close() } catch (e: IOException) { Log.e(TAG, "Error closing GPIO", e) } }

Android Things をやってみよう - プロジェクト作成編



form factors に Android Things を選ぶ









以下のような build.gradle が作成される apply plugin: 'com.android.application' ... android { ... } dependencies { ... compileOnly 'com.google.android.things:androidthings:+' }


2017年11月11日土曜日

Android Things をやってみよう - セットアップ編

https://developer.android.com/things/index.html

DroidCon SF で Android Things のハンズオンに参加し、Developer Kit をもらったのでいろいろやってみています。

事前準備

- Android Studio 3.0 をインストールする


- システム Image を用意する 1. Android Things Console に行ってサインイン(初回時に Terms of Service の同意が必要)する

2. Create product ボタンをクリックし、プロダクト名と詳細を入力し、対応する SOM type (もらった kit だと NXP Pico i.MX7D) を選択して Create をクリックする



3. 作成されたプロダクトを選択する



4. FACTORY IMAGES タブを選択する

5. Bundles で Empty Bundle を選択する



6. Android Things versions で最新のバージョンを選択し、Click Create Build Configuration をクリックする



7. Build が完了するまで待って、Build configuration list に追加されたアイテムをダウンロードし、解凍する




- Flashing the image 1. ボードと USB ケーブルで接続する (NXP i.MX7D なら USB-C cable が必要)

2. Android SDK の platform-tools/ にパスを通す

3. macOS が High Sierra (10.13) の場合、別の fastboot バイナリをダウンロードする

macOS が High Sierra (10.13) の場合、platform-tools/fastboot に既知の Issue があります。
https://issuetracker.google.com/issues/64292422
#comment8 に添付されているバイナリが使えます。

4. 以下のコマンドを実行してボードが Fastboot モードで起動していることを確認する $ fastboot devices Fastboot モードで起動していない場合は以下のコマンドでデバイスを再起動する $ adb reboot bootloader

5. ダウンロード&解凍した Image 内の flash-all.sh または flash-all.bat を実行する

6. 成功したら再起動するので、以下のコマンドで Android が動いていることを確認する $ adb devices

- WiFi に接続する $ adb shell am startservice -n com.google.wifisetup/.WifiSetupService -a WifiSetupService.Connect -e ssid [network_ssid] -e passphrase [network_pass]

サンプルで遊ぶ

https://developer.android.com/things/sdk/samples.html

ハンズオンでは をやりました。他にもサンプルたくさんあります。

ボードと USB で接続して、普通に Android Studio で開いて実行すれば OK です。別のものをインストールする前に今インストールされているものをアンインストールする必要があるので、忘れないように注意です


コードラボで学ぶ

https://codelabs.developers.google.com/io2017?cat=IoT


2017年11月6日月曜日

Kotlin for Android What's New メモ

Update on Kotlin for Android : Android Developers Blog

Support Library 27 から API に nullability annotations をつけはじめた。

Android Kotlin Guides を公開。Kotlin for Android の style と interop のためのガイド。
  • 1. Style guide - Kotlin for Android でコーディングするときの Google が推奨するルールと coding standards の詳細。命名規約、フォーマット、ソースの構成などについて言及している。
  • 2. Interop guide - Java および Kotlin で API を作成するときに、もう一方の言語で慣例的に利用できるようにするためのルールセットを提供している。
Android samples のいくつかを Kotlin に porting。official documentation に Kotlin を追加することも開始。

Android Kotlin page

Kotlin on Android FAQ

Resources to Learn Kotlin

2017年11月4日土曜日

KotlinConf 2017 に参加してきました。

KotlinConf 2017 | Kotlin Programming Conference in SF, USA

KotlinConf は Kotlin のカンファレンスです。今回が第1回目で、1200のチケットは完売したそうです。



1日目

キーノート前。人いっぱいです。



Coroutines



experimental は unstable ということではない。まだ API を変更する自由度を持っておきたいので experimental にしている。coroutines は production ready。Kotlin 1.3 で experimental が外れる予定。





Ktor

読み方はケイター、100% Kotlin の web framework
http://ktor.io/



Kotlin/JS





Android

Play Store で Kotlin を使っているアプリの数が 2.5倍に(いつからかは不明)



Android Studio Project の 17% が Kotlin を使っている(具体的な話は不明)



Android Studio
- Bundled Kotlin Plugin
- Kotlin Lint Support (in-IDE)
- Kotlin Templates for Projects and Activities



Android Support library 27.0.0 Kotlin Annotations



Android Kotlin Guides でました。
android.github.io/kotlin-guides/

Kotlin の Style Guide はこちら。
https://android.github.io/kotlin-guides/style.html

Java 呼ばれることを意識した Kotlin の書き方、Kotlin から呼ばれることを意識した Java の書き方はこちら。
https://android.github.io/kotlin-guides/interop.html



Android Kotlin Docs & Samples
https://developer.android.com/samples/index.html?language=kotlin



iOS

Kotlin で iOS きたー。





Multiplatform Projects

expect と actual 修飾子で共通の部分の Kotlin コードから .class と .js を生成。





Server, iOS, Android, Browser 全部 Kotlin!すごい世界だ。



Summary / Today

- Kotlin 1.2 RC available (keynote recap at blog.jetbrains.com)
- Multiplatform Projects (KotlinConf アプリが参考になるよ https://github.com/jetbrains/kotlinconf-app)
- Common modules
- expect/actual fun/class
- More in the blog post



Summary / Future

- Multiplatform libraries
- I/O, networking, serialization, dates...
- Kotlin/Native : compiler and IDE



Gradle Kotlin DSL

gradle の kotlin DSL よさげだった
GitHub - gradle/kotlin-dsl: Kotlin language support for Gradle build scripts
https://github.com/gradle/kotlin-dsl
↑まだ情報古そう

2日目キーノート

話は面白かった。あんまり Kotlin 関係なかった。





Kotlin/Native Demo

Swift の interop は in progress。現状は Objective-C の interop。CLion と XCode でデモしてた。





Kotlin Types

個人的にこのセッションが一番よかったです。おすすめ。





その他

Coding Conventions : the current coding style for the Kotlin language.

Udacity に Kotlin コースくるらしい



お昼ご飯おいしかった。でも初日のパーティのご飯はいまいちで、結局外に食べに行った。

おやつの Kotlin カップケーキかわいかった。すごく甘くてアメリカを感じた。



会場がちょっと寒いからかすぐコーヒーなくってしまってた。

1日目の最後、Party Keynote。なんかずっと手品してた。



1日目夜。中心部のビルの夜景がきれい。



2日目昼ごはん

2017年9月22日金曜日

Play Billing Library 1.0 がリリースされました

2017年9月19日に Play Billing Library の 1.0 がリリースされました(Android Developers Blog : Google Play Billing Library 1.0 released)。



Play Billing Library 1.0 では自動で com.android.vending.BILLING Permission が追加されるので手動で追加する必要はありません。

ライブラリの設定 compile 'com.android.billingclient:billing:1.0' or implementation 'com.android.billingclient:billing:1.0' BillingClient というクラスを利用します。 val billingClient : BillingClient = BillingClient.newBuilder(context) .setListener(this) // PurchasesUpdatedListener .build() Builder パターンになっていますが、PurchasesUpdatedListener を設定しないと build() を呼んだときに IllegalArgumentException が起こります。
IabHelper にあった enableDebugLogging() に相当するメソッドは BillingClient にはありません。


開始と終了

IabHelper の startSetup() に相当するのが BillingClient.startConnection() です。 startConnection() で開始して endConnection() で終了します。 private lateinit var billingClient: BillingClient private var isBillingClientConnected: Boolean = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) billingClient = BillingClient.newBuilder(this) .setListener(this) .build() startServiceConnection(null) } private fun startServiceConnection(executeOnSuccess: Runnable?) { billingClient.startConnection(object : BillingClientStateListener { override fun onBillingSetupFinished(@BillingClient.BillingResponse response: Int) { when (response) { OK -> { isBillingClientConnected = true executeOnSuccess?.run() } else -> { ... } } } override fun onBillingServiceDisconnected() { isBillingClientConnected = false } }) } override fun onDestroy() { billingClient.endConnection() super.onDestroy() }

購入

購入処理は BillingClient の launchBillingFlow() で開始します。 val params = BillingFlowParams.newBuilder() .setType(BillingClient.SkuType.INAPP) .setSku(SKU_INAPP) .build() val responseCode = billingClient.launchBillingFlow(activity, params) val params = BillingFlowParams.newBuilder() .setType(BillingClient.SkuType.SUBS) .setSku(SKU_SUBS) .addOldSku(SKU_OLD_SUBS) // replace するときは必須 .setReplaceSkusProration(true) // Optional : デフォルトは false .setAccountId(hashedUserAccountId) // Optional : 難読化されたユーザー別の文字列 .setVrPurchaseFlow(false) // Optional : デフォルトは false .build() val responseCode = billingClient.launchBillingFlow(activity, params) BillingFlowParams の各設定値については BillingFlowParams.Builder のドキュメントをよく読みましょう。

購入処理の結果は PurchasesUpdatedListener の onPurchasesUpdated() で通知されます。

launchBillingFlow() の戻り値と PurchasesUpdatedListener.onPurchasesUpdated() の第1引数は @BillingClient.BillingResponse です。 override fun onPurchasesUpdated(@BillingClient.BillingResponse responseCode: Int, purchases: List<Purchase>?) { when (responseCode) { OK -> { if (purchases != null) { purchases.forEach { handlePurchase(it) } } else { ... } } ITEM_ALREADY_OWNED -> { ... } USER_CANCELED -> { // do nothing } ... else -> { ... } } } 第2引数は最新の購入(Purchase)リストです。

IabHelper のときは com.android.vending.billing.PURCHASES_UPDATED を受け取るために IabBroadcastReceiver を用意していましたが、その必要はなくなりました。代わりに、アプリからの購入だけでなく Play Store で開始された購入のときも onPurchasesUpdated() に通知されます。


購入済みアイテムの問い合わせ

BillingClient の queryPurchases() で行います。 このメソッドは Google Play Store アプリが提供するキャッシュから結果(PurchasesResult)を受け取ります。ネットワーク処理は開始されません。 val result : PurchasesResult = billingClient.queryPurchases(BillingClient.SkuType.INAPP)

購入可能なアイテムの問い合わせ

BillingClient の querySkuDetailsAsync() で行います。 問い合わせるアイテムは SkuDetailsParams で設定します。 fun querySkuDetailsAsync() { val executeOnConnectedService = Runnable { val params = SkuDetailsParams.newBuilder() .setType(BillingClient.SkuType.INAPP) .setSkusList(listOf("gas", "premium")) .build() billingClient.querySkuDetailsAsync(params) { responseCode, skuDetailsList -> when (responseCode) { OK -> { skuDetailsList?.forEach { ... } } else -> { ... } } } } if (isBillingClientConnected) { executeOnConnectedService.run() } else { startServiceConnection(executeOnConnectedService) } }

注意

2017年9月22日時点では、https://codelabs.developers.google.com/codelabs/play-billing-codelab は dp-1 の内容なので古いです(Builder インスタンスの生成方法や購入可能なアイテムの問い合わせのAPIが 1.0 で変わっています)。https://github.com/googlecodelabs/play-billing-codelab は新しくなっています。

追記: https://codelabs.developers.google.com/codelabs/play-billing-codelab も更新されました。



2017年9月18日月曜日

Kotlin メモ : Class Delegation を使って Adapter の処理を委譲する

ベースクラスの異なる 2つの Adapter があります

1. ArrayAdapter を継承した FavoriteAdapter
  • FavoriteAdapter は任意の型のデータを取りうる
  • その型のデータに対する date(T), balance(T) の実装が必要
abstract class FavoriteAdapter<T>(context: Context, objects: List<T>) : ArrayAdapter<T>(context, 0, objects) { abstract fun date(data: T): String abstract fun balance(data: T): Int private val inflater = LayoutInflater.from(context) override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { val view: View = convertView ?: ItemViewHolder .create(inflater, parent) .also { it.view.tag = it } .view getItem(position)?.let { (view.tag as ItemViewHolder).bind(date(it), balance(it)) } return view } } 2. CursorAdapter を継承した HistoryAdapter
  • CursorAdapter は任意の型のデータを取りうる
  • その型のデータに対する date(T), balance(T) の実装が必要
  • BaseData からその型のデータに変換するメソッドの実装が必要
abstract class HistoryAdapter<T>(context: Context) : CursorAdapter(context, null, 0) { abstract fun date(data: T): String abstract fun balance(data: T): Int abstract fun create(data: BaseData): T private val inflater: LayoutInflater = LayoutInflater.from(context) override fun newView(context: Context, c: Cursor, parent: ViewGroup) { return ItemViewHolder.create(inflater, parent).also { it.view.tag = it }.view } override fun bindView(view: View, context: Context, c: Cursor) { val baseData = convert(c) val data = create(baseData) (view.tag as ItemViewHolder).bind(date(data), balance(data)) } private fun convert(c: Cursor): BaseData { ... } }

Delegation なし

データとして MyData をとる Adapter を用意してみましょう。 class MyDataAdapter(context: Context, objects: List<MyData>) : FavoriteAdapter<MyData>(context, objects) { override fun date(data: MyData) = DateFormat.format(context.getString(R.string.format_date), data.getDate()).toString() override fun balance(data: MyData) = data.getBalance() } class MyDataAdapter(context: Context) : HistoryAdapter<MyData>(context) { override fun date(data: MyData) = DateFormat.format(context.getString(R.string.format_date), data.getDate()).toString() override fun balance(data: MyData) = data.getBalance() override fun create(data: BaseData): MyData = MyData(data) } val adapter: FavoriteAdapter<*> = MyDataAdapter(context, list) val adapter: HistoryAdapter<*> = MyDataAdapter(context) FavoriteAdapter を継承した MyDataAdapter と HistoryAdapter を継承した MyDataAdapter をそれぞれ用意しました。しかし2つの Adapter の処理はほぼ同じなので、1つのクラスにまとめるのがよいでしょう。

そこで、まずは通常の Delegation パターンで実装してみます。

Delegation パターン

Adapter<T> インターフェースを用意し、FavoriteAdapter と HistoryAdapter に abstract で定義していたメソッドを Adapter のメソッドに置き換えます。 interface Adapter<T> { fun date(data: T): String fun balance(data: T): Int fun create(data: BaseData): T } class FavoriteAdapter<T>(context: Context, objects: List<T>, private val adapter: Adapter<T>) : ArrayAdapter<T>(context, 0, objects) { fun date(data: T): String = adapter.date(data) fun balance(data: T): Int = adapter.balance(data) ... } class HistoryAdapter<T>(val context: Context, private val adapter: Adapter<T>) : CursorAdapter(context, null, 0) { fun date(data: T): String = adapter.date(data) fun balance(data: T): Int = adapter.balance(data) fun create(data: BaseData): T = adapter.create(data) ... } Adapter を継承した MyDataAdapter を用意します。 class MyDataAdapter(private val context: Context) : Adapter<MyData> { override fun date(data: MyData) = DateFormat.format(context.getString(R.string.format_date3), data.getDate()).toString() override fun balance(data: MyData) = data.getBalance() override fun create(data: BaseData): MyData = MyData(data) } val adapter: FavoriteAdapter<*> = FavoriteAdapter(context, list, MyDataAdapter(context)) val adapter: HistoryAdapter<*> = HistoryAdapter(context, MyDataAdapter(context)) FavoriteAdapter と HistoryAdapter を継承しなくてよくなりました。

一方、FavoriteAdapter と HistoryAdapter の date() や balance() メソッドでは、Adapter のメソッドをそのまま呼び出しているだけです。
Class Delegation を使うと、このような明示的な記述をしなくてよくなります。

Class Delegation

FavoriteAdapter と HistoryAdapter も Adapter<T> を実装し、by を使ってコンストラクタでもらった adapter に処理を委譲します。 class FavoriteAdapter<T>(context: Context, objects: List<T>, private val adapter: Adapter<T>) : ArrayAdapter<T>(context, 0, objects), Adapter<T> by adapter { ... } class HistoryAdapter<T>(val context: Context, private val adapter: Adapter<T>) : CursorAdapter(context, null, 0), Adapter<T> by adapter { ... } Class Delegation により、FavoriteAdapter と HistoryAdapter では date() や balance() の明示的な記述をしなくてよくなりました。


2017年9月15日金曜日

自前アプリを Java から Kotlin に書き換えてみた。

Kotlin の練習のために Suica Reader のコードを Kotlin で書き換えてみました。

最終的にコード量は2割ちょっと減りました。



Java コードを全て一括で Kotlin に自動変換することも可能ですが、個別のクラスを順番に Kotlin 化していくことにしました。
どのように変換されるかを確認し、さらにより Kotlin らしい記述に書き換えるには、全部一括でやるのは合わないと思ったからです。


書き換え順

1. enum

ほとんど自動変換で済みました。 まだ他の Java コードが残っているので、companion object の関数に @JvmStatic をつけるのと、Companion が差し込まれた Java 側のコードを元に戻すことを追加でやりました。


2. interface

これもほとんど自動変換で済みました。


3. Parcelable

できるものは data class にしました。
いつも CREATOR に @JvmField をつけ忘れそうになる... Parcelable については「Kotlin メモ : Parcelable」に書きました。

Android Kotlin Extensions の Parcelable サポートの experimental が取れたらそっちに移行したいです。


4. ロジック部分

途中 data class ではまって interface にすることで解決しました。
詳しくは 「Kotlin メモ : data class を継承できないので interface で実現した話」に書いてあります。


5. データ保存部分

SharedPreferences と ContentProvider を使っています。
SharedPreferences についてはドメインの方に interface を定義してあり、get と set 用にメソッドが分かれていたのを、フィールド1つにして実装側はカスタムアクセサを用意するように変えました。詳しくは「SharedPreferences を使ったデータアクセス部分を Kotlin のカスタムアクセサ で実装する」に書いてあります。
ContentProvider については特にないです。Cursor 部分で use を使ったくらいかな。


6. UI部分

ButterKnife を使っていたのですが、Android Kotlin Extensions に切り替えました。
Nullable か NonNull か確認するためにプラットフォームのコードを見にいくのがめんどかったです。
(Java 側には @Nullable, @NonNull アノテーションつけよう!)

自動変換後 ? と !! をちまちま直します。適宜 lateinit とか使います。



よかった点

テストコードをがっつり書いてあるので、安心して移行処理ができました。


反省点

先にテストコードを Kotlin 化してもよかったかもしれない。
でも Java のテストコードいじらないほうが移行後の Kotlin に自信(安心?)持てるかも。


まとめ
  • NonNull, Nullable のコンパイル時チェックの安心感ぱない。
  • 標準ライブラリが充実していて素晴らしい。
  • 一から新しいアプリを Kotlin で書き始めるより既存のアプリを Kotlin に変換するほうが早くかけるようになりそう。

2017年9月14日木曜日

Kotlin メモ : use

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.io/use.html

Java public int queryCount(Context context, Uri uri) { final Cursor c = context.getContentResolver() .query(uri, null, null, null, null); if (c == null) { return 0; } final int count = c.getCount(); c.close(); return count; } Kotlin : 自動変換直後 fun queryCount(context: Context, uri: Uri): Int { val c = context.contentResolver .query(uri, null, null, null, null) ?: return 0 val count = c.count c.close() return count } Kotlin : use 使用 fun queryCount(context: Context, uri: Uri): Int { val c = context.contentResolver .query(uri, null, null, null, null) ?: return 0 c.use { return it.count } }

Kotlin メモ : CustomView は @JvmOverloads でコンストラクタ部分を短くするのがよさげ

class CustomView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attrs, defStyleAttr) { init { ... } }

2017年9月10日日曜日

Android Studio 3.0 で compileSdkVersion を 26 にすると findViewById で型の指定が必要になる

環境
  • Android Studio 3.0 Beta 5 (Android Studio 2系では試してません)
  • Kotlin
compileSdkVersion を 25 から 26 にしたところ、以下のように findViewById で型の指定を求められるようになりました。



以下のようにすれば ok です。 as TextView もいらなくなります。



追記: 変数の型を明示する方法でも ok です。



2017年9月1日金曜日

SharedPreferences を使ったデータアクセス部分を Kotlin のカスタムアクセサ で実装する

ユーザーの血液型を保存したいとします。

SharedPreferences に保存するとして、保存・読み出しでキーを間違えたり、対象の SharedPreferences を間違えたりしないためには、SharedPreferences への保存と読み出しを行うためのクラスを用意するとよいです。

例えば次のような Utils クラスを用意したとしましょう。 public class ProfileSettingUtils { private static final String PREF_KEY_BLOOD_TYPE = "blood_type"; private static SharedPreferences getPref(@NonNull Context context) { return PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); } @Nullable public static String getBloodType(@NonNull Context context) { return getPref(context).getString(PREF_KEY_BLOOD_TYPE, null); } public static void setBloodType(@NonNull Context context, @Nullable String bloodType) { getPref(context) .edit() .putString(PREF_KEY_BLOOD_TYPE, bloodType) .apply(); } } この Utils クラスで保存・読み出しを行えば、キーを間違えたり対象の SharedPreferences を間違えたりはしません。
しかし、"a" がA型を意味するなど、利用側が返される文字列の意味を知っている必要がありますし、"-" など意図しない文字列も保存できてしまいます。

血液型のような取り得る値が決まっているものは enum で定義して、保存・読み出し部分も enum でやりとりするべきです。 public enum BloodType { A("a"), B("b"), O("o"), AB("ab"); @NonNull public final String value; BloodType(@NonNull String value) { this.value = value; } @Nullable public static BloodType from(@Nullable String value) { if (value != null) { for (BloodType bloodType : values()) { if (bloodType.value.equals(value)) { return bloodType; } } } return null; } } public class ProfileSettingUtils { private static final String PREF_KEY_BLOOD_TYPE = "blood_type"; private static SharedPreferences getPref(@NonNull Context context) { return PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); } @Nullable public static BloodType getBloodType(@NonNull Context context) { return BloodType.from(getPref(context).getString(PREF_KEY_BLOOD_TYPE, null)); } public static void setBloodType(@NonNull Context context, @Nullable BloodType bloodType) { getPref(context) .edit() .putString(PREF_KEY_BLOOD_TYPE, bloodType != null ? bloodType.value : null) .apply(); } } Robolectric を使えばテスト時に SharedPreferences の動きをモック化できますが、テストのセットアップとして SharedPreferences に値をセットするよりは、BloodType を返す部分をモック化できたほうが柔軟性があります。

では Utils クラスをやめてみましょう。 public class ProfileSetting { private static final String PREF_KEY_BLOOD_TYPE = "blood_type"; @NonNull private final SharedPreferences pref; public ProfileSetting(@NonNull Context context) { pref = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); } @Nullable public BloodType getBloodType() { return BloodType.from(pref.getString(PREF_KEY_BLOOD_TYPE, null)); } public void setBloodType(@Nullable BloodType bloodType) { pref .edit() .putString(PREF_KEY_BLOOD_TYPE, bloodType != null ? bloodType.value : null) .apply(); } } このクラスを使って、読み出した BloodType を判断・加工するロジック部分があるとします。
このままだとロジック部分がデータアクセス部分に依存しています。
そこで、依存関係逆転の原則(DIP)を適用してロジックが抽象に依存できるように interface を用意します。 public interface Profile { @Nullable BloodType getBloodType(); void setBloodType(@Nullable BloodType bloodType); } public class ProfileSetting implements Profile { private static final String PREF_KEY_BLOOD_TYPE = "blood_type"; @NonNull private final SharedPreferences pref; public ProfileSetting(@NonNull Context context) { pref = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); } @Override @Nullable public BloodType getBloodType() { return BloodType.from(pref.getString(PREF_KEY_BLOOD_TYPE, null)); } @Override public void setBloodType(@Nullable BloodType bloodType) { pref .edit() .putString(PREF_KEY_BLOOD_TYPE, bloodType != null ? bloodType.value : null) .apply(); } } ロジック部分は ProfileSetting ではなく interface の Profile を外部から渡してもらうようにします。

さて、これを Kotlin 化してみましょう。 enum class BloodType(val value: String) { A("a"), B("b"), O("o"), AB("ab"); companion object { fun from(value: String?): BloodType? = value?.let { values().firstOrNull { it.value == value } } } } interface Profile { var bloodType: BloodType? } class ProfileSetting(context: Context) : Profile { companion object { private const val PREF_KEY_BLOOD_TYPE = "blood_type" } private val pref: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.applicationContext) override var bloodType: BloodType? get() = BloodType.from(pref.getString(PREF_KEY_BLOOD_TYPE, null)) set(bloodType) = pref.edit().putString(PREF_KEY_BLOOD_TYPE, bloodType?.value).apply() } Kotlin ではプロパティの定義にカスタムアクセサを書けるので、同じキーに対する保存と読み込みを一箇所に書けて対応がわかりやすくなりますね。


2017年8月24日木曜日

Kotlin メモ : forEachIndexed

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/for-each-indexed.html


Java int pos = 1; for (Item item : items) { log(pos, item); pos++; } Kotlin 変換直後 var pos = 1 for (item in items) { log(pos, item) pos++ } forEachIndexed 使用 items.forEachIndexed { index, item -> log(index + 1, item)}


2017年8月22日火曜日

Kotlin メモ : mapNotNull

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.collections/map-not-null.html


val list = (0 until adapter.count) .map { adapter.item(it) } .filterNotNull() .toList() mapNotNull 使用 val list = (0 until adapter.count) .mapNotNull { adapter.item(it) } .toList()

Kotlin メモ : until

https://kotlinlang.org/docs/reference/ranges.html
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.ranges/until.html


val list = (0..adapter.count - 1) .map { adapter.item(it) } .filterNotNull() .toList() until 使用 val list = (0 until adapter.count) .map { adapter.item(it) } .filterNotNull() .toList()

Kotlin メモ : filterNotNull

filterNotNull


Java final List<Item> list = new ArrayList<>(); for (int i = 0, count = adapter.getCount(); i < count; i++) { final Item item = adapter.getItem(i); if (item != null) { list.add(item); } } Kotlin 自動変換直後 val list = ArrayList<Item>() var i = 0 val count = adapter.count while (i < count) { val item = adapter.item(i) if (item != null) { list.add(item) } i++ } range, let 使用 val list = ArrayList<Item>() for(i in 0..adapter.count - 1) { adapter.item(i)?.let { list.add(it) } } map, filterNotNull 使用 val list = (0..adapter.count - 1) .map { adapter.item(it) } .filterNotNull() .toList()

2017年8月16日水曜日

Kotlin メモ : indexOfFirst

indexOfFirst

predicate にマッチする最初の位置の index を返す。マッチするものが無い場合は -1 を返す。


Java @Override public int getSectionForPosition(int position) { final Object[] sections = getSections(); if (sections == null) { return 0; } int section = 0; for (int i = 0; i < sections.length; i++) { final MonthSection ms = (MonthSection) sections[i]; if (ms.position > position) { return section; } section = i; } return section; } Kotlin override fun getSectionForPosition(position: Int): Int { val sections = getSections() ?: return 0 return sections.indexOfFirst { it.position > position } .let { when { it > 0 -> it - 1 it == 0 -> 0 else -> sections.lastIndex } } }

2017年8月10日木曜日

Kotlin メモ : ArrayAdapter

Java class MyAdapter extends ArrayAdapter<MyData> { private final LayoutInflater inflater; MyAdapter(Context context, List<MyData> objects) { super(context, 0, objects); inflater = LayoutInflater.from(context); } ... @NonNull @Override public View getView(int position, View convertView, @NonNull ViewGroup parent) { final MyViewHolder holder; if (convertView == null) { holder = MyViewHolder.create(inflater, parent); convertView = holder.view; convertView.setTag(holder); } else { holder = (MyViewHolder) convertView.getTag(); } final MyData data = getItem(position); assert data != null; holder.bind(data); return convertView; } } Kotlin class MyAdapter(context: Context, objects: List<MyData>) : ArrayAdapter<MyData>(context, 0, objects) { private val inflater = LayoutInflater.from(context) ... override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { val view: View = convertView ?: MyViewHolder.create(inflater, parent) .also { it.view.tag = it } .view getItem(position)?.let { (view.tag as MyViewHolder).bind(it) } return view } }

2017年8月3日木曜日

Kotlin メモ : joinToString

kotlin-stdlib / kotlin.collections / joinToString

Java /** * 00 11 22 33 44 55 66 77 */ @NonNull public String expression(byte[] bytes) { final StringBuilder sb = new StringBuilder(); boolean firstTime = true; for (byte each : bytes) { if (firstTime) { firstTime = false; } else { sb.append(" "); } sb.append(hex(each)); } return sb.toString(); } Java その2 /** * 00 11 22 33 44 55 66 77 */ @NonNull public String expression(byte[] bytes) { final List<String> tokens = new ArrayList<>(); for (byte each : bytes) { tokens.add(hex(each)); } return TextUtils.join(" ", tokens); } Kotlin /** * 00 11 22 33 44 55 66 77 */ fun expression(bytes : ByteArray): String { return bytes.joinToString(separator = " ", transform = { hex(it) }) }

2017年8月1日火曜日

Kotlin メモ : takeIf

Java public Hoge createFromParcel(Parcel source) { final int length = source.readInt(); final byte[] data; if (length > 0) { data = new byte[length]; source.readByteArray(data); } else { data = null; } return new Hoge(data); } Kotlin 変換直後 override fun createFromParcel(source: Parcel): Hoge { val length = source.readInt() val data: ByteArray? if (length > 0) { data = ByteArray(length) source.readByteArray(data) } else { data = null } return Hoge(data) } source.run {} を使う override fun createFromParcel(source: Parcel): Hoge = source.run { val length = readInt() val data: ByteArray? if (length > 0) { data = ByteArray(length) readByteArray(data) } else { data = null } Hoge(data) } also を使う override fun createFromParcel(source: Parcel): Hoge = source.run { val length = readInt() val data: ByteArray? if (length > 0) { data = ByteArray(length).also { readByteArray(it) } } else { data = null } Hoge(data) } if 式にしてみる override fun createFromParcel(source: Parcel): Hoge = source.run { val length = readInt() val data: ByteArray? = if (length > 0) { ByteArray(length).also { readByteArray(it) } } else { null } Hoge(data) } length に let を使う override fun createFromParcel(source: Parcel): Hoge = source.run { val length = readInt() val data: ByteArray? = if (length > 0) { length.let { ByteArray(it).also { readByteArray(it) } } } else { null } Hoge(data) } readInt() に takeIf を使う override fun createFromParcel(source: Parcel): Hoge = source.run { val data: ByteArray? = readInt() .takeIf { it > 0 } ?.let { ByteArray(it).also { readByteArray(it) } } Hoge(data) }


2017年7月30日日曜日

Android で Dagger を使う(その3 : Android Support)



基本的に https://google.github.io/dagger/android.html に書かれている内容です。詳しくは原文を参照してください。


Activity や Fragment のインスタンスは Android フレームーワークによって生成されるため、ライフサイクルメソッドで inject する処理を明示的に行う必要があります。 public class MainActivity extends Activity { @Inject GithubService service; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ((MyApplication) getApplication()) .getAppComponent() ... .inject(this); setContentView(R.layout.activity_main); } } この方法の問題点について、上記のリンク先では以下のように指摘されています。
  • (各 Activity や Fragment でこの処理を書くためコピー&ペーストされることになるだろう。)コピー&ペーストされたコードはのちのちリファクタリングするのが大変になる。多くの開発者がコピー&ペーストするようになってしまったら、実際に何をしているのか知っているのは一部の人だけになるだろう。
  • より本質的な話をすると、この場合 inject をリクエストしている型(つまり MainActivity)がその injector について知っている必要がある。実際の型ではなく interface を通して inject されるとしても、「どのように inject されるのかを知っているべきではない」という dependency injection の原則を破ってしまっている。

この問題に対応するために、android 用のライブラリが別途用意されています。 dependencies { compile('com.google.dagger:dagger-android:2.11') { exclude group: 'com.google.code.findbugs', module: 'jsr305' } compile('com.google.dagger:dagger-android-support:2.11') { exclude group: 'com.google.code.findbugs', module: 'jsr305' } annotationProcessor 'com.google.dagger:dagger-android-processor:2.11' } 主に dagger.android パッケージに定義されているクラスやアノテーションを利用します。


1. AndroidInjectionModule の追加

AndroidInjectionModule を application component に追加します。 @Component(modules = AndroidInjectionModule.class) interface AppComponent { ... }

2.A @ContributesAndroidInjector を使わない方法

AndroidInjector を継承した Subcomponent を定義します。 @Subcomponent(modules = ...) public interface MainActivitySubcomponent extends AndroidInjector<MainActivity> { @Subcomponent.Builder public abstract class Builder extends AndroidInjector.Builder<MainActivity> {} } 定義した subcomponent を利用する abstract module を用意します。 @Module(subcomponents = MainActivitySubcomponent.class) abstract class MainActivityModule { @Binds @IntoMap @ActivityKey(MainActivity.class) abstract AndroidInjector.Factory<? extends Activity> bindMainActivityInjectorFactory(MainActivitySubcomponent.Builder builder); } 定義した module を application component に追加します。 @Component(modules = {AndroidInjectionModule.class, MainActivityModule.class}) interface AppComponent { ... }

2.B @ContributesAndroidInjector を使う方法

2.A の subcompoennt およびその builder が他のメソッドや supertype を必要としないなら @ContributesAndroidInjector を使って生成させることができます。 @Module abstract class ActivityModule { @ContributesAndroidInjector(modules = ...) abstract MainActivity contributeMainActivityInjector(); } @ContributesAndroidInjector をつけるメソッドには、必要に応じてスコープアノテーションをつけることができます。

定義した module を application component に追加します。 @Component(modules = {AndroidInjectionModule.class, ActivityModule.class}) interface AppComponent { ... }

3. Application に HasActivityInjector を実装

Application に HasActivityInjector を実装し、inject した DispatchingAndroidInjector<Activity> を返すようにします。 public class MyApplication extends Application implements HasActivityInjector { @Inject DispatchingAndroidInjector<Activity> dispatchingActivityInjector; @Override public void onCreate() { super.onCreate(); DaggerAppComponent.create() .inject(this); } @Override public AndroidInjector<Activity> activityInjector() { return dispatchingActivityInjector; } } @Component(modules = {...}) @Singleton public interface AppComponent { void inject(MyApplication myApplication); ... }

4. AndroidInjection.inject(this)

Activity の onCreate で super.onCreate() より前に AndroidInjection.inject(this) を呼ぶようにします。 public class MainActivity extends Activity { @Inject GithubService service; @Override public void onCreate(Bundle savedInstanceState) { AndroidInjection.inject(this) super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } }

Base クラス

いくつかベースクラスが用意されています。dagger.android.DaggerXX クラスです。

例えば DaggerApplication を使うと次のようになります。
AppComponent は AndroidInjector<MyApplication> を継承するようにし、MyApplication は DaggerApplication を継承するようにします。 @Component(modules = {...}) @Singleton public interface AppComponent extends AndroidInjector<MyApplication> { } public class MyApplication extends DaggerApplication { @Override public void onCreate() { super.onCreate(); } @Override protected AndroidInjector<? extends DaggerApplication> applicationInjector() { return DaggerAppComponent.create(); } }


2017年7月25日火曜日

Android で Dagger を使う(その2 : subcomponent)


subcomponent は何?

親(parent) Component の object graph を継承し、拡張するための component

subcomponent は何のため?

アプリケーションの object graph を subgraph に分けるため

subgraph にわけるのは何のため?

- アプリケーションのさまざまな部分を互いに隔離(カプセル化)するため
- コンポーネント内で複数のスコープを使うため


つまり、subgraph に分ける必要がないのであれば、subcomponent を使う必要もなさそうです。
関係を図示すると次のようになります。
(parent)
component - modules
                            |
                            |
  ----------------------------
  |                                     |
(sibling)                  (sibling)
subcomponent       subcomponent - modules
  • subcomponent にひも付けられている object は、親および祖先の component の object に依存できる
  • subcomponent にひも付けられている object は、兄弟(sibling)subcomponent の object に依存できない
  • 親 component にひも付けられている object は、subcomponent の object に依存できない
  • 親 component の object graph は、subcomponent の object graph の subgraph になる
subcomponent は component と同じように abstract class または interface で用意します。 つけるアノテーションは @Subcomponent で、subcomponent に必要な Module を modules 属性に指定します。 modules は必須ではありません。必要な Module が無い場合は書かないこともできます。 @Subcomponent public interface MySubcomponent { } @Subcomponent(modules = MySubcomponentSpecificModule.class) public interface MySubcomponent { } @Component をつけた AppComponent interface を用意してビルドすると DaggerAppComponent が自動生成されますが、@Subcomponent をつけた interface をビルドしても何も生成されません。

subcomponent では @Subcomponent.Builder をつけた abstract class または interface を用意する必要があります。
この Builder には、引数がなく subcomponent を返すメソッドを用意する必要があります。メソッド名は build() にすることが多いです。 @Subcomponent public interface MySubcomponent { @Subcomponent.Builder interface Builder { MySubcomponent build(); } } @Subcomponent(modules = MySubcomponentSpecificModule.class) public interface MySubcomponent { @Subcomponent.Builder interface Builder { Builder mySubcomponentSpecificModule(MySubcomponentSpecificModule module) MySubcomponent build(); } } subcomponent を親の component に追加するには、親の component が取り込む Module の @Module アノテーションに subcomponents 属性で指定します。 @Module(subcomponents = MySubcomponent.class) public class MySubcomponentModule { } @Component ではなく @Module で指定するようになっているのは subcomponent の可用性・再利用性のためかなと思います。
もし subcomponentA の部分をまるっと subcomponentB に置き換えたいとなった場合、@Component で直接 subcomponentA を指定していると、subcomponentA を指定した全ての component で subcomponentB に変更する処理が必要になります。
一方、@Module で subcomponentA を指定しているなら、そこを subcomponentB に置き換えるだけで、その Module を利用している全ての component で修正は必要ありません。

よって、subcomponent を指定するための Module (特定の subcomponent 専用というよりは、subcomponent が担う処理を表現する)を別途用意するのがよいのではないかと思っています。


subcomponent を指定した Module を親の component に指定します。 @Component(modules = MySubcomponentModule.class) @Singleton public interface AppComponent { MySubcomponent.Builder mySubcomponentBuilder(); } 親の component では、@Subcomponent.Builder がついた Builder を返すメソッドを用意することができます。 これをビルドすると、DaggerAppComponent には MySubcomponent を実装した MySubcomponentImpl や、MySubcomponent.Builder を実装した MySubcomponentBuilder が生成されます。 public final class DaggerAppComponent implements AppComponent { private Provider<MySubcomponent.Builder> mySubcomponentBuilderProvider; ... @SuppressWarnings("unchecked") private void initialize(final Builder builder) { this.mySubcomponentBuilderProvider = new dagger.internal.Factory<MySubcomponent.Builder>() { @Override public MySubcomponent.Builder get() { return new MySubcomponentBuilder(); } }; } @Override public MySubcomponent.Builder mySubcomponentBuilder() { return mySubcomponentBuilderProvider.get(); } ... private final class MySubcomponentBuilder implements MySubcomponent.Builder { @Override public MySubcomponent build() { return new MySubcomponentImpl(this); } } private final class MySubcomponentImpl implements MySubcomponent { private MySubcomponentImpl(MySubcomponentBuilder builder) { assert builder != null; } } } mySubcomponentBuilder() で MySubcomponent.Builder が取得できるので、Builder の build() を呼んで MySubcomponent が取得できます。
あとの使い方は component と同じです。 final MySubcomponent mySubcomponent = ((MyApplication) getApplication()) .getAppComponent() .mySubcomponentBuilder() .build(); Android で Dagger を使う(その1) で AppComponent に void inject(MainActivity target) を用意しました。 @Component(modules = ApiModule.class) public interface AppComponent { void inject(MainActivity target); } MainActivity 用の subcomponent を用意して、inject() をそちらに移動してみましょう。

(MainActivity でしか使わない Module があるとか、Activity 単位でスコープを指定しないとまずいというわけでもない限り、わざわざ MainActivity 用の subcomponent を用意する必要はないと思います。) @Subcomponent public interface MainActivitySubcomponent { void inject(MainActivity target); @Subcomponent.Builder interface Builder { MainActivitySubcomponent build(); } } @Module(subcomponents = MainActivitySubcomponent.class) public class MainActivityModule { } @Component(modules = {ApiModule.class, MainActivityModule.class}) @Singleton public interface AppComponent { MainActivitySubcomponent.Builder mainActivitySubcomponentBuilder(); } public class MainActivity extends AppCompatActivity { @Inject ApiService apiService; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ((MyApplication) getApplication()) .getAppComponent() .mainActivitySubcomponentBuilder() .build() .inject(this); setContentView(R.layout.activity_main); } }

Scope

Subcomponent に分割する目的の一つにスコープがあります。
通常のスコープされていない紐付けでは、inject される方は新しい個別のインスンタンスをうけとります。 一方スコープされている紐付けでは、スコープのライフサイクル内では同じインスタンスを受け取ります。

Dagger では component にスコープ(@Scope アノテーションで注釈されたアノテーション)を指定することができます。 指定すると、component の実装クラスでは同じスコープが指定された型のインスタンスを保持するようになります。これにより同じインスタンスを再利用することができます。

標準スコープが @Singleton です。スコープなので Singleton アノテーションの定義には @Scope がついています。

subcomponent は親および祖先と同じスコープにすることはできません。

これはだめ @Component(modules = {ApiModule.class, MainActivityModule.class}) @Singleton public interface AppComponent { MainActivitySubcomponent.Builder mainActivitySubcomponentBuilder(); } @Subcomponent @Singleton public interface MainActivitySubcomponent { ... } 互いに到達できない2つの subcomponent に同じスコープを指定することはできます(同じスコープアノテーションを使用しても、実際のスコープインスタンスは異なります)。

これはできる @Component(modules = {...}) @Singleton public interface AppComponent { MainActivitySubcomponent.Builder mainActivityComponent(); MainActivity2Subcomponent.Builder mainActivity2Component(); } @Subcomponent @ActivityScope public interface MainActivitySubcomponent { ... } @Subcomponent @ActivityScope public interface MainActivity2Subcomponent { ... } @Scope @Documented @Retention(RUNTIME) public @interface ActivityScope { }


2017年7月23日日曜日

Android で Dagger を使う(その1)


Dependency Injection

API にアクセスするための interface が用意されているとします。 interface ApiService { ... } この interface の実装クラスが何であるかや、どうインスタンス化するかを利用側(例えば Activity)では意識したくありません。
ApiService をどうインスタンス化するかは Activity 側の責務ではないからです。 public class MainActivity extends AppCompatActivity { // どうやってインスタンス化するかは知りたくない ApiService service; } 必要としているもの(dependency)を内部で生成するのではなく、外部から注入(injection)する手法が dependency injection(DI)です。

外部から注入すると
  • テストなど、状況に応じて注入するインスタンスを切り替えられる
  • インスタンス化の方法が変わっても利用側は影響をうけない
などの利点があります。

外部から注入する一番簡単な方法は、コンストラクタで渡すことです。 しかし Activity はフレームワークが生成するため、この方法はとれません。

↓これはできない public class MainActivity extends AppCompatActivity { ApiService service; public MainActivity(ApiService service) { this.service = service; } } そこで、インスタンス化を責務とするクラス(例えば Factory など)を用意して、Activity からはそれを使ってインスタンスを取得するようにします。 public class MainActivity extends AppCompatActivity { ApiService service; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); service = ApiServiceFactory.create(); setContentView(R.layout.activity_main); } } public class ApiServiceFactory { private static ApiService service; @NonNull public synchronized static ApiService create() { if (service == null) { final Retrofit retrofit = ...; return retrofit.create(ApiService.class); } return service; } } Retrofit では毎回インスタンスを生成するとコストが高いので Singleton にします。

この方法には以下のような欠点があります。
  • 外部から注入したいクラスごとに同じようなFactoryクラスが必要
  • どの Factory を使うかを結局知っていなくてはいけない
Dagger を使うと、これらの欠点を解消して DI を行うことができます。



Dagger

Dagger (https://google.github.io/dagger) は Java で DI を行うことをサポートするライブラリです。

「DI = Dagger を使うこと」ではありません。上記で書いたように Dagger を使わなくても DI はできます。 dependencies { compile 'com.google.dagger:dagger:2.11' annotationProcessor 'com.google.dagger:dagger-compiler:2.11' } Dagger では ComponentModule が主な構成要素です。

Component は injector です。inject される側(例だと Activity)は、Component を経由して必要なインスタンス(例だと ApiService のインスンタンス)をもらいます。

Module の責務はインスタンスの生成です。そのため、Module には inject するもの(例だと ApiService)をどうインスタンス化するかを定義します。

Module 用のクラスには @Module アノテーションをつけます。
inject するもの(ApiService のインスタンス)を返すメソッドを用意し、@Provides アノテーションをつけます。 @Module public class ApiModule { @Provides public ApiService provideApiService() { final Retrofit retrofit = ...; return retrofit.create(ApiService.class); } } 慣例で Module 用のクラスには Module suffix、@Provides をつけるメソッドには provide prefix をつけることが多いです。

Component は abstract class か interface で用意します。
@Component アノテーションをつけ、modules 属性に必要な Module クラスを指定します。 @Component(modules = ApiModule.class) public interface AppComponent { ApiService apiService(); } ビルドすると Component の実装クラスが生成されます。名前は Dagger + Component クラス名になるので、この場合だと DaggerAppComponent クラスが自動生成されます。 public final class DaggerAppComponent implements AppComponent { private Provider<ApiService> provideApiServiceProvider; private DaggerAppComponent(Builder builder) { assert builder != null; initialize(builder); } public static Builder builder() { return new Builder(); } public static AppComponent create() { return new Builder().build(); } @SuppressWarnings("unchecked") private void initialize(final Builder builder) { this.provideApiServiceProvider = ApiModule_ProvideApiServiceFactory.create(builder.apiModule); } @Override public ApiService apiService() { return provideApiServiceProvider.get(); } public static final class Builder { private ApiModule apiModule; private Builder() {} public AppComponent build() { if (apiModule == null) { this.apiModule = new ApiModule(); } return new DaggerAppComponent(this); } public Builder apiModule(ApiModule apiModule) { this.apiModule = Preconditions.checkNotNull(apiModule); return this; } } } (AppComponent や ApiModule の実装を変えると DaggerAppComponent の実装も変わります)


Module の @Provides がついたメソッドからは、対応する Factory が自動生成されます。例えば ApiModule の provideApiService() メソッドに対しては ApiModule_ProvideApiServiceFactory が自動生成されます。 public final class ApiModule_ProvideApiServiceFactory implements Factory<ApiService> { private final ApiModule module; public ApiModule_ProvideApiServiceFactory(ApiModule module) { assert module != null; this.module = module; } @Override public ApiService get() { return Preconditions.checkNotNull( module.provideApiService(), "Cannot return null from a non-@Nullable @Provides method"); } public static Factory<ApiService> create(ApiModule module) { return new ApiModule_ProvideApiServiceFactory(module); } } DaggerAppComponent はこの Factory を介して ApiModule の provideApiService() を呼び出し、ApiService のインスタンスを取得します。



AppComponent のインスタンスは自動生成される DaggerAppComponent.Builder を使って取得します。 final AppComponent appComponent = DaggerAppComponent.builder() .apiModule(new ApiModule()) .build(); 必要な Module が全てデフォルトコンストラクタで生成できる、もしくは @Provides がつけられたメソッドが全て static の場合は create() メソッドも用意されます。 final AppComponent appComponent = DaggerAppComponent.create();

Android では Application クラスで Component を生成して保持しておくことが多いです。 public class MyApplication extends Application { private AppComponent appComponent; @Override public void onCreate() { super.onCreate(); appComponent = DaggerAppComponent.builder() .apiModule(new ApiModule()) .build(); } public AppComponent getAppComponent() { return appComponent; } } これで MainActivity では AppComponent の apiService() から ApiService インスタンスをもらえるようになりました。 public class MainActivity extends AppCompatActivity { ApiService service; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); service = ((MyApplication) getApplication()) .getAppComponent() .apiService(); setContentView(R.layout.activity_main); } } Activity が自分で field に代入する書き方は field が増えると冗長になってきます。 public class MainActivity extends AppCompatActivity { ApiService apiService; HogeService hogeService; FugaService fugaService; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); final AppComponent appComponent = ((MyApplication) getApplication()) .getAppComponent(); apiService = appComponent.apiService(); hogeService = appComponent.hogeService(); fugaService = appComponent.fugaService(); setContentView(R.layout.activity_main); } } そこで、field に代入する部分も Dagger に任せるようにしてみましょう。

まず AppComponent に inject される側を引数にとる void メソッドを定義します。 @Component(modules = ApiModule.class) public interface AppComponent { void inject(MainActivity target); } そして、Dagger で注入したい field に @Inject アノテーション をつけます。
field の代入していた部分を上記で定義した inject メソッドを呼ぶように変更します。 public class MainActivity extends AppCompatActivity { @Inject ApiService apiService; @Inject HogeService hogeService; @Inject FugaService fugaService; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); ((MyApplication) getApplication()) .getAppComponent() .inject(this); setContentView(R.layout.activity_main); } } AppComponent に void inject(MainActivity target) を定義したので、 MainActivity_MembersInjector が自動生成されます。

DaggerAppComponent の inject() を呼ぶと、MainActivity_MembersInjector を使って MainActivity の @Inject がついた field にインスタンスが代入されます。 public final class DaggerAppComponent implements AppComponent { ... @SuppressWarnings("unchecked") private void initialize(final Builder builder) { this.provideApiServiceProvider = ApiModule_ProvideApiServiceFactory.create(builder.apiModule); this.mainActivityMembersInjector = MainActivity_MembersInjector.create(provideApiServiceProvider); } @Override public void inject(MainActivity target) { mainActivityMembersInjector.injectMembers(target); } ... } public final class MainActivity_MembersInjector implements MembersInjector<MainActivity> { private final Provider<ApiService> apiServiceProvider; public MainActivity_MembersInjector(Provider<ApiService> apiServiceProvider) { assert apiServiceProvider != null; this.apiServiceProvider = apiServiceProvider; } public static MembersInjector<MainActivity> create(Provider<ApiService> apiServiceProvider) { return new MainActivity_MembersInjector(apiServiceProvider); } @Override public void injectMembers(MainActivity instance) { if (instance == null) { throw new NullPointerException("Cannot inject members into a null reference"); } instance.apiService = apiServiceProvider.get(); } public static void injectApiService( MainActivity instance, Provider<ApiService> apiServiceProvider) { instance.apiService = apiServiceProvider.get(); } }



Singleton

Retrofit では毎回インスタンスを生成するとコストが高いので ApiService は Singleton にしたいのでした。

ApiModule の provideApiService() に @Singleton アノテーションをつけると、provideApiService() で生成されたインスタンスは Singleton として保持されます。 @Module public class ApiModule { @Singleton @Provides public ApiService provideApiService() { final Retrofit retrofit = ...; return retrofit.create(ApiService.class); } } 実際に Singleton として管理するのは Component の実装クラスです。
どの Component が管理するかを指定するために、管理する Component にも @Singleton をつけます。 @Component(modules = ApiModule.class) @Singleton public interface AppComponent { ... } DaggerAppComponent では、@Singleton アノテーションがついた @Provides メソッドは、DoubleCheck クラスを使って Singleton として管理するようになります。 public final class DaggerAppComponent implements AppComponent { ... @SuppressWarnings("unchecked") private void initialize(final Builder builder) { this.provideApiServiceProvider = DoubleCheck.provider(ApiModule_ProvideApiServiceFactory.create(builder.apiModule)); ... } } ちなみに @Singleton はスコープです。定義に @Scope がついています。 @Scope @Documented @Retention(RUNTIME) public @interface Singleton {} 独自のスコープを作ることもできます。スコープの詳しい説明はここではしません。



テスト

テスト時に mock 化した ApiService インスタンスを注入することを考えましょう。

まず MyApplication が持つ AppComponent を差し替えられるように、テスト用のメソッドを用意します。 public class MyApplication extends Application { private AppComponent appComponent; ... @VisibleForTesting public void setAppComponent(AppComponent appComponent) { this.appComponent = appComponent; } } 次に ApiModule を継承した MockApiModule を用意します。 public class MockApiModule extends ApiModule { private final ApiService service; public MockApiModule(ApiService service) { this.service = service; } @Override public ApiService provideApiService() { return service; } } DaggerAppComponent.Builder で ApiModule を差し替えることができるので、mock 化した ApiService インスタンス持った MockApiModule に差し替えます。 final AppComponent appComponent = DaggerAppComponent.builder() .apiModule(new MockApiModule(mockApiService)) .build(); テストでは MyApplication が持つ AppComponent を差し替えてから MainActivity を起動します。
こうすることで MainActivity には MockApiModule を介して mock 化された ApiService インスタンスが注入されます。 @RunWith(AndroidJUnit4.class) public class MainActivityUiTest { @Rule ActivityTestRule<MainActivity> activityTestRule = new ActivityTestRule<>(MainActivity.class, false, false); @Test public void test() throws Exception { final Context context = InstrumentationRegistry.getTargetContext(); final ApiService mockApiService = mock(ApiService.class); ... final AppComponent appComponent = DaggerAppComponent.builder() .apiModule(new MockApiModule(mockApiService)) .build(); ((MyApplication) context.getApplicationContext()) .setAppComponent(appComponent); activityTestRule.launchActivity(new Intent(context, MainActivity.class)); ... } }



余談

Subcomponent 使ってません。
Activity ごとに Subcomponent にわけるべきなのか私にはまだわかりません。
Subcomponent に分けるということは Component が1つの場合にくらべて明らかに複雑性は増します。
そのデメリットを上回るメリットがわかりませんでした。
(MVPとかMVVMではメリットあるのかな?Androidの思想に沿った設計が好きでそういうの取り入れてないのでわからないです)


2017年7月14日金曜日

Kotlin メモ : data class で List はいい感じに処理してくれるけど Array おまえはダメだ

data class A と B と C があって、C は A の配列と B を持っています。 data class A(val name: String) data class B(val age: Int) data class C(val names: Array<A>, val age: B) A と B に対する以下のテストは通ります assertEquals(A("hoge"), A("hoge")) assertEquals(B(10), B(10)) まぁ、そうだよね。

C に対する以下のテストは通りません assertEquals(C(arrayOf(A("hoge")), B(10)), C(arrayOf(A("hoge")), B(10))) data class がいい感じにやってくれるのかと思っていたのよ。やってくれなかった。

警告が出るのはそういうことなのね。



override すればいいんだけど... いけてない class C(val names: Array<A>, val age: B) { override fun equals(other: Any?): Boolean { if (this === other) return true if (other?.javaClass != javaClass) return false other as C if (!Arrays.equals(names, other.names)) return false if (age != other.age) return false return true } override fun hashCode(): Int { var result = Arrays.hashCode(names) result = 31 * result + age.hashCode() return result } } Array で持つのやめて List にしたらどうかなと思って試したら data class A(val name: String) data class B(val age: Int) data class C(val names: List<A>, val age: B) assertEquals(C(listOf(A("hoge")), B(10)), C(listOf(A("hoge")), B(10))) 通った


結論: Array はダメだが List ならいい感じにやってくれる