Kotlin + Architecture Component + Dagger2によるAndroidアプリ設計

  • 142
    いいね
  • 0
    コメント

はじめに

Google I/O 2017でKotlinがAndroidアプリ開発における公式言語としてサポートされることになりました。
また、同時にAndroid Architecture ComponentsがAndroidのライフサイクルにおける画面回転などへの対応を容易にするライブラリとして追加され、このライブラリを用いたAndroidアプリ開発におけるおすすめの設計が提案されています。
以下のGoogleのSampleリポジトリにはその設計指針を用いたサンプルが公開されています。

https://github.com/googlesamples/android-architecture-components/tree/master/GithubBrowserSample

本記事では上記のリポジトリを参考にしながら、KotlinとArchitecture Components、そしてDagger2を用いた設計を備忘録的にまとめました。
また、現在進行系で開発しているので、何か間違っている点やご意見等あればぜひ、コメントやTwitterなどでお願いします。

Sample

記事における実装のサンプルは以下にあります。

https://github.com/satorufujiwara/kotlin-architecture-components

Android Studio3.0 Canary3以降でビルド可能です。

実装について

Kotlin

Kotlinについては私も執筆に参加させて頂いた「Kotlin入門までの助走読本」(PDF注意)を読んで頂くのが良いかと思います。

またGoogle I/O 2017のセッションの中でIntroduction to KotlinというKotlinを知って頂く上でおすすめのセッションがあり、そちらを翻訳した記事がありますのであわせてご覧ください。

build.gradle

Kotlinはbuild.gradleに以下のような記述を追加することで使うことが出来ます。

build.gradle
buildscript {
    ext.kotlin_version = '1.1.2-4'
    dependencies {
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
    }
}
app/build.gradle
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'

//...
dependencies {
    // kotlin
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version"
}

Architecture ComponentsやDagger2といったライブラリがAnnotations Processingを用いてますので、Kotlin用のkotlin-kaptも忘れずに導入します。
今回はkotlin-stdlib-jre7を用いてますが、こちらの記事によるとminSdkVersion<19ではkotlin-stdlibを用いたほうが良さそうです。

Moshi

JSONのパーサーとしてはGsonが有名ですが、今回はMoshiを用いました。
理由として、MoshiにはKotinサポートが入っています。
特にKotlinのNon-Nullなpropertyの扱いがGsonだと良くないため、Moshiをおすすめします。
上記に関してはこちらの記事に詳しく解説されています。

MoshiとMoshiのkotlinサポートは以下のように導入します。

app/build.gradle
dependencies {
    implementation "com.squareup.moshi:moshi:$moshi_version"
    implementation "com.squareup.moshi:moshi-kotlin:$moshi_version"
}

Moshiインスタンスを初期化する時に、KotlinJsonAdapterFactoryを用い、またRetrofitのConverterFactoryとしてMoshiConverterFactoryを指定します。

.data.di.DataModule.kt
@Module
class DataModule {

  @Singleton @Provides
  fun provideMoshi() = Moshi.Builder()
      .add(KotlinJsonAdapterFactory())
      .build()

  @Singleton @Provides
  fun provideRetrofit(oktHttpClient: OkHttpClient, moshi: Moshi): Retrofit
      = Retrofit.Builder()
      .client(oktHttpClient)
      .baseUrl("https://api.github.com")
      .addConverterFactory(MoshiConverterFactory.create(moshi))
      .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
      .build()
}

Databinding

DataBindingとKotlinを合わせて使うためには、com.android.databinding:compilerを導入する必要があります。
ライブラリの最新バージョンはこちらでチェックすることが出来ます。

app/build.gradle
android {
    //...
    dataBinding {
        enabled = true
    }
}

dependencies {
    kapt "com.android.databinding:compiler:2.3.3"
}

Architecture Components

Architecture Componentsを導入するにはbuild.gradleに以下のように記述します。

app/build.gradle
dependencies {
    // architecture components
    implementation "android.arch.lifecycle:runtime:$arch_version"
    implementation "android.arch.lifecycle:extensions:$arch_version"
    implementation "android.arch.lifecycle:reactivestreams:$arch_version"
    kapt "android.arch.lifecycle:compiler:$arch_version"
}

