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 のアイコンのアニメーションを無効化できる


2017年5月5日金曜日

android コマンドから sdkmanager に移行

android コマンドは廃止になりました。
https://developer.android.com/studio/releases/sdk-tools.html の SDK Tools, Revision 25.3.0 (March 2017)

代わりに sdkmanager コマンドを使います。
使い方は
https://developer.android.com/studio/command-line/sdkmanager.html $ sdkmanager "build-tools;25.0.0" のように使います。 複数のパッケージをファイルに羅列して $ sdkmanager --package_file=file のように指定することもできます。 file の中身はこんな感じ platform-tools build-tools;25.0.0 platforms;android-24 platforms;android-25 extras;android;m2repository extras;google;m2repository extras;google;google_play_services extras;m2repository;com;android;support;constraint;constraint-layout-solver;1.0.2 extras;m2repository;com;android;support;constraint;constraint-layout;1.0.2

ライセンスの accept はどうするかというと、
https://developer.android.com/studio/intro/update.html#download-with-gradle にあるように、
ローカルで許可したあとの
[sdk dir]/licenses/android-sdk-license
ファイルをビルドサーバーにコピーしておきます。


2017年5月2日火曜日

Android UI Test : Dialog を表示する

@RunWith(AndroidJUnit4.class) @LargeTest public class ShowDialogUiTest { // Activity は起動できて副作用がないやつならなんでもよい @Rule public ActivityTestRule mActivityRule = new ActivityTestRule<>(LoginActivity.class); @Test public void showDialog() throws Throwable { mActivityRule.runOnUiThread(new Runnable() { @Override public void run() { // Dialog は Window token のある context が必要なので Context として Activity を使う new AlertDialog.Builder(mActivityRule.getActivity()) .setTitle(R.string.title) .setMessage(R.string.message) .setPositiveButton(android.R.string.yes, null) .setNegativeButton(android.R.string.no, null) .setCancelable(false) .show(); } }); onView(withText(R.string.message)).check(matches(isDisplayed())); } }
ActivityTestRule の対象にする Activity は「androidTest に DummyActivity を用意する」みたいな方法をとりたかったんだけど、アプリとテストで apk が分かれるので無理そうだ ↓ http://stackoverflow.com/questions/36276909/create-dummyactivity-inside-androidtest-folder-for-testing


2017年4月5日水曜日

DrawerLayout を使った画面で StatusBar の色を動的に変える

結論

DrawerLayout の setStatusBarBackground()setStatusBarBackgroundColor() を使う。


補足

DrawerLayout では android:fitsSystemWindows="true" がセットされている場合、自分で StatusBar 部分を描画します。 public class DrawerLayout extends ViewGroup implements DrawerLayoutImpl { ... private Drawable mStatusBarBackground; ... public DrawerLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); ... if (ViewCompat.getFitsSystemWindows(this)) { IMPL.configureApplyInsets(this); mStatusBarBackground = IMPL.getDefaultStatusBarBackground(context); } ... } ... @Override public void onDraw(Canvas c) { super.onDraw(c); if (mDrawStatusBarBackground && mStatusBarBackground != null) { final int inset = IMPL.getTopInset(mLastInsets); if (inset > 0) { mStatusBarBackground.setBounds(0, 0, getWidth(), inset); mStatusBarBackground.draw(c); } } } } そのため Window.setStatusBarColor() で色を変えようとしても意図したようになりません。DrawerLayout のメソッドを使いましょう。


2017年3月14日火曜日

アプリの Notification がブロックされているかどうかを取得する

結論

NotificationManagerCompat.areNotificationsEnabled()

を使う。

補足

フレームワークに追加されたのは API Level 24 なので、KitKat (API Level 19〜)対応するなら NotificationManagerCompat を使う。
NotificationManager.areNotificationsEnabled()

NotificationManagerCompat では、 KitKat 〜 に対応するため NotificationManagerCompatKitKat に処理がバックポートされている。



Settings - Apps - App Info - Notifications で行ける画面で、矢印の Block all にチェックすると areNotificationsEnabled() の戻り値が false になる。

2017年3月9日木曜日

ドメイン駆動設計について DroidKaigi 2017 で登壇しました。

