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 されます。