今回はRetrofitのレスポンスをRxJavaのFlowableで受け取っているため、そちらをLiveDataに変換するためのライブラリandroid.arch.lifecycle:reactivestreamsも合わせて導入しています。
このサンプルのようにRetrofitのCallAdapterFactoryとしてLiveDataに変換するためのクラスを用意すれば、RetrofitのレスポンスをそのままLiveDataに変換することも出来ます。
RxJavaへの依存をしたくない場合などはこちらが良いでしょう。

ViewModel

Architecture Componentsを導入する場合のアプリの設計は以下のようになります。
出典:Guide to App Architecture

final-architecture.png

.ui.main.MainViewModel.kt
class MainViewModel @Inject constructor(private val repository: GitHubRepository) : ViewModel() {

  val ownerId = MutableLiveData<String>()
  val repos: LiveData<List<Repo>>

  init {
    repos = ownerId.switchMap {
      if (it.isEmpty()) AbsentLiveData.create()
      else repository.loadRepos(it)
          .subscribeOn(Schedulers.io())
          .observeOn(AndroidSchedulers.mainThread())
          .onErrorResumeNext(Flowable.empty())
          .toLiveData()
    }
  }

}

ViewModelとして上記のModelViewModelクラスにMutableLiveDataとしてownerIdと、LiveDataとしてreposの2つのプロパティを定義してあります。

switchMap()android.arch.lifecycle.Transformationsクラス内に定義されているJavaのstatic methodですが、以下のようにKotlinの拡張関数で呼び出せるようにしてあります。

.util.ext.ArchitectureComponentsExt.kt
fun <X, Y> LiveData<X>.switchMap(func: (X) -> LiveData<Y>)
    = Transformations.switchMap(this, func)

同様にandroid.arch.lifecycle:reactivestreamsにはRxJavaのFlowableLiveDataに変換するためのstatic methodであるfromPublisher()が定義されているので、同様にKotlinの拡張関数で呼び出せるようにしてあります。

.util.ext.LiveDataReactiveStreamsExt.kt
fun <T> Publisher<T>.toLiveData() = LiveDataReactiveStreams.fromPublisher(this)

以上のようにして、ownerId(の中身)が変更された時に(空でなければ)、GitHubRepository.loadRepos() : Flowableが呼ばれ、repos(の中身)が変更されます。

.ui.main.MainFragment.kt
class MainFragment : Fragment(), LifecycleRegistryOwner, Injectable {

  @Inject lateinit var viewModelFactory: ViewModelProvider.Factory
  private val viewModel by lazy { ViewModelProviders.of(activity, viewModelFactory).get(MainViewModel::class.java) }
  private val binding by lazy { DataBindingUtil.setContentView<MainFragmentBinding>(activity, R.layout.main_fragment) }
  private val lifecycleRegistry by lazy { LifecycleRegistry(this) }
  private val adapter = MainAdapter()

  override fun getLifecycle() = lifecycleRegistry

  override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    binding.recyclerView.adapter = adapter
    binding.recyclerView.layoutManager = LinearLayoutManager(activity)

    viewModel.repos.observe(this) {
      it ?: return@observe
      adapter.run {
        items.clear()
        items.addAll(it)
        notifyDataSetChanged()
      }
    }
    viewModel.ownerId.value = "satorufujiwara"
  }
}

MainFragmentからはMainViewModel.reposobserveし、その内容に応じてRecyclerViewに反映しています。

MainViewModelのインスタンスはViewModelProviders.of().get()によって提供され、ViewModelProvider.Factoryのインスタンスは後述するDaggerによってInjectされています。

Fragment上でviewModelのインスタンスを取る際に、ViewModelProviders.of()の第一引数にFragment.activity(JavaではgetActivity())を与えることによって親となるActivityや同じActivityに乗る他のFragmentとインスタンスを共通化出来ます。
こちらについては以下に詳細が書かれています。

Dagger2

Dagger2を導入するにはbuild.gradleに以下のように記述します。