スライドだけ公開しても伝わらないと思うので、スピーチ原稿と合わせて公開します。
すごく長いです。


本日はドメイン駆動設計というものについて、Androidアプリ開発と絡めてお話させていただきます。
私あんざいゆきと言います。Androidアプリの開発をなりわいとしております。
長らく Y.A.Mの雑記帳というブログでAndroidの技術情報を発信しています。最近はなかなか投稿できなくなってしまいましたが、それも仕事としてAndroidに関われているためです。Androidを触り始めたころはまだ学生だったので時間があったんでしょうね。
はじめて Android に関するエントリを投稿したのは 2009 年 5 月 24 日です。当時はJavaFXを触っていたので、NetbeansでAndroidをやろうとしていたようです。
次の日にはEclipseに乗り換えるんですけどね。やっぱりAndroidアプリを開発するならEclipseですよねー。
当時のAndroidのバージョンは1.5、Fragment もなく、Support Library もなく、マルチタッチすらなく、ストアは Google Play ではなく Android Market という名前でした。
ここから2、3年くらいは、仕事でAndroid アプリを開発している人はもっぱらメーカーのプリインアプリを作っている方たちで、多くの人は趣味でAndroidアプリを開発して、マーケットに公開していました。
彼ら、彼女らが作るアプリはほとんどが無料で、ユーザーは開発者自身、自分が欲しいもの、自分にとって便利なものをみな作っていました。
こういったものは大抵がシンプルな単機能のもので、継続的にメンテナンスすることを考えずに作られています。
当時趣味でアプリを作っていた人で、半年後や1年後に自分のコードをみて、何をしているのかわからない、という状況になった方はいらっしゃるのではないでしょうか。私もその一人です。
さらに趣味アプリの場合、コードをいじるのは自分だけです。今ほど github を使っている人もいませんでしたし、趣味アプリの場合、OSS として github 上に公開してもコミットするのは自分だけという状況は多いのではないでしょうか。
このように規模が小さくシンプルなアプリで、作って公開したら終わり、しかも作り手は自分だけ、という状況では設計手法なんて考えなくてよかったのです。設計にそれほど注意を払わなくても作成できますから。
さて、その後 Android は破竹の勢いで世界を席巻しました。スマートフォンも普及し、今ではあるのが当たり前、対応するのが当たり前のプラットフォームです。
スマートフォンを対象に新たにサービスを立ち上げたり、すでに展開しているビジネスのクライアントとしてアプリを開発するというのは、趣味アプリとは前提が異なります。
まず、サービスとしてさまざまな機能を提供するため、アプリが複雑になります。画面数も多く、さまざまなことに注意を払う必要があります。
次に作って終わりではありません。継続的にメンテナンスし、ビジネス要件の変化に対応し、機能の追加や変更が必要です。ランタイムパーミッションに対応するなど、プラットフォームの進化にも追随しなくてはいけません。
ライバルのアプリに新しい機能が追加されたら、うちのアプリにも入れろと言われ、あれやこれやの施策をやりたいと言われ、しかも早くやれと言われる。開発するほうは大変です。
さらに、作り手は自分だけではありません。複数の開発者がアプリのさまざまな部分に手を入れます。人が入れ替わることもあるでしょう。
他の人が書いたところをすべて読まなくては機能変更ができない、という状況では困ります。
我々のアプリは危機に瀕している。
機能を満たすだけのコードが無秩序に積み上がった状態はまるでハウルの動く城。
あやまってネジを抜いてしまったら、全てが瓦解しかねない。
デグレの嵐、燃えるユーザーレビュー。
おそろしい。
よくわからないコードをいじる時はおそるおそる。すごく時間がかかる...
このままではいけない。

どうすれば、複数人で複雑なアプリを継続的に、安定して素早く開発していけるのか。

なんとかアーキテクチャや、なんたらパターンがあふれているけれど、どれもしっくりこない。 うまくいっているという話もあまり聞こえてこない。 もし技術的なパターンを適用するだけでうまくいくなら、今頃みんなそれをやっているはず。でも、そんなふうにはなっていない。 表面的な技術的パターンではなく、もっと本質的なことを考えるべきではないのか。

