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 } }