app/build.gradle
dependencies {
    // dagger
    implementation "com.google.dagger:dagger:$dagger_version"
    implementation "com.google.dagger:dagger-android:$dagger_version"
    implementation "com.google.dagger:dagger-android-support:$dagger_version"
    kapt "com.google.dagger:dagger-compiler:$dagger_version"
    kapt "com.google.dagger:dagger-android-processor:$dagger_version"
}

Dagger2のバージョンは2.11でDagger2の2.10から追加されたdagger.androidを利用しています。

Dagger2はActivityやFragmentにRepositoryやViewModelProvider.FactoryといったArchitecture Componentsの要素となるクラスのインスタンスをInjectする用途に用いています。

ActivityへのInject

ActivityへのInjectはdagger.android内にあるAndroidInjectionModuleクラスをアプリのComponentのmoduleとして以下のように指定します。
今回はSupport Library内にあるandroid.support.v4.app.FragmentへのInjectを利用するため、AndroidSupportInjectionModuleクラスを代わりに使用しています。

ApplicationクラスへのInjectに対応するためAppComponentAndroidInjector<Application>を継承しています。

di.AppComponent.kt
@Singleton
@Component(modules = arrayOf(
    AndroidSupportInjectionModule::class,
    AppModule::class,
    ActivityModule::class)
)
interface AppComponent : AndroidInjector<App> {

  @Component.Builder
  interface Builder {
    @BindsInstance fun application(application: App): Builder
    fun build(): AppComponent
  }

  override fun inject(app: App)
}

.di.ActivityModule.kt
@Module
internal abstract class ActivityModule {

  //...

  @ContributesAndroidInjector(modules = arrayOf(MainModule::class))
  internal abstract fun contributeMainActivity(): MainActivity

}

Activityを継承したMainActivityへのInjectは@ContributesAndroidInjectorアノテーションを用いて上記のように記述することによって、MainActivity内の必要な依存性を解決するためのコードが以下のように生成されます。

.di.DaggerAppComponent.java(Daggerによって生成される)

public final class DaggerAppComponent implements AppComponent {

private Provider<
          Map<Class<? extends Activity>, Provider<AndroidInjector.Factory<? extends Activity>>>>
  private Provider<DispatchingAndroidInjector<Activity>> dispatchingAndroidInjectorProvider;

  private void initialize(final Builder builder) {

    this.mainActivitySubcomponentBuilderProvider =
        new dagger.internal.Factory<
            ActivityModule_ContributeMainActivity$app_debug.MainActivitySubcomponent.Builder>() {
          @Override
          public ActivityModule_ContributeMainActivity$app_debug.MainActivitySubcomponent.Builder
              get() {
            return new MainActivitySubcomponentBuilder();
          }
        };

    this.bindAndroidInjectorFactoryProvider = (Provider) mainActivitySubcomponentBuilderProvider;

      mapOfClassOfAndProviderOfFactoryOfProvider;
    this.mapOfClassOfAndProviderOfFactoryOfProvider =
        MapProviderFactory
            .<Class<? extends Activity>, AndroidInjector.Factory<? extends Activity>>builder(1)
            .put(MainActivity.class, bindAndroidInjectorFactoryProvider)
            .build();

    this.dispatchingAndroidInjectorProvider =
        DispatchingAndroidInjector_Factory.create(mapOfClassOfAndProviderOfFactoryOfProvider);
    //...
  }
}

上記コード内に出てくるDispatchingAndroidInjector<Activity>を保持するためにApplicationクラスがHasActivityInjectorインタフェースを実装する必要があります。

今回は既にHasActivityInjectorの実装がなされているDaggerApplicationクラスを継承して実装しています。
このクラスは同名のクラスがdagger.android.DaggerApplicationdagger.android.support.DaggerApplicationの2つ用意されているので注意が必要です。
今回はSupport Libraryに対応した後者のクラスを継承しています。

App.kt
class App : DaggerApplication() {

  override fun applicationInjector() = DaggerAppComponent.builder()
      .application(this)
      .build()

  override fun onCreate() {
    super.onCreate()
    applyAutoInjector()
  }

}

DaggerApplicationは以下のようにAndroidInjector<? extends DaggerApplication>を返すabstract methodを持ってますので、DaggerAppComponentbuild()して実装します。