そのヒントを探してドメイン駆動設計の本を手に取りました。
最初に読んだのが「エリック・エヴァンスのドメイン駆動設計」です。
ドメイン駆動設計の本といえばこれですね。
すごく分厚いです。3cmあります。
一人で読み切るのは大変だと思います。私は友人と読書会をすることで通読できました。

チームでDDDに取り組もうとするときには、少なくとも一人はこの本を読み切った人がいるべきだと思っています。 実は、この本は読みながら出てきた内容を順番にチームで取り組んでいけば良い、という構成ではありません。最後の方、第4節、14章に大事なことが書いてあるんです。
次に読んだのが「実践ドメイン駆動設計」です。
こちらのほうが説明の日本語がわかりやすいかと思います。

この本は実践とついているだけあって、チームでDDDに取り組むにはどうやっていけばよいかという視点から書かれています。

先ほど言及したエリック・エヴァンスの本の14章で扱っている内容は、2章、3章で取り上げられています。

「実践ドメイン駆動設計」のほうが、チームでDDDを学びながら順次取り組んでいきたい、という要求に的しています。
ただし、この本ではエリック・エヴァンスの本を読んでいる前提で詳細な解説がされていないところもありますから、結局は2冊とも読むべきだと思います。

「ドメイン駆動設計をどうAndroidアプリ開発に取り入れるか」が、このセッションの主題ですが、その話をするにはまずドメイン駆動設計とはなんなのかの話をしなければいけません。
ドメイン駆動設計、Domain Driven Design、という言葉を聞いて最初に思ったことは「ドメイン」とはいったいなんだろう?でした。 インターネット上の住所などと言われるドメイン名のドメインのことではありません。

本には次のように書かれています。
私たちが作るアプリにもユーザーがそれを適用する対象領域があります。
例えば料理のレシピを検索するアプリはどうでしょうか。
ユーザーはこのアプリをどのような領域に適用するのか。つまりこのアプリで何をするのか。アパートを検索するわけではありませんね。料理のレシピを検索します。つまりこのアプリのドメインは料理のレシピ、または料理そのものと言えます。
アパートを検索するなら、別のアプリを使いますね。ユーザーは建物が欲しいわけではなく、住むところを探したいわけですから、このようなアプリのドメインはアパートではなく、住まいでしょう。住むということとも言えそうです。
UberやLyftはどうでしょうか。適用する対象領域は「移動」と言えるでしょう。海を渡るためには使いませんからドメインは「近距離移動」でしょうか。

自分のアプリやサービスのドメインについて考えてみてください。

ドメインについて考えると、そこには実際に存在するものから概念的なものまでさまざまなものが含まれます。例えば、食材や調理器具や間取りや車など。
githubのようなソースコードを管理するサービスのドメインにはソフトウェアという概念が含まれるでしょう。

ドメイン駆動設計でのドメインとは何かがわかってきました。
すると次の疑問は「ドメイン駆動で設計するとはどういうことか?何をするのか?」です。
まずドメインがあります。
次にドメインエキスパートという人が登場します。
また知らない単語が出てきました。ドメインエキスパートってなんでしょう?
端的にいうとドメインに詳しい人です。ドメインが何かは先ほど話しましたね。アプリによってドメインは異なりますから、アプリによってドメインエキスパートも異なります。
エキスパートとついているので専門家じゃないといけないと思うかもしれませんが、そうではありません。ドメインについて自分より詳しい人はみなドメインエキスパートです。
ユーザーだったり、同僚だったり、さまざまです。
ドメインエキスパートが何かわかったので、ドメイン駆動設計で何をするのかに戻りましょう。
ドメインエキスパートの頭の中には、ドメインを構成する物や振る舞い、関係性などの概念的な何かがあります。

例えば料理をするとはどういうことですかと聞くと、「材料を用意して、それぞれの分量を計り、手順に沿って調理していく」ことです。のような答えが出てくるでしょう。

なにやら「材料」というものがあって、それが複数必要なようだ。材料には「分量」というものがあって、「分量」というのは測るものらしい。さらに「手順」というものがあって「調理」というものをするらしい、と。
頭の中は直接見えませんから、ドメインエキスパートに質問したり話を聞いたり互いに協力して、ドメインエキスパートの頭の中にある概念的な何かをうまく取り出し、解釈し、蒸留し、我々のソフトウェアに役立つモデルとして作り上げます。
そうして作り上げたものがドメインを反映したモデル、ドメインモデルと呼んでいるものです。

