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 ならいい感じにやってくれる


Kotlin メモ : data class を継承できないので interface で実現した話

データクラス的な A と B があります。B は A を継承しています。 public class A { @NonNull public final String name; public A(@NonNull String name) { this.name = name; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; A a = (A) o; return name.equals(a.name); } @Override public int hashCode() { return name.hashCode(); } } public class B extends A { public final int size; public B(String name, int size) { super(name); this.size = size; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; if (!super.equals(o)) return false; B b = (B) o; return size == b.size; } @Override public int hashCode() { int result = super.hashCode(); result = 31 * result + size; return result; } } test はこんな感じです。B が A を継承しているのは、A のリストに B を入れたいからです。 public class ABTest { @Test public void test() { final B b = new B("hoge", 10); assertEquals("hoge", b.name); assertEquals(10, b.size); assertEquals(new B("hoge", 10), new B("hoge", 10)); assertNotEquals(new B("hoge", 10), new B("fuga", 10)); assertNotEquals(new B("hoge", 10), new B("hoge", 11)); List<A> aList = new ArrayList<>(); aList.add(new B("hoge", 10)); assertEquals(new B("hoge", 10), aList.get(0)); } } こういう Java コードがあって、これを Kotlin 化するときに A, B それぞれを data class にしようとしたら、data class は親クラスになれない(final)ので困りました。
このテストが通るように、いい感じに Kotlin 化したいのです。

1. data class にするのを諦める

自分で equals と hasCode を override すれば data class にしなくてもやりたいことはできます。
以下は自動変換しただけのもの。 open class A(val name: String) { override fun equals(o: Any?): Boolean { if (this === o) return true if (o == null || javaClass != o.javaClass) return false val a = o as A? return name == a!!.name } override fun hashCode(): Int { return name.hashCode() } } class B(name: String, val size: Int) : A(name) { override fun equals(o: Any?): Boolean { if (this === o) return true if (o == null || javaClass != o.javaClass) return false if (!super.equals(o)) return false val b = o as B? return size == b!!.size } override fun hashCode(): Int { var result = super.hashCode() result = 31 * result + size return result } } もちろん上のテストは通ります。
しかし、いけてない。

2. A を interface にする

Kotlin では interface に抽象プロパティを持たせることができるので、A を interface に変えてみます。 interface A { val name: String } こうすると、B を data クラスにできます。 data class B(override val name: String, val size: Int) : A めっちゃ短い!
テストもちゃんと通ります。



2017年7月13日木曜日

Kotlin メモ : Parcelable

