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 ではプロパティの定義にカスタムアクセサを書けるので、同じキーに対する保存と読み込みを一箇所に書けて対応がわかりやすくなりますね。