dagger.android.support.DaggerApplication.java
public abstract class DaggerApplication extends dagger.android.DaggerApplication implements HasSupportFragmentInjector {
  @Inject
  DispatchingAndroidInjector<Fragment> supportFragmentInjector;

  public DaggerApplication() {
  }

  protected abstract AndroidInjector<? extends DaggerApplication> applicationInjector();

  public DispatchingAndroidInjector<Fragment> supportFragmentInjector() {
    return this.supportFragmentInjector;
  }
}

以上のようにすることで、MainActivity.onCreate()内でAndroidInjection.inject(this)を呼出すことで必要なInjectが出来ます。

今回は以下のようなApplication.ActivityLifecycleCallbacksを登録する拡張関数用いて、Injectableインタフェースを実装したクラス内においてはAndroidInjection.inject(this)の記述を不要にしています。

.di.AutoInjector.kt
interface Injectable

fun Application.applyAutoInjector() = registerActivityLifecycleCallbacks(
    object : Application.ActivityLifecycleCallbacks {

      override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
        handleActivity(activity)
      }

      override fun onActivityStarted(activity: Activity) {

      }

      override fun onActivityResumed(activity: Activity) {

      }

      override fun onActivityPaused(activity: Activity) {

      }

      override fun onActivityStopped(activity: Activity) {

      }

      override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle?) {

      }

      override fun onActivityDestroyed(activity: Activity) {

      }
    })

private fun handleActivity(activity: Activity) {
  if (activity is Injectable || activity is HasSupportFragmentInjector) {
    AndroidInjection.inject(activity)
  }
  if (activity is FragmentActivity) {
    activity.supportFragmentManager.registerFragmentLifecycleCallbacks(
        object : FragmentManager.FragmentLifecycleCallbacks() {
          override fun onFragmentCreated(fm: FragmentManager, f: Fragment, s: Bundle?) {
            if (f is Injectable) {
              AndroidSupportInjection.inject(f)
            }
          }
        }, true)
  }
}

FragmentへのInject

FragmentへのInjectはActivityと同様に@ContributesAndroidInjectorアノテーションを用いて記述することで必要なコードが生成されます。

.ui.main.di.MainModule.kt
@Module
internal abstract class MainModule {

  //...

  @ContributesAndroidInjector
  abstract fun contributeMainFragment(): MainFragment

}

親となるActivityはHasSupportFragmentInjectorインタフェースを実装し、DispatchingAndroidInjector<Fragment>クラスを保持する必要があります。
このクラスのインスタンスは上記コードにより必要なコードが生成されているので、DaggerによってInject出来ます。

.ui.main.MainActivity.kt
class MainActivity : AppCompatActivity(), LifecycleRegistryOwner, HasSupportFragmentInjector {

  @Inject lateinit var androidInjector: DispatchingAndroidInjector<Fragment>

  override fun supportFragmentInjector() = androidInjector

  //...
}

ViewModelのInject

ViewModelのインスタンスはArchitecture ComponentのViewModelProvidersによって管理、提供されますが、その際に必要なViewModelProvider.FactoryはDaggerによってそのインスタンスをInjectします。

.di.ActivityModule
@Module
internal abstract class ActivityModule {

  @Binds
  abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory

  //...
}

上記のように記述し、実体となるViewModelFactoryクラスを以下のように定義します。

.di.ViewModelFactory.kt
class ViewModelFactory @Inject
constructor(private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>)
  : ViewModelProvider.Factory {

  @Suppress("UNCHECKED_CAST")
  override fun <T : ViewModel> create(modelClass: Class<T>): T {
    var creator: Provider<ViewModel>? = creators[modelClass]
    if (creator == null) {
      for ((key, value) in creators) {
        if (modelClass.isAssignableFrom(key)) {
          creator = value
          break
        }
      }
    }
    if (creator == null) throw IllegalArgumentException("unknown model class " + modelClass)
    try {
      return creator.get() as T
    } catch (e: Exception) {
      throw RuntimeException(e)
    }
  }
}

引数となる

private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>

へは@Injectインタフェースを用いてDaggerによってInjectします。