ドメインモデルは図を書いて説明したり、文章で説明したりできますが、図それ自体がドメインモデルである、というわけではありません。 あくまで頭の中の共通概念としてモデル化したものです。
ドメインエキスパートと話をしていると、ドメインを構成する言葉が見つかります。
先ほどの例だと「材料」や「分量」や「調理」という単語や「材料を用意する」「分量を計る」などのフレーズです。ドメイン駆動設計ではこれらドメインを構成する言葉をユビキタス言語といいます。
ユビキタスは、日本語で「どこにでも存在する」というような意味ですが、ドメイン駆動設計ではユビキタス言語として見つけ出した単語やフレーズをあらゆる場所で使います。ドキュメントはもちろん、会議での会話、ドメインモデル、そしてそれを実装するコードにも使います。

ドメインエキスパートを含めたチーム全員が同じユビキタス言語を使います。

そのため、何をユビキタス言語とするかはドメインエキスパートを含めたチーム全員で議論して合意の上で決めます。
ユビキタス言語はドメインエキスパートと開発者、そしてチームの共通言語です。単なる用語集ではありません。言葉は進化し、育っていくものです。


「開発者とドメインエキスパートが協力して、ドメインを構成するユビキタス言語を確立し、それを使ってドメインを反映したモデルを作り上げる」というところまできました。
ドメインモデルを作ったら、それを正確にコードで表現するように実装します。

ドメイン駆動設計の利点としてよく、コードが設計であり、設計がコードであるという点が挙げられますが、コードがドメインモデルを正確に表現していれば、それはすなわち設計である、ということです。
共通の概念モデルとしてドメインモデルを作り、それをコードで正確に表現するよう実装するので、他の人にとっても誤解しにくいコードになります。

じゃあ最初にドメインモデルを全部作り上げ、それから一気に実装すればいいのかというと、そうではありません。
最初から完璧なドメインモデルを作ることは不可能です。実装してみると、このモデルだとうまく実装できない、ということが判明したり、実装の途中でもっとよいモデルを思いつくことはよくあります。


そのためドメイン駆動設計では、ドメインモデルを作り、実装してみて、その結果をフィードバックし、ドメインモデルを修正したり変更したり、まったく新しいモデルを作ったり、そしてそれをまた実装する。これを繰り返してドメインモデルとコードの両方を洗練させていきます。
つまり、アジャイル的なプロセスを前提としています。
まとめると、

まず、ドメインエキスパートの言葉を観察し、ドメインを構成するユビキタス言語を見つけます。
次にユビキタス言語を使ってドメインを適切に反映した、我々のソフトウェアに役立つドメインモデルを作ります。
そして、作ったドメインモデルを正確に表現するようコードを実装し、これを繰り返します。

ドメインモデルを作ってから実装です。いきなり実装ではありません。
やることはわかった。でも実際やるのは難しい。

そこで、ドメイン駆動設計では、実践するために役立つさまざまな手法が出てきます。これらは主に2つに分けることができます。
戦略的設計と戦術的設計です。
ドメインモデルを作り上げるために役立つ手法が戦略的設計
ドメインモデルからそれを表現した実装を行っていくのに役立つ手法が戦術的設計です。
今までの話にでてきたユビキタス言語は戦略的設計です。他にも境界づけられたコンテキストやコンテキストマップがあります。

一方の戦術的設計には値オブジェクトやエンティティ、サービスなどがあります。名前を聞いたことがあるという方もいるでしょう。他にも集約やドメインイベントやリポジトリなどもあります。

このようなコーディングにおける技術的な手法は理解しやすいため、ここだけを取り入れてみました、という話がよくありますが、これは完全なDDDではありません。
なぜか。ドメインモデルがないからです。
さて、ドメイン駆動設計が何であるかの話をしてきました。

ここでちょっとドメイン駆動設計が何でないかの話をしたいと思います。
「DDDってClean Architecture のことでしょ?」