Java public class Receipt implements Parcelable { @NonNull private final Date date; private final int value; public Receipt(@NonNull Date date, int value) { this.date = date; this.value = value; } @NonNull public Date getDate() { return date; } public int getValue() { return value; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Receipt receipt = (Receipt) o; if (value != receipt.value) return false; return date.equals(receipt.date); } @Override public int hashCode() { int result = date.hashCode(); result = 31 * result + value; return result; } // // Parcelable // @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeSerializable(date); dest.writeInt(value); } public static final Creator<Receipt> CREATOR = new Creator<Receipt>() { @Override public Receipt createFromParcel(Parcel in) { return new Receipt((Date) in.readSerializable(), in.readInt()); } @Override public Receipt[] newArray(int size) { return new Receipt[size]; } }; } Kotlin 自動変換直後 class Receipt(val date: Date, val value: Int) : Parcelable { override fun equals(o: Any?): Boolean { if (this === o) return true if (o == null || javaClass != o.javaClass) return false val receipt = o as Receipt? if (value != receipt!!.value) return false return date == receipt.date } override fun hashCode(): Int { var result = date.hashCode() result = 31 * result + value return result } // // Parcelable // override fun describeContents(): Int { return 0 } override fun writeToParcel(dest: Parcel, flags: Int) { dest.writeSerializable(date) dest.writeInt(value) } companion object { val CREATOR: Parcelable.Creator<Receipt> = object : Parcelable.Creator<Receipt> { override fun createFromParcel(`in`: Parcel): Receipt { return Receipt(`in`.readSerializable() as Date, `in`.readInt()) } override fun newArray(size: Int): Array<Receipt> { return arrayOfNulls(size) } } } } return arrayOfNulls(size) で型があわないと言われるので、newArray() の返す型を Array<Receipt?> にする

override fun newArray(size: Int): Array { return arrayOfNulls(size) } CREATOR が Java から見えるように @JvmField をつける companion object { @JvmField val CREATOR: Parcelable.Creator<Receipt> = object : Parcelable.Creator<Receipt> {


(Parcelable とは関係ないが) data クラスにして equals() と hashCode() を省略する data class Receipt(val date: Date, val value: Int) : Parcelable { override fun describeContents(): Int { return 0 } override fun writeToParcel(dest: Parcel, flags: Int) { dest.writeSerializable(date) dest.writeInt(value) } companion object { @JvmField val CREATOR: Parcelable.Creator<Receipt> = object : Parcelable.Creator<Receipt> { override fun createFromParcel(`in`: Parcel): Receipt { return Receipt(`in`.readSerializable() as Date, `in`.readInt()) } override fun newArray(size: Int): Array<Receipt?> { return arrayOfNulls(size) } } } } describeContents() と newArray() で = を使って関数の本体を省略 data class Receipt(val date: Date, val value: Int) : Parcelable { override fun describeContents() = 0 override fun writeToParcel(dest: Parcel, flags: Int) { dest.writeSerializable(date) dest.writeInt(value) } companion object { @JvmField val CREATOR: Parcelable.Creator<Receipt> = object : Parcelable.Creator<Receipt> { override fun createFromParcel(`in`: Parcel): Receipt { return Receipt(`in`.readSerializable() as Date, `in`.readInt()) } override fun newArray(size: Int): Array<Receipt?> = arrayOfNulls(size) } } } createFromParcel() の引数名に Java では in が使われているが、 Kotlin では予約語なので ` でエスケープされている。
紛らわしいので source に変える data class Receipt(val date: Date, val value: Int) : Parcelable { override fun describeContents() = 0 override fun writeToParcel(dest: Parcel, flags: Int) { dest.writeSerializable(date) dest.writeInt(value) } companion object { @JvmField val CREATOR: Parcelable.Creator<Receipt> = object : Parcelable.Creator<Receipt> { override fun createFromParcel(source: Parcel): Receipt { return Receipt(source.readSerializable() as Date, source.readInt()) } override fun newArray(size: Int): Array<Receipt?> = arrayOfNulls(size) } } } writeToParcel() の dest と createFromParcel() の source でスコープ関数を使う(お好みで) data class Receipt(val date: Date, val value: Int) : Parcelable { override fun describeContents() = 0 override fun writeToParcel(dest: Parcel, flags: Int) { dest.run { writeSerializable(date) writeInt(value) } } companion object { @JvmField val CREATOR: Parcelable.Creator<Receipt> = object : Parcelable.Creator<Receipt> { override fun createFromParcel(source: Parcel): Receipt = source.run { Receipt(readSerializable() as Date, readInt()) } override fun newArray(size: Int): Array<Receipt?> = arrayOfNulls(size) } } } run() を使っています。
他のスコープ関数(let, with, run, apply, also)を使うと次のようになります。

writeToParcel() ではどれを使ってもやりたいことはできます。
選ぶとしたら it が省略できる(そのレシーバを this とする関数を呼び出す) with か run か apply がよさそうです。 // let dest.let { it.writeSerializable(date) it.writeInt(value) } // with with(dest) { writeSerializable(date) writeInt(value) } // run dest.run { writeSerializable(date) writeInt(value) } // apply dest.apply { writeSerializable(date) writeInt(value) } // also dest.also { it.writeSerializable(date) it.writeInt(value) } createFromParcel() では、レシーバーの Parcel ではなく Receipt を返したいので apply, also は使えません。
ここでも選ぶとしたら it が省略できる with か run がよさそうです。 // let override fun createFromParcel(source: Parcel): Receipt = source.let { Receipt(it.readSerializable() as Date, it.readInt()) } // with override fun createFromParcel(source: Parcel): Receipt = with(source) { Receipt(readSerializable() as Date, readInt()) } // run override fun createFromParcel(source: Parcel): Receipt = source.run { Receipt(readSerializable() as Date, readInt()) } 65行が21行になった。



2017年7月11日火曜日

Kotlin メモ : firstOrNull()

Java public enum Fruit { APPLE(0), BANANA(1), PEACH(2), ORANGE(3); private final int value; Fruit(int value) { this.value = value; } @NonNull public static Fruit find(int value) { for (Fruit fruit : values()) { if (fruit.value == value) { return fruit; } } return APPLE; } } Kotlin 変換直後 enum class Fruit(private val value: Int) { APPLE(0), BANANA(1), PEACH(2), ORANGE(3); companion object { @JvmStatic fun find(value: Int): Fruit { for (fruit in values()) { if (fruit.value == value) { return fruit } } return APPLE } } } Kotlin with firstOrNull() enum class Fruit(private val value: Int) { APPLE(0), BANANA(1), PEACH(2), ORANGE(3); companion object { @JvmStatic fun find(value: Int): Fruit { return values().firstOrNull { it.value == value } ?: APPLE } } }

2017年7月7日金曜日

Kotlin スタートブックを読みました。



正誤表 http://www.ric.co.jp/book/error/error1039.html

Java をある程度できる人にとって、手っ取り早く Kotlin で読み書きできるようになれる入門書だと思いました。 第II部は特によかったです。

以下、もやもやした点と、正誤表に載っていなかった気になる点について書いておきます。





p46 4章 1.4
「原則として val を使用し、再代入を極力避けるべきです。」
とだけあってなぜなのか書かれていませんでした。わかる人にはわかりますが、わからない人にはわからないと思います。



p49 4章 2.1
イミュータブルという単語がいきなり出てきて特に説明もないので、この本を読む人はイミュータブルの意味を知っていて当然ということのようです。
カタカナでイミュータブルと書かれるともにょもにょしますが、まぁ決めの問題なので書く人の好きなようにすればよいと思います。こういう単語の翻訳は難しいですね。



p51 4章 2.1
trimMargin() は行頭の空白と続く marginPrefix を消してくれる関数ですが、本書での説明だと | だけ消すかのように読み取れました。
https://kotlinlang.org/docs/reference/basic-types.html
trimMargin() の marginPrefix のデフォルトが | であることも触れた方がよいと思いました。



p59 4章 3.2
myFavoriteInt() の実体が書かれていないので混乱しましたが、Int を返す関数のようです。
「条件分岐の部分に、定数以外を指定できるからです」
とあったので、条件式を書けるのかと思ってしまいました。そうではなく評価した結果の値が定数のように扱われるようでした。
つまり when (x) { 1 -> "one" x % 2 == 0 -> "odd" else -> "even" } のように書けるのかと思ってしまったのですが、以下のように書かないといけません。 val x = 4 when { x == 1 -> "one" x % 2 == 0 -> "odd" else -> "even" }



p243 15章 2
view_article.xml のルート View を RelativeLayout にしていますが、 これを FrameLayout を継承した ArticleView に追加するのは View 階層の無駄なので、ここでは ルートを <merge> にして ArticleView が RelativeLayout を継承する形にするのが適切だと思います。



p245 15章 2
そもそも lazy の例として、カスタムビューで子 View のインスタンスを取得するのは適切ではないと思います。
Java で書くときも以下のように final field にしてコンストラクタで初期化するようにしています。 public class ArticleView extends RelativeLayout { private final ImageView profileImageView; private final TextView titleView; private final TextView userNameTextView; public ArticleView(Context context) { this(context, null); } public ArticleView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ArticleView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); LayoutInflater.from(context).inflate(R.layout.view_article, this); profileImageView = (ImageView) findViewById(R.id.profile_image_view); titleView = (TextView) findViewById(R.id.title_text_view); userNameTextView = (TextView) findViewById(R.id.user_name_text_view); } } これを Kotlin でもやるなら以下のようになって、そもそも lazy はいりません。 class ArticleView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : RelativeLayout(context, attrs, defStyleAttr) { private val profileImageView: ImageView private val titleView: TextView private val userNameTextView: TextView init { LayoutInflater.from(context).inflate(R.layout.view_article, this) profileImageView = findViewById(R.id.profile_image_view) as ImageView titleView = findViewById(R.id.title_text_view) as TextView userNameTextView = findViewById(R.id.user_name_text_view) as TextView } } Fragment で getArguments() から値を取り出す場合のほうが lazy の例として適切だと思います。



p247 15章 2
p253 15章 3
ArticleView のコンストラクタに applicationContext を渡していますが、よくないです。
理由は View のコンテキストに Application Context を渡すとテーマが適用されない に書きました。



p251 15章 3
私だったら Adapter が持つ List
は外部に公開しないようにするかな。



p274 16章
Context に拡張関数で toast() を生やす方法、Window を持たない Context (Application Context とか Service の Context とか)でうっかり呼び出すと落ちるから、個人的にはあんまり適切な例ではないと思っています。
Toast の Context は Application Context でも OK でした。最近 Toast 使わないからうっかりしてました。 Toast では Context をパッケージ名の取得と string とかのリソースの取得にしか使ってないので Window は関係ありませんでした。 Application Context を渡すと Window が無くて死ぬのは Dialog でした。




View のコンテキストに Application Context を渡すとテーマが適用されない

View のコンストラクタは Context を必要とします。
Context として以下のように Application Context を渡しているコードを見かけました。 public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Button textView = new Button(getApplicationContext()); } } これがなぜよくないのか説明します。

View に渡された Context はスタイル属性を取得するのに使われます。

View.java public View(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) { this(context); final TypedArray a = context.obtainStyledAttributes( attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes); ... background = a.getDrawable(attr); ... View のスタイルは
  1. 1. XMLタグで指定された値(例 android:background="@drawable/button_bg")
  2. 2. style で指定された値(例 style="@style/MyButtonStyle)
  3. 3. テーマで指定された値(例 テーマの中で @style/MyButtonStyle
の優先度で適用されます。

このことを踏まえて obtainStyledAttributes() の中身をみると

Context.java public final TypedArray obtainStyledAttributes( AttributeSet set, @StyleableRes int[] attrs, @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { return getTheme().obtainStyledAttributes( set, attrs, defStyleAttr, defStyleRes); } getTheme() で Theme を取得し、Theme の obtainStyledAttributes() を呼んでいます。
つまり、3. では Context が持っているテーマ情報を使っているのです。

Application Context を渡してはいけない理由がわかったでしょうか。

Activity ごとにテーマを指定できるのに、Application Context を渡してしまうと、Activity のテーマが全く利用されません。
実際にやってみましょう。

Application には Theme.AppCompat(黒系)、MainActivity には Theme.AppCompat.Light(白系)を指定します。 <manifest ...> <application ... android:theme="@style/Theme.AppCompat"> <activity android:name=".MainActivity" android:theme="@style/Theme.AppCompat.Light"> ... </activity> </application> </manifest> public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button button1 = new Button(getApplicationContext()); button1.setText("Application Context"); Button button2 = new Button(this); button1.setText("Activity Context"); LinearLayout ll = (LinearLayout) findViewById(R.id.container); ll.addView(button1); ll.addView(button2); } }



画面のテーマは Theme.AppCompat.Light (白系)なのに、Application Context を渡した上のボタンは Theme.AppCompat(黒系)の色になってしまっています。



Context が持つテーマが利用されるという仕組みは、v7 AppCompat Support Library でも使われています。 AppCompatTextView を見てみましょう。 public class AppCompatTextView extends TextView implements TintableBackgroundView { ... public AppCompatTextView(Context context, AttributeSet attrs, int defStyleAttr) { super(TintContextWrapper.wrap(context), attrs, defStyleAttr); TintContextWrapper でラップした Context を TextView に渡しています。
これにより、colorAccent の指定色によって自動でテキストカーソルなどが tint されます。





2017年5月28日日曜日

What's New in Android Design Tools (Google I/O '17)



# What's new ではない復習的な内容は一部省略しています。

ConstraintLayout

  • Unified and expressive way to create Layouts
  • Flat Layouts
  • Deep INtegration with Android Studio & the Layout Editor
  • Unbundled Library
  • Compatible with 99% of Android Devices
1.0
  • Google I/O 2016 から17回リリース
  • 2017年2月に 1.0 リリース
  • パフォーマンス改善
  • Relative positioning
  • Center positioning & bias
  • Guidelines
  • Chains
  • Ratio
  • ConstraintSet
Android Studio でプロジェクトを作った時のデフォルトが ConstraintLayout に

コミュニティベースのサイトがオープン https://constraintlayout.com/

1.1.0 beta1 maven { url "https://maven.google.com" } dependencies { compile "com.android.support.constraint:constraint-layout:1.1.0-beta1" }
  • Barriers
    • widget の集まりに対して、最小 or 最大の端を取るもの
  • Group
    • widget の集まりを定義できる
    • group に対して setVisibility() すると、それに含まれるすべての widget に setVisibility() される
  • Placeholder

Placeholder

virtual view を作成し、ConstraintLayout の別の view を contents としてセットできる TransitionManager.beginDelayedTransition(container); placeholder.setContentId(view.getId()); 縦横のレイアウトを Placeholder を使った template として用意し、メインのレイアウトを1つにできる layout/header_template.xml <merge ... android:layout_width="match_parent" android:layout_height="match_parent" tools:parentTag="android.support.constraint.ConstraintLayout"> <android.support.constraint.Placeholder android:id="@+id/template_main_image" app:content="@+id/top_image" app:layout_constraintDimensionRatio="16:9" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" /> <android.support.constraint.Placeholder android:id="@+id/template_action" android:layout_width="48dp" android:layout_height="48dp" app:content="@+id/action_button" app:layout_constraintBottom_toBottomOf="@id/template_main_image" app:layout_constraintHorizontal_bias="0.80" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toBottomOf="@id/template_main_image" /> </merge> layout-land/header_template.xml <merge ... android:layout_width="match_parent" android:layout_height="match_parent" tools:parentTag="android.support.constraint.ConstraintLayout"> <android.support.constraint.Placeholder android:id="@+id/template_main_image" app:content="@+id/top_image" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintDimensionRatio="1:1" app:layout_constraintTop_toTopOf="parent" /> <android.support.constraint.Placeholder android:id="@+id/template_action" android:layout_width="48dp" android:layout_height="48dp" app:content="@+id/action_button" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toRightOf="@id/template_main_image" app:layout_constraintRight_toRightOf="@id/template_main_image" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.80" /> </merge> layout/activity_main.xml <android.support.constraint.ConstraintLayout ... android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.sample.myapplication.MainActivity"> <ImageView android:id="@+id/top_image" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <ImageButton android:id="@+id/action_button" android:layout_width="48dp" android:layout_height="48dp" /> <include layout="@layout/header_template" /> </android.support.constraint.ConstraintLayout> template に割り当てる部分は ViewGroup や include でも問題ない <android.support.constraint.ConstraintLayout ... android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.sample.myapplication.MainActivity"> <ImageView android:id="@+id/top_image" android:layout_width="wrap_content" android:layout_height="wrap_content" /> <include android:id="@+id/action_button" layout="@layout/action_button_content" /> <include layout="@layout/header_template" /> </android.support.constraint.ConstraintLayout>

ConstraintSet

  • View 自体と、それをどのようにレイアウトするかを分離
  • すべての制約を一つのオブジェクトにカプセル化
  • 既存の ConstraintLayout に ConstraintSet を適用できる
  • 複数の ConstraintSet 間を切り替え
    • layout を切り替えるわけではないので、view は reload されない
ConstraintSet mConstraintSet1 = new ConstraintSet(); ConstraintSet mConstraintSet2 = new ConstraintSet(); // get constraints from layout mConstraintSet2.clone(context, R.layout.state2); setContentView(R.layout.state1); mConstraintLayout = (ConstraintLayout) findViewByid(R.id.activity_main); // get constraints from ConstraintLayout mConstraintSet1.clone(mConstraintLayout); // switch with animation TransitionManager.beginDelayedTransition(mConstraintLayout); // switch to state2 mConstraintSet2.apply(mConstraintLayout);
その他の利用例) 縦横それぞれのレイアウトとその ConstraintSet を用意し、画面回転を自分でハンドリングして、そのときに ConstraintSet を切り替えることで自分でレイアウトを切り替えることが可能


ConstraintLayout & Motion
  • Flat Hierarchy == No clipping issues
  • Scene Graph
  • ConstraintSet == Keyframe


Android Studio 3.0

Tools がいろいろある
  • alignment tools
  • arrengement tools
  • guideline tools
  • right click menu
Inference
  • 接続の確率モデルに基づく
  • 制約されていない view を制約する(すでに制約されているものは何もしない)
  • view は動かさない(alignment tools ではない)
Advanced Inspector
  • properties がどの layout, dimen, strings 由来なのか表示
Tools attributes https://developer.android.com/studio/write/tool-attributes.html
  • tools:
    • デザイン時の属性を上書き
  • tools:showIn
    • このレイアウトを他のやつに埋め込んで表示する
  • tools:layout
    • fragmentで利用するレイアウト
  • tools:listitem
    • list item のレイアウト
  • tools:parentTag
    • merge tag の parent のレイアウトを指定

Sample Data

* Sample Data は Android Studio 3.0 Canary 3 で使えるようになりました。

  • Default Adapter で使える
  • content を指定する
  • tools:listitem で list item のレイアウトを指定する
  • list item のレイアウトで sample data を使う tools:text="@tools:sample/lorem" tools:text="@tools:sample/full_names" tools:text="@tools:sample/us_phones" tools:text="@tools:sample/date_mmddyyyy"
  • sampledata フォルダーを作る(app > sampledata)
  • フォルダーに colors ファイルを追加 #d50000 #2962ff #00e5ff #aeea00 #ff6d00 tools:background="@sample/colors"
  • フォルダーに json(contacts.json とか)ファイルを追加 { "contacts": [ { "name":"...", ... } ] } tools:text="@sample/contacts.json/contacts/name"
  • Baked-in data types
    • date, names, phone numbers...
  • JSON files
  • Resources in sample data folder
    • Text
    • Color
    • Image (collections if in folder)



2017年5月25日木曜日

What's New in Android Development Tools (Google I/O '17)



Android Studio 3.0

2.4 ではなく 3.0 にした理由
  • incremental change ではないから
  • breaking gradle API change があるから

Develop

最新の IntelliJ stable 27.1 ベース

Kotlin サポート

  • Create Android Project ウィザードに Include Kotlin support チェックボックスが追加
  • 既存のプロジェクトに Kotlin ファイルを直接作成すると自動で project の dependencies が更新
  • [Code] - [Convert Java File to Kotlin File] で既存の Java ファイルを Kotlin に変換
  • show Kotlin Bytecode で Kotlin Bytecode Window を起動し、上部の Decompile ボタンで Java コードをチェック
  • Java で動く lint は Kotlin でも動く

Layout Editor

  • ConstraintLayout 1.0.0 beta1 の機能(barriers など)に対応
新しい sample リソースタイプ tools:text="@tools:sample/lorem" tools:text="@tools:sample/date_day_of_week" 表示データ用の json ファイル(hoge_log.json)を用意して tools:src="@sample/hoge_log.json/hoge/icon" tools:text="@sample/hoge_log.json/hoge/description"

Downloadable Font 対応

  • TextView を選択して Properties の fontFamily から More Fonts... を選択
  • font を検索して選択

Adaptive Icon

  • Image Assert ウィザード で Adaptive Icon に対応

Device File Explorer

  • 右下にある
  • デバイス内のファイルを見たり、アップロード、ダウンロードできる

Instant Apps

  • リファクタリングサポート
    • クラスファイルで Modularize... を起動
    • 依存クラス含めてどれを module に移動するか選択
    • 現状 initial version でまだまだ改善中らしい

Apk Analyzer

  • クラスを右クリック - Show bytecode
  • Load proguard mapping ボタンから mapping ファイルを設定
  • クラスを右クリック - Generate Proguard keep rule
  • クラスを右クリック - Find Usages

Apk Debugging

  • [File] - [Profile or Debug APK...]

Profiler

  • Android Profiler ウィンドウ
  • CPU, Memory, Network

Network Profiler

  • Network をクリック、ズームイン、ネットワークリクエストが表示される
  • ネットワークリクエストをクリックすると、詳細と Call Stack が表示される
  • HttpUrlConnection とそれをベースにしているもの(Volleyとか)、OkHttp に対応

CPU Profiler

  • Recording をクリック、アプリを操作、Stop Recording をクリック
  • Thread が表示される

Memory Profiler

  • Garbage Collection
  • Heap dump
  • Recording


Build

Google's maven Repo

  • Gradle plugin
  • Support Libraries
  • Data-Binding Library
  • Constraint Layout
  • Instant Apps Library
  • Architecture Compoennts
  • Android Testing Libraries
  • Espresso
repositories{ maven {url 'https://maven.google.com'} or google() // 4.0-milestone-2 or later }

Build Performance

  • Incremental Tasks
    • Resource Processing(planned for 3.0)
    • Shrinking (experimental, 2.3)
    • Java Compilation (Gradle 3.5)
    • Dexing (3.0)
    • Annotation Processor x
    • APK Packaging (2.2)

Build Cache

  • Local
    • Switch between branches without recompiling -- build-cache org.gradle.caching=true
  • Distributed
    • Share with team members And Build Server
    • Hazelcast implementation Gradle Enterprise API for custom backend

Multi-modules Advantages

  • Code Reuse
  • Improve Caching
  • API/Dependency Control
  • Compilation Avoidance
複数のモジュールでの Parallel Build に取り組んでいる

Support for Java 8 Language Features

android { compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } defaultConfig.jackOptions.enabled=true は廃止

Dependency Management

  • いままでは依存ライブラリ(module) の release build が利用されていた
  • 手動で buildType を合わせる方法もあったが割と設定が大変
  • 3.0 では新しい gradle の API によって同じ名前の buildType や flavor があれば自動で合わせてくれるようになった
  • ライブラリに flavor があってアプリ側に対応する flavor がない場合のために、新しい flavorSelection が追加
  • flavor が一つでも flavorDimension が必須に
アプリ側 android { flavorSelection 'color', 'blue' } ライブラリ側 android { flavorDimension 'color' productFlavors { blue {} orange {} } }

Instant Apps

  • feature module (com.android.feature) に分割
  • feature module を組み合わせて Instant-app (com.android.instantapp) を作る


Test

Android Emulator

  • Google Play Store が搭載された Google Play Edition が増えた
  • Open GL ES 3.0
  • Proxy Support

  • Android Open Source Project
    • x : Google APIs
    • x : Google Play
    • o : Elevated Privileges
  • Google Play Edition
    • o : Google APIs
    • o : Google Play
    • x : Elevated Privileges (root access など無し)

App Bug Reporting

  • エミュレータからスクリーンショットなど情報つきでバグレポートをQAチームなどに簡単に送れる

Android Wear Emulator

  • Emulator Rotary Input support

Layout Inspector

  • いろいろ改善した

Optimize

  • Android Profiler (CPU, Memory, Network)
  • APK Analyzer
  • WebP support



2017年5月23日火曜日

What's New in Android Support Library (Google I/O '17)

  • v26.0.0-beta1 の話
  • support library の minSdkVersion が 14 になった
  • メソッドやクラスを削減して、メソッドカウントが約1.4k減った
  • 今後より多くメソッドやクラスを削減したいので 約 30 classes / interfaces, 約 400 メソッドが deprecated になった
    • later version で削除される予定
  • Google Maven Repository で配布されるようになった
    • Constraint Layout Library や Architecture Components Library も含まれる
    • 過去の Support Library version (13.0.0 〜) も含まれる
repositories{ maven { // Google Maven Repository url "https://maven.google.com" } } dependencies { compile "com.android.support:appcopmat-v7:26.0.0-beta1" }

XML Font (14+)

  • font を xml で指定できるようになった
  • res/font/font1.ttf, res/font/xml_font.xml
  • font-family で font をグループ化
res/font/myfont.xml <?xml version="1.0" encoding="utf-8"?> <font-family xmlns:app="http://schemas.android.com/apk/res-auto"> <font app:fontStyle="normal" app:fontWeight="400" app:font="@font/myfont_regular" /> <font app:fontStyle="normal" app:fontWeight="800" app:font="@font/myfont_bold" /> </font-family> <TextView ... android:fontFamily="@font/myfont" android:textStyle="bold" /> Typeface typeface = ResourceCompat.getFont(context, R.font.myFont); textView.setTypeface(typeface);

Downloadable Fonts(14+)

  • 今までもフォントファイルをアプリの中に持って使うことができたがアプリサイズが大きくなる要因
  • Font Provider は font を fetch し、cache し、アプリが必要なフォントを返す
  • 複数のアプリから単一のエントリーポイント(FontsContractCompat)を経て Font Provider にアクセスする
    • 複数のアプリでメモリを節約できる
  • Font Provider は Google Play Services を介して 800+ の Google Fonts を利用できる
FontRequest request = new FontRequest( "com.example.fontprovider.authority", "com.example.fontprovider", "Name fo font", R.array.certs); FontsContractCompat.FontRequestCallback callback = new FontsContractCompat.FontRequestCallback() { @Override public void onTypefaceRetrieved(Typeface typeface) {} @Override public void onTypefaceRequestFailed(int reason) {} }; FontsContractCompat.requestFont(context, request, callback, new Handler()); xml/font/downloadedfont.xml <font-family xmlns:app="http://schemas.android.com/apk/res-auto"> app:fontProviderAuthority="com.example.fontprovider.authority" app:fontProviderPackage="com.example.fontprovider" app:fontProviderQuery="dancing-script" app:fontProviderCerts="@array/certs"> </font-family>
  • これをレイアウトに指定したら、フォントを fetch して適用するまでやってくれる
  • 適用できるまでの間はデフォルトのフォントで表示される
  • Android Studio で google fonts からフォントを選択できるようになった(3.0)
    • 自動で downloadedfont.xml が作られる

Emoji Compatibility Library (19+)

tofu 問題を解決するぞ dependencies { compile "com.android.support:support-emoji:${version}" } FontRequest fontRequest = new FontRequest( "com.example.fontprovider", "com.example", "emoji compat Font Query", CERTIFICATES); ); EmojiCompat.Config config = new FontRequstEmojiCompatConfig(this, fontRequest); EmojiCompat.init(config);
  • Google Play Service がないデバイスをターゲットにするには
    • Bundled configuration - 7MB
dependencies { compile "com.android.support:support-emoji-bundled:${version}" } EmojiCompat.Config config = new BundledEmojiCompatConfig(this); EmojiCompat.init(config);
  • android.support.text.emoji.widget.EmojiTextView, EmojiEditText, EmojiButton
    • 自動で Emoji Compat を利用して Emoji を表示する

Autosizing TextView

<TextView ... app:autoSizeTextType="uniform" /> より細かく指定したい場合
xml で定義したサイズの中から一番合うサイズを選択してくれる <TextView ... app:autoSizeTextType="uniform" app:autoSizePresetSizes="@array/autosize_sizes" /> min, max を指定する場合 <TextView ... app:autoSizeTextType="uniform" app:autoSizeMinTextSize="12sp" app:autoSizeMaxTextSize="100sp" app:autoSizeStepGranularity="2sp" />

DynamicAnimation (16+)

final SpringAnimation anim = new SpringAnimation( bugdroidImageView, // object to animate TRANSLATION_Y, // property to animate 0); // equilibrim state anim.getSpring() .setDampingRatio(0.7f /* lower is more bouncy */) .setStiffness(1500f /* higher oscillates faster */) anim.setStartVelocity(velocityTracker.getYVelocity()) .start();

Vector Drawable Compat - FillType (14+)

  • android:fillType
  • Determines "inside" of shape
  • Corresponds to SVG's fill-rule
  • Commonly used by vector drawing tools

Animated Vector Drawable Compat - pathData morphing (14+)

  • Animate <vector> android:pathData attribute
  • Set valueFrom, valueTo using VectorDrawable path data
  • Path formats must match
res/drawable/buffalo.xml <vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="600dp" android:width="320dp" android:viewportHeight="600" android:viewportWidth="320"> <group> <path android:name="buffalo_path" android:pathData="@string/buffalo" /> </group> </vector> res/anim/buffalo_to_hippo.xml <objectAnimator xmlns:android="http://schemas.android.com/apk/res/android" android:duration="1000" android:propertyName="pathData" android:valueFrom="@string/buffalo" android:valueTo="@string/hippo" android:valueType="pathType" /> res/drawable/animal_morph.xml <animated-vector xmlns:android="http://schemas.android.com/apk/res/android" android:drawable="@drawable/buffalo"> <target android:name="buffalo_path" android:animation="@anim/buffalo_to_hippo" /> </animated-vector> aapt を使う res/drawable/animal_morph.xml <animated-vector xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android"> <aapt:attr name="android:drawable"> <vector android:height="600dp" android:width="320dp" android:viewportHeight="600" android:viewportWidth="320"> <group> <path android:name="buffalo_path" android:pathData="@string/buffalo" /> </group> </vector> </aapt:attr> <target android:name="buffalo_path" android:animation="@anim/buffalo_to_hippo" /> </animated-vector> 全部を1つのファイルにする res/drawable/animal_morph_bundle.xml <animated-vector xmlns:aapt="http://schemas.android.com/aapt" xmlns:android="http://schemas.android.com/apk/res/android"> <aapt:attr name="android:drawable"> <vector android:height="600dp" android:width="320dp" android:viewportHeight="600" android:viewportWidth="320"> <group> <path android:name="buffalo_path" android:pathData="@string/buffalo" /> </group> </vector> </aapt:attr> <target android:name="buffalo_path"> <aapt:attr name="android:animation"> <objectAnimator android:duration="1000" android:propertyName="pathData" android:valueFrom="@string/buffalo" android:valueTo="@string/hippo" android:valueType="pathType" /> </aapt:attr> </target> </animated-vector>

<pathInterpolator>

  • Parity with platform AVD
  • <objectAnimator> で利用 android:interpolator
  • Used VectorDrawable (SVG-like) path data
<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android" android:pathData="M 0.0, 0.0 c 0.08,0.0 0.04,1.0 0.2,0.8 l 0.6,0.1 L 1.0,1.0" />

Wear

New support-wear module

TV

Leanback

PreferenceDataStore

PreferenceDataStore
  • preference storage mechanism のカスタマイズを可能に
class CloudDataStore extends PreferenceDataStore { @Override public void putBoolean(String key, boolean value) { // cache value immediately, launch async task to persist // data to cloud service. } @Override public void getBoolean(String key, boolean defValue) { // Return cached value. return false; } } メソッドは Main スレッドで呼ばれるので注意 // Set up this PreferenceFragment to store // and retrieve data using CloudDataStore. PreferenceManager prefManager = getPreferenceManager(); CloudDataStore cloudStore = new CloudDataStore(); prefManager.setPreferenceDataStore(cloudStore);

FragmentManager

executePendingTransaction(), commitNow(), and similar transaction calls are no longer allowed during FragmentManager state changes.

FrameMetricsAggregator

  • FrameMetricsAggregator
  • Performance monitoring tool used to capture a variety of information about Activity drawing.

ActionBarDrawerToggle

setDrawerSlideAnimationEnabled() メソッドで Navigation drawer のアイコンのアニメーションを無効化できる