はじめに
Google I/O 2017でKotlinがAndroidアプリ開発における公式言語としてサポートされることになりました。
また、同時にAndroid Architecture ComponentsがAndroidのライフサイクルにおける画面回転などへの対応を容易にするライブラリとして追加され、このライブラリを用いたAndroidアプリ開発におけるおすすめの設計が提案されています。
以下のGoogleのSampleリポジトリにはその設計指針を用いたサンプルが公開されています。
本記事では上記のリポジトリを参考にしながら、KotlinとArchitecture Components、そしてDagger2を用いた設計を備忘録的にまとめました。
また、現在進行系で開発しているので、何か間違っている点やご意見等あればぜひ、コメントやTwitterなどでお願いします。
Sample
記事における実装のサンプルは以下にあります。
Android Studio3.0 Canary3以降でビルド可能です。
実装について
Kotlin
Kotlinについては私も執筆に参加させて頂いた「Kotlin入門までの助走読本」(PDF注意)を読んで頂くのが良いかと思います。
豪華執筆陣による「Kotlin入門までの助走読本」をリリースします!
— 日本Kotlinユーザグループ (@kotlin_jp) 2017年5月29日
話題のプログラミング言語の「味見」をぜひ!PDF注意https://t.co/LJJDReAzue
またGoogle I/O 2017のセッションの中でIntroduction to KotlinというKotlinを知って頂く上でおすすめのセッションがあり、そちらを翻訳した記事がありますのであわせてご覧ください。
build.gradle
Kotlinはbuild.gradleに以下のような記述を追加することで使うことが出来ます。
buildscript {
ext.kotlin_version = '1.1.2-4'
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
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サポートは以下のように導入します。
dependencies {
implementation "com.squareup.moshi:moshi:$moshi_version"
implementation "com.squareup.moshi:moshi-kotlin:$moshi_version"
}
Moshiインスタンスを初期化する時に、KotlinJsonAdapterFactory
を用い、またRetrofitのConverterFactory
としてMoshiConverterFactory
を指定します。
@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
を導入する必要があります。
ライブラリの最新バージョンはこちらでチェックすることが出来ます。
android {
//...
dataBinding {
enabled = true
}
}
dependencies {
kapt "com.android.databinding:compiler:2.3.3"
}
Architecture Components
Architecture Componentsを導入するには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
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
として上記のMainViewModel
クラスにMutableLiveData
としてownerId
と、LiveData
としてreposの2つのプロパティを定義してあります。
switchMap()
はandroid.arch.lifecycle.Transformations
クラス内に定義されているJavaのstatic methodですが、以下のようにKotlinの拡張関数で呼び出せるようにしてあります。
fun <X, Y> LiveData<X>.switchMap(func: (X) -> LiveData<Y>)
= Transformations.switchMap(this, func)
同様にandroid.arch.lifecycle:reactivestreams
にはRxJavaのFlowable
をLiveData
に変換するためのstatic methodであるfromPublisher()
が定義されているので、同様にKotlinの拡張関数で呼び出せるようにしてあります。
fun <T> Publisher<T>.toLiveData() = LiveDataReactiveStreams.fromPublisher(this)
以上のようにして、ownerId
(の中身)が変更された時に(空でなければ)、GitHubRepository.loadRepos() : Flowable
が呼ばれ、repos
(の中身)が変更されます。
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.repos
をobserve
し、その内容に応じてRecyclerViewに反映しています。
MainViewModel
のインスタンスはViewModelProviders.of().get()
によって提供され、ViewModelProvider.Factory
のインスタンスは後述するDaggerによってInjectされています。
Fragment上でviewModelのインスタンスを取る際に、ViewModelProviders.of()
の第一引数にFragment.activity
(JavaではgetActivity()
)を与えることによって親となるActivityや同じActivityに乗る他のFragmentとインスタンスを共通化出来ます。
こちらについては以下に詳細が書かれています。
Dagger2
Dagger2を導入するには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に対応するためAppComponent
はAndroidInjector<Application>
を継承しています。
@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)
}
@Module
internal abstract class ActivityModule {
//...
@ContributesAndroidInjector(modules = arrayOf(MainModule::class))
internal abstract fun contributeMainActivity(): MainActivity
}
Activityを継承したMainActivity
へのInjectは@ContributesAndroidInjector
アノテーションを用いて上記のように記述することによって、MainActivity
内の必要な依存性を解決するためのコードが以下のように生成されます。
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.DaggerApplication
とdagger.android.support.DaggerApplication
の2つ用意されているので注意が必要です。
今回はSupport Libraryに対応した後者のクラスを継承しています。
class App : DaggerApplication() {
override fun applicationInjector() = DaggerAppComponent.builder()
.application(this)
.build()
override fun onCreate() {
super.onCreate()
applyAutoInjector()
}
}
DaggerApplication
は以下のようにAndroidInjector<? extends DaggerApplication>
を返すabstract methodを持ってますので、DaggerAppComponent
をbuild()
して実装します。
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)
の記述を不要にしています。
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
アノテーションを用いて記述することで必要なコードが生成されます。
@Module
internal abstract class MainModule {
//...
@ContributesAndroidInjector
abstract fun contributeMainFragment(): MainFragment
}
親となるActivityはHasSupportFragmentInjector
インタフェースを実装し、DispatchingAndroidInjector<Fragment>
クラスを保持する必要があります。
このクラスのインスタンスは上記コードにより必要なコードが生成されているので、DaggerによってInject出来ます。
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します。
@Module
internal abstract class ActivityModule {
@Binds
abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
//...
}
上記のように記述し、実体となるViewModelFactory
クラスを以下のように定義します。
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します。
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>)
@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だとこのサンプルのような記述になります)。
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
@Module
internal abstract class ActivityModule {
@Binds
abstract fun bindViewModelFactory(factory: ViewModelFactory): ViewModelProvider.Factory
@ContributesAndroidInjector(modules = arrayOf(MainModule::class))
internal abstract fun contributeMainActivity(): MainActivity
}
@Module
internal abstract class MainModule {
@Binds
@IntoMap
@ViewModelKey(MainViewModel::class)
abstract fun bindMainViewModel(viewModel: MainViewModel): ViewModel
@ContributesAndroidInjector
abstract fun contributeMainFragment(): MainFragment
}
ActivityModule
とMainModule
の実装は全体で以上のようになります。
アプリ内にActivityが増えるたびにActivityModule
内に、Activityとそれに対応するModuleの対応の記述が増え、Activity内にFragmentやViewModelが増えるたびに対応するModule内(MainModule
など)に記述が増えるような実装にしました。
pacakge構成
最後に全体のフォルダ構成(debug時)を記載します。
まとめ
いくつか注意点は必要ですが、KotlinとArchitecture ComponentsそしてDagger2による実装は問題なく行えるように感じました。
Architecture ComponentsによってActivity/Fragmentのライフサイクルが考慮されているので、savedInstanceState
まわりの記述が不要になるのはとても大きなメリットです。
また、RxLifecycleを用いることでLiveDataの代わりにRxJavaを用いることも可能そうです。
Architecture Componentsの正式リリースを待つことにはなりますが、現在開発中のアプリでは以上のような実装にしたいと考えています。
コメントやTwitterやGitHubへ、フィードバックお待ちしています!