違います。Clean Architecture を知らないという人は気にしなくてよいです。
もちろんDDDとClean Architectureを組み合わせて使うということは可能ですし、Clean Architecture のコンセプトはDDDの影響を受けている、参考にしているということはあるでしょう。しかし、ここで言いたいのは DDD = Clearn Architecture ではない、ということです。
「DDDって MVC とか MVP とかの仲間でしょ?」

これも違います。今までの話の中でビューはでてきましたか?出てきてませんね。我々が話してきたのはドメインモデルについてだけです。
「レイヤ化アーキテクチャにすればDDDだよね?」

エリック・エヴァンスの本でレイヤ化アーキテクチャが紹介されているからか、DDD=レイヤ化アーキテクチャにすること、のように勘違いしているのを見かけることがあります。ドメイン駆動設計とアーキテクチャの関係はこのあと取り上げますが、ドメイン駆動設計は特定のアーキテクチャに依存しているものではありません。
「ドメインモデルは作ってないけど技術的なパターンを真似したからDDDだよね?」

ドメイン駆動設計ではドメインモデルを作ってそれを正確に表現するようにコードを実装することであって、特定の技術的パターンをとることではありません。
「ドメインモデルを作ってそれを正確に表現するように実装したからDDDだよね?」

これが正解です。

簡単に言うけど、やるのは難しいんだよね。
そろそろAndroidの話をしましょうか。

Androidアプリ開発でドメイン駆動設計に取り組む場合も同じです。ドメインエキスパートの話を聞いて、概念を適切に反映するドメインモデルを作って、それを正確に表現するよう実装する、これを繰り返します。

そうは言われてもどこから手をつけたらいいかわからない。
アプリにはいろいろ機能があるし、どれをやるべきなの?
どこから手をつけるべきか、それを知るにはアプリの全体像、地勢を把握する必要があります。
そのためにドメイン駆動設計で登場するのが境界づけられたコンテキストとコンテキストマップです。

また知らない単語がでてきました。
境界づけられたコンテキストとコンテキストマップ。

先ほど戦略的設計で名前が出てきました。この2つもドメイン駆動設計の重要な要素です。
境界づけられたコンテキスト。

境界はわかります。コンテキストとは何でしょうか。日本語ではよく文脈などと訳されますね。Androidでよく出てくるあのコンテキストではありません。

ユビキタス言語の言葉が特定の意味を持つ領域がコンテキストです。
例えばAccountという言葉があります。
この言葉がユーザー認証の文脈で語られていた場合、その意味はサービスを利用する際の利用単位のことだとわかります。
一方、銀行の文脈で語られていた場合、その意味は口座になります。
さらに、文学のもとではAccountの意味は報告書になります。
コンテキストが異なると、同じ言葉でも意味が変わります。
ユビキタス言語を構成する言葉が特定の意味を持つ領域がコンテキストであり、
境界づけられたコンテキストの内部では、ユビキタス言語を構成する言葉は特定の意味を持ちます。
ドメインモデルはユビキタス言語で構成されますから、ドメインモデルはそれを構成するユビキタス言語の境界づけられたコンテキストに属します。
では、自分のアプリの境界づけられたコンテキストをどう見つければいいのでしょうか
言語の境界がコンテキストの境界ですから、言葉の境界を探せばよさそうです。
どういうところが言葉の境界になるのでしょうか。
例えばチームが異なると、単一のユビキタス言語を維持するのは難しいでしょう。コミュニケーションにコストがかかることから、チーム内だけの言葉が発展していき、お互いの言葉が徐々に解離していきます。
いずれ、ドメインモデルを境界内で厳密に一貫性のあるものに保つことができなくなります。
特定の機能が外部のライブラリやSDKとして提供されている場合、その部分は別のコンテキストになっていることが多いです。ドメインモデルが一貫性をもつ範囲を考えると理解しやすいでしょう。
同じようにコードベースが異なる場合もヒントになります。わかりやすくいうとgithubのリポジトリが異なるのなら別のコンテキストではないか、ということです。
見つかったコンテキストには名前をつけます。
そして、その名前をユビキタス言語の一部にします。
境界づけられたコンテキストがわかってきたら、次にコンテキストマップを描きます。
コンテキストマップは、現時点の境界づけられたコンテキストと、それらがどのようにやりとりしているのかを示すものです。
大事なのは理想の姿ではなく現状の状態を描くことです。

