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