.di.ViewModelKey.kt
import android.arch.lifecycle.ViewModel
import dagger.MapKey
import kotlin.reflect.KClass

@MustBeDocumented
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
@MapKey
internal annotation class ViewModelKey(val value: KClass<out ViewModel>)
.ui.main.di.MainModule.kt
@Module
internal abstract class MainModule {

  @Binds
  @IntoMap
  @ViewModelKey(MainViewModel::class)
  abstract fun bindMainViewModel(viewModel: MainViewModel): ViewModel

  //...

}

上記のように記述することで、DaggerのMultibindingsの機能を用いて、creatorsにInjectするためのMap<Class<? extends ViewModel>, Provider<ViewModel>>(Javaでの記述)クラスをInjectするためのコードが以下のように生成されます。
ViewModelKeyアノテーションを定義する際はKotlinのアノテーションを利用しています(Javaだとこのサンプルのような記述になります)。

.di.DaggerAppComponent.java(Daggerによって生成される)

public final class DaggerAppComponent implements AppComponent {
  //...
private final class MainActivitySubcomponentImpl
      implements ActivityModule_ContributeMainActivity$app_debug.MainActivitySubcomponent {

    private Provider<Map<Class<? extends ViewModel>, Provider<ViewModel>>>
        mapOfClassOfAndProviderOfViewModelProvider;
    private Provider<ViewModelFactory> viewModelFactoryProvider;
    private Provider<ViewModelProvider.Factory> bindViewModelFactoryProvider;

    private void initialize(final MainActivitySubcomponentBuilder builder) {
      //...

      this.bindMainViewModelProvider = (Provider) mainViewModelProvider;

      this.mapOfClassOfAndProviderOfViewModelProvider =
          MapProviderFactory.<Class<? extends ViewModel>, ViewModel>builder(1)
              .put(MainViewModel.class, bindMainViewModelProvider)
              .build();

      this.viewModelFactoryProvider =
          ViewModelFactory_Factory.create(mapOfClassOfAndProviderOfViewModelProvider);

      this.bindViewModelFactoryProvider = (Provider) viewModelFactoryProvider;

    }
private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>

creatorsを上記のように@JvmSuppressWildcardsを用いずに定義すると、Kotlinはコンパイル時にcreatorsの型を以下のようなクラスと解釈し、DaggerによるInjectが行えないので注意が必要です。

java.util.Map<java.lang.Class<? extends android.arch.lifecycle.ViewModel>,? extends javax.inject.Provider<android.arch.lifecycle.ViewModel>>

ActivityModuleとMainModule

.di.ActivityModule
@Module
internal abstract class ActivityModule {

  @Binds
  abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory

  @ContributesAndroidInjector(modules = arrayOf(MainModule::class))
  internal abstract fun contributeMainActivity(): MainActivity

}
.ui.main.di.MainModule.kt
@Module
internal abstract class MainModule {

  @Binds
  @IntoMap
  @ViewModelKey(MainViewModel::class)
  abstract fun bindMainViewModel(viewModel: MainViewModel): ViewModel

  @ContributesAndroidInjector
  abstract fun contributeMainFragment(): MainFragment

}

ActivityModuleMainModuleの実装は全体で以上のようになります。
アプリ内にActivityが増えるたびにActivityModule内に、Activityとそれに対応するModuleの対応の記述が増え、Activity内にFragmentやViewModelが増えるたびに対応するModule内(MainModuleなど)に記述が増えるような実装にしました。

pacakge構成

最後に全体のフォルダ構成(debug時)を記載します。

app-packages.png

まとめ

いくつか注意点は必要ですが、KotlinとArchitecture ComponentsそしてDagger2による実装は問題なく行えるように感じました。
Architecture ComponentsによってActivity/Fragmentのライフサイクルが考慮されているので、savedInstanceStateまわりの記述が不要になるのはとても大きなメリットです。
また、RxLifecycleを用いることでLiveDataの代わりにRxJavaを用いることも可能そうです。

Architecture Componentsの正式リリースを待つことにはなりますが、現在開発中のアプリでは以上のような実装にしたいと考えています。
コメントやTwitterGitHubへ、フィードバックお待ちしています!