だからといって正確に把握したものでなければならないというわけでもありません。最初はわかっている範囲で十分です。状態が変わったり、新たな知見が見つかったらその都度マップを更新しましょう。

凝ったつくりにする必要もありません。ホワイトボードに書いたものを写真で撮れば十分です。

依存する他のプロジェクトに何があって、それとどのような関係なのかをチームで共有し、考えるきっかけにします。
作ったマップはいつでも見れるところに置いておく。Activityのライフサイクルポスターの横に貼り出すなんて最高ですね。

境界づけられたコンテキストの間の関係がどのようになるのか、この関係性についてDDDには組織的なパターンや統合のパターンがいくつか紹介されています。
自分たちのコンテキスト間の関係性がこれらのパターンのどれに一番近いか考えてみましょう。
そしてコンテキストマップのコンテキスト同士をつなぐ線にどのパターンなのかを書いてみましょう。

パターンに名前がついていることは、とても重要です。後からチームに参加したひとでもコンテキストマップをみることで、どの依存プロジェクトが協力的で、どこが融通がきかないのか把握することができます。

いくつかのパターンをAndroidアプリ開発でありそうな状況にあてはめてみます。
パートナーシップというのは、成功・失敗の運命を共にする関係です。例えばアプリの主要な機能がすべてそのアプリ用のSDKとして提供されている場合、これらの運命は一蓮托生であり、パートナーシップの関係が一番近いでしょう。
特定の機能を社内SDKとして提供している場合はどうでしょうか。同じ社内ですから、アプリ側のニーズに対応してくれるかもしれない。もし対応してくれるような関係性であるなら、それは顧客/供給者の関係が一番近いでしょう。
3rd party が提供しているSDKではどうでしょうか。例えば twitter や facebook SDK など。この場合我々は提供されているSDKをそのまま利用するしかありません。このような関係は順応者になります。 社内SDKであっても、アプリ側のニーズに対応してくれない関係性なら順応者になります。
公開ホストサービスについてドメイン駆動設計の本には次のように書かれています「サブシステムにアクセスできるようにするプロトコルを、サービスの集合として定義すること。そのプロトコルを公開し、サブシステムと統合する必要のある人が全員使用できるようにすること。」

よくわからないですね。ようはこういうことです。

あるサブシステムとやりとりしたい人が複数います。
それぞれに個別に対応するのは大変なので外部に方法を公開するよということです。方法はRESTかもしれないしRPCかもしれない。
公開ホストサービスと一緒に使われることが多いのが公表された言語です。
わかりやすく言うと、XMLとかJSONとかProtocol Buffer とか、ようは形式が公表されている言語です。
さらに一緒に取り入れることが多いのが腐敗防止層です。
別のコンテキストのモデルによって自分のドメインモデルが汚染されないように、必要に応じて自分のコンテキスト内のモデルに変換します。なので、この変換はコンテキストの外側にあることになります。
コンテキストマップに描くとこのようになります。
OHS は公開ホストサービス、PL は公表された言語、ACL は腐敗防止層です。

公開ホストサービスに限らないのですが、サーバー上のサービスが提供しているコンテキストとアプリのコンテキストが異なるのであれば、モデルの変換が必要になります。
実践ドメイン駆動設計では次のような例があります。本ではXMLですが、ここではJSONに置き換えました。

別のコンテキストの userInRole をそのまま利用側のコンテキスト内で使うのではなく、利用側のコンテキストに存在するドメインモデルである Moderator に変換して利用します。
さて、我々のアプリのドメインが何か考えました。
ユビキタス言語がDDDを行っていくうえで重要な要素であることを理解しました。
境界づけられたコンテキストを見つけ、コンテキストマップを描きました。

次に考えることはなんでしょうか?
実践ドメイン駆動設計では、コンテキストマップの次の章はアーキテクチャです。
ドメイン駆動設計でアーキテクチャというと、レイヤ化アーキテクチャのことがよく出てきます。エリック・エヴァンスの本で紹介されているからか、ドメイン駆動設計ではレイヤ化アーキテクチャを使わなければならない、とか、ドメイン駆動設計はレイヤ化アーキテクチャにすることだ、のような誤った認識をときどき見かけます。
ドメイン駆動設計は特定のアーキテクチャに依存するものではありません。
ではレイヤ化アーキテクチャを取り上げた理由はなぜか、何をしたいのか、
それはドメイン層を隔離することです。
ここでいうドメイン層というのは、ドメインモデルの集まり。正確にいうとドメインモデルを表現したコード、実装の集まりです。
ドメインにある概念や知識、ビジネスロジックとも表現しますが、これをドメインモデルとしてその他から隔離するということです。
Androidアプリ開発において、本来ドメインモデルとして隔離すべき、ドメインにある概念や知識、ビジネスロジックが混入しがちなのが、ユーザーインタフェースです。

なぜ我々はユーザーインタフェースに、ドメインにある概念や知識、ビジネスロジックを詰め込んでしまうのか。
それはアプリの作り方に深く関わっています。
エリック・エヴァンスの本にも登場する利口なUI(スマートUI)は、ユーザーインタフェースにすべてのビジネスロジックを埋め込むパターンです。

その利点には次のように書かれています。
・単純なアプリケーションの場合、生産性が高く、すぐに作れる。
・それほど有能でない開発者でも、この方法なら、ほとんど訓練しないで仕事ができる。
・要求分析が不足していても、プロトタイプをユーザに公開し、その要望を満たすように製品を変更することで、問題を克服できる。
などなど

この利点が利点として生きる場所があります。モックやプロトタイピングです。
モックやプロトタイピングでは、画面のデザインを動くもので素早く確認するのが目的ですから、単純なビジネスロジックも含めすべてユーザーインタフェースに入れます。


つまり、動くアプリをすばやく作ってユーザーに見せて検証したい、という場合、利口なUIになりがち、ということです。


他にも、仕様が決まった後に画面デザインとちょっとした機能説明が書かれたドキュメントが来て、それをもとに実装する...
ありがちな状況ですが、画面デザインと単純な機能を短い期間で作ることを求められる状態でも利口なUIになりがちです。
そこから抜け出すにはどうしたらいいのか。

理想を言えば、機能について議論するところから開発者も参加して、みんなの頭のなかにある概念的なモデルについて観察し、ドメインモデルを作りたい。

でもいきなりそんなこと言われてもできないよ、ってなりますよね。


重要なのは利口なUIになりがちだと認識すること。そして、ドメインにある概念や知識、ビジネスロジックがUIに存在していないか観察し、ドメインモデルとしてUIから隔離できないか考えることです。

ただし注意してほしいのは、ユーザーインタフェースからビジネスロジックを単純に隠蔽することと、ドメインモデルとして隔離することは違います。
Activityとライフサイクルを同期するようにした、なんたらプレゼンターみたいな名前のクラスを作って、そこに処理を全部移譲することではありません。

大事なのはドメインモデルです。ドメインを反映したドメインモデルを作ることで、ユーザーインターフェースからビジネスロジックを引きはがせないか考えましょう。


ここまでの話を一旦まとめましょう。
まず、アプリのドメインとは何か考えました。
次にドメインエキスパートと会話をしてドメインを構成する言語を見つけ、ユビキタス言語として確立し、育てていく必要があることがわかりました。
そして、言語の境界がコンテキストの境界になり、アプリにはそれを構成する境界づけられたコンテキストが複数存在することがありえるということをみました。
コンテキスト同士の関係をコンテキストマップとして描くことで、どのような関係性がコンテキスト同士の間にあるのか把握できるようになりました。
これら戦略的設計と分類できる手法の目的は
ドメインを反映したモデル、ドメインモデルを作り上げることです。
作ったドメインモデルを正確に表現するコードとして実装し、これを繰り返してドメインモデルとコードの両方を洗練させていく。
ドメインモデルを正確にコードで表現する、言うのは簡単ですが実行するのは難しい。そこでドメイン駆動設計ではそのために役立つ技術的なパターンも紹介されていて、それが戦術的設計に分類される手法です。
アーキテクチャの話のところで出てきた「ドメインを隔離する」というのは、ドメインモデルを正確にコードで表現するために必要ですし、他にも値オブジェクトやエンティティ、サービス、リポジトリなどの手法があります。

重要なのは、これらの技術的なパターン、手法は「ドメインモデルを正確に表現したコードを実装する」ためのものです。表現したいドメインモデルがないのに、技術的なパターンを真似してもドメイン駆動設計の恩恵は限定的です。
ドメイン駆動設計について、多少でもおわかりいただけたでしょうか。

なんとなく理解したけどコードが出てこないとやっぱりよくわからない。 私もそうです。言葉だけで理解するのは難しい。

というわけで、ここからはおまけです。
ドメイン、そしてドメインモデルについて考える練習として、すごく単純な例を用意しました。
動物の体重をTextViewに表示するコードです。 体重がわからない動物のときは空表示にしたいようです。

このコードには2つ問題があります。
1つ目が、-1という数字が特別な意味をもつということをユーザーインタフェースが知ってしまっているということ。
2つ目が、マイナスの体重というものが表現されてしまっていることです。

体重というものについて考えてみてください。体重の概念にマイナスというものはありますか?私の体重マイナスですっていうかたいますかね?いたとしたら多分その人は反物質でできているんだと思います。 ここで大事なのは科学的にありえるかどうかではなく、ドメインについて考えることです。動物の体重という概念を反映したドメインモデルを考えるとマイナスという状況はないですよね。

ではどうするか、体重を反映したドメインモデルを作るんです。そのモデルは浮動小数の値を持っていて、その値はマイナスではない。いや、マイナスではないというより 0 より大きいと考えるほうが適切ですね。

ドメインモデルを考えたので実装してみましょう。

Weight オブジェクトのインスタンスから取得した体重の値は、必ず 0 より大きい。つまり体重に対するドメインモデルを表現できています。
これを利用すると先ほどのコードはこうなります。

注意してほしいのが、未入力を -1 で表すか null で表すかという話ではない、ということです。
ドメインにおける体重というものについて考え、それを反映したドメインモデルを考え、Weight クラスというドメインオブジェクトとして表現する、という話です。
今度は性別を表示するようです。

このコードにも問題がありますね。
文字列が “M” の場合がオスで”F”の場合はメスだそうです。文字列の意味をUIが知っている。なんて賢いUIなんでしょう。

なんでこんな実装になっているのか聞いてみました。
「サーバーのレスポンスが文字列だったので...」
なるほどーーーー。

UIが文字列の意味を知ってるのは変だから直してみてくれる?
「わかりました。できました。ユーティリティクラスを作って、そっちの static メソッドで判定するようにしました!」

そういうことじゃないんだ...


UIがどのユーティリティメソッドを使って判定するか知っていなければならない、結局UIが賢い問題は解決していない。

必要なのはドメイン、つまり性別について考えることです。
我々の頭のなかには性別という概念的なモデルがあるのだから、それを表現するドメインモデルを用意しましょう。
動物の性別を表現するモデルで、オス(MALE)とメス(FEMALE) がある。

このドメインモデルをコードで表現するなら enum でよさそうです。
「サーバーのレスポンスは文字列ですよ。どこで enum に変換するんですか?」

そこで出てくるのがコンテキストの統合で紹介した腐敗防止層です。

ここで、サーバーのレスポンスをアプリのコンテキスト内のドメインモデルに変換します。
長い時間お疲れ様でした。

ドメイン駆動設計について多少なりとも理解いただけたでしょうか。
難しかったかもしれませんが、
まずはドメインについて考えるところから始めてみてください。
そして、大事なのはドメインモデルです。

技術的なパターンだけとりあげたブログなどがよくありますが、表面的なことを真似しただけでうまくいくようなそんな簡単なものではありません。

例であげたようなシンプルなドメインモデルからでいいのです。ドメインを反映したモデルを作るという本質に取り組んでほしいです。
DDDは概念的な話が多くて理解するのが難しいかもしれません。理解しても実行するのは一筋縄ではいかないです。でも怖くはありません。

行き当たりばったりの設計に、とりあえず動けばいいやで作られたアプリを長年メンテナンスし機能追加するほうがよっぽど恐ろしいです。

シンプルなところからぜひドメイン駆動設計の本質を取り入れてみてください。