Dagger2 ( Java -> Kotlin )
Dagger から Hilt, Koinに 引越しする前に覚えたことを忘れないうちに書いておきたいと思ってここに書き留める。
参考:DIというのは。
DIは外部から機能を注入することでなるべくインスタンスを生成しないようにするのが基本概念だ。
DIを使わないとインタフェースを使って同じく実現できるがそんなことしようとすると作るファイル数が増えるのでめんどくさくなるので普通はDIライブラリーを使う。
プログラミングにてDIの仕組みは外部に自分が定義したモジュール単位のクラスを使いたいインスタンス変数にタイプを合わせて注入して使うことだ。
作ったモジュールがインスタンス変数に注入されるのはDIライブラリーがやってくれるので指定だけやればDIが実現できる。
Android には Dagger, Hilt, Koin などが普通。
Daggerから DI をするためには以下の概念を理解する必要がある。
- Component : こいつは病院の役割だ。Inject(注射を受けるやつ)に必要な色んな薬を持っている。格製薬会社が作ったModule(薬)を必要な時に要請しているところに注射をする。
- Subcomponent : 大きい病院の役割がデカくなった場合には各分野ことに切り分けて専門にする小さい病院みたいに小分けして親子関係ようにするのが可能だ。
- Module & Provides : ModuleはComponentに薬を提供する製薬会社だ。Component(病院)はModule(製薬会社)と契約して指定された薬をProvides(提供)する。ModuleとProvidesは定義するだけで実際に注射するやつはComponentだ。
- Scope : DaggerではApplication単位を制御するために@Singletonを指定してアプリケーション全盤の範囲にて活動をしようとしている。なので使っているActivityとかFragmentとかに限られたInjectが必要な場合には余計なメモリを使いたくないから別途でScopeを指定する必要がある。(Dagger2@PerFragment,@PerActivityなどを提供している。無論、自分のScopeも作れる)
- Inject : Inject (注射を要請)するものにComponentが適当なModule(製薬会社)のProvides(薬)を処方して注射を受ける。
Basic
Dagger はAndroid専用のライブラリではない。なのでbuild.grade(Module:app) に三つのライブラリを追加する。
plugins {
...
id 'kotlin-kapt'
}
dependencies {
...
// dagger2
def DAGGER_VERSION = '2.26'
implementation "com.google.dagger:dagger:$DAGGER_VERSION"
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
implementation "com.google.dagger:dagger-android:$DAGGER_VERSION"
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
...
}
ライブラリ名の通りにDaggerがAndroidを支援する(DaggerApplication, AndroidInjectorなとのインターフェース)ために必要だ。
Daggerを使う目的はClass間に依存関係を減らすためなのでTopLevel(アプリケーション単位)でDagger関連の宣言が必要だ。
宣言は三つぐらいがありそれぞれのパタンーで作れる。
- すでにアプリケーションが作られているのでここからDaggerを導入しようとしたら既存のソースに影響がされたくない時には既存のコードに必要なDagger宣言をする。
// MyApplicationはすでにあった場合
public class MyApplication extends Application {
public static MyFirstComponent myFirstComponent;
@Override
public void onCreate() {
super.onCreate();
// 既存のコードにこれを追加する
myFirstComponent = DaggerMyFirstComponent
.builder()
.build();
}
}
//-----------------------//
// package name/mydi/components/MyFirstComponent.java
//-----------------------//
// Singletone スコープはアプリケーションの全範囲
// このコンポーネントはMyFirstModule.classを持つ
// Daggerが自動にDaggerMyFirstModuleというクラスを作る
@Singleton
@Component(modules = {MyFirstModule.class})
public interface MyFirstComponent {
FirstViewModel firstViewModel();
SecondViewModel secondViewModel();
}
//-----------------------//
// package name/mydi/modules/MyFirstComponent.java
//-----------------------//
@Singleton
@Module
public class MyFirstModule {
@Singleton
@Provides public FirstViewModel firstViewModel() {
return new FirstViewModel();
}
@Singleton
@Provides public SecondViewModel secondViewModel() {
return new SecondViewModel();
}
}
//-----------------------//
// package name/viewmodel/FirstViewModel.java
//-----------------------//
public class FirstViewModel {
@Inject
public FirstViewModel() {}
public String getSay() {
return "Say First Module";
}
}
//-----------------------//
// package name/viewmodel/SecondViewModel.java
//-----------------------//
public class SecondViewModel {
@Inject
public SecondViewModel() {}
public String getSay() {
return "Say Second Module";
}
}
//
public class MainActivity extends AppCompatActivity {
@Inject
public FirstViewModel firstViewModel;
@Inject
public SecondViewModel secondViewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
firstViewModel = MyApplication.component.firstViewModel();
secondViewModel = MyApplication.component.secondViewModel();
}
}
一旦、FirstViewModelとSecondViewModelがMainActiviy内でnewを使わなくてもインスタンスを生成できたのは良いところだ。
しかし、これは依存関係を減らしただけだ。新しいものが追加されたら直接に修正しなければいけないのでこちらはあんまりに使わないかもしれない。
- HasAndroidInjectorインターフェースが出来てそれを使う。
//-----------------------//
// App.kt
//-----------------------//
class App : Application(), HasAndroidInjector { //HasAndroidInjectorを継承する
@Inject lateinit var androidInjector: DispatchingAndroidInjector<Any>
override fun androidInjector(): AndroidInjector<Any> {
DaggerAppComponent.factory().create(this).inject(this)
return androidInjector
}
}
//-----------------------//
// AppComponent.kt
//-----------------------//
@Singleton
@Component(
modules = [
AndroidInjectionModule::class, //AndroidSupportInjectionModuleからAndroidInjectionModuleに変更
ActivityBindingModule::class,
AppModule::class
]
)
//-----------------------//
// MainActivity.kt
//-----------------------//
class MainActivity : AppCompatActivity(), HasAndroidInjector {
@Inject
lateinit var androidInjector: DispatchingAndroidInjector<Any>
@Inject
lateinit var prefs: SharedPreferenceStorage
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
AndroidInjection.inject(this)
}
override fun androidInjector(): AndroidInjector<Any> {
return androidInjector
}
}
- 最初からアプリケーションを作るならDaggerが提供するクラスを継承して簡単(?)に作れる。
//-----------------------//
// App.kt
//-----------------------//
// DaggerApplicationを継承してAppComponenのfactoryを実行する
class App : DaggerApplication() {
override fun applicationInjector(): AndroidInjector<out DaggerApplication> {
return DaggerAppComponent.factory()
.create(this)
}
}
//-----------------------//
// your package/di/AppComponentt.kt
//-----------------------//
// このファイルはDaggerがDaggerAppComponentに実装して提供する
@Singleton
@Component(
//アプリに使うModuleを定義する
modules = [
AndroidInjectionModule::class,
AppModule::class,
MainActivityBuilder::class,
RepositoryModule::class
]
)
interface AppComponent : AndroidInjector<App> {
@Component.Factory
interface Factory {
fun create(@BindsInstance app: App): AppComponent
}
}
//-----------------------//
// your package/di/AppModule.kt
//-----------------------//
// Global Application ContextためのModule
@Module
abstract class AppModule {
@Binds
abstract fun provideContext(application: App): Context
}
@Singleton
//-----------------------//
// your package/di/ui/MainActivityBuilder.kt
//-----------------------//
// ContributesAndroidInjectorにより自動にMainActivityModuleが連携される
@Module
abstract class MainActivityBuilder {
@ContributesAndroidInjector(modules = [MainActivityModule::class])
abstract fun bindMainActivity(): MainActivity
}
//-----------------------//
// your package/di/ui/MainActivityModule.kt
//-----------------------//
@Module
interface MainActivityModule {
@Binds
@IntoMap @ViewModelKey(MainViewModelImpl::class)
fun bindMainViewModel(viewModel: MainViewModelImpl): ViewModel
@ContributesAndroidInjector
fun bindMainFragment(): MainFragment
}
//-----------------------//
// your package/di/ui/MainActivityModule.kt
//-----------------------//
@Module
interface MainActivityModule {
@Binds
@IntoMap @ViewModelKey(MainViewModelImpl::class)
fun bindMainViewModel(viewModel: MainViewModelImpl): ViewModel
@ContributesAndroidInjector
fun bindMainFragment(): MainFragment
}
//-----------------------//
// your package/ui/MainFragment.kt
//-----------------------//
class MainFragment : DaggerFragment() {
@Inject
lateinit var viewModelFactory: ViewModelFactory
private val viewModel: MainViewModel by lazy {
ViewModelProvider(this, viewModelFactory).get(MainViewModelImpl::class.java)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel.getRepo()
viewModel.repo.observe(this, Observer {
Toast.makeText(
context,
"id = ${it.id}, title = ${it.title}, content = ${it.content}",
Toast.LENGTH_SHORT
).show()
})
return inflater.inflate(R.layout.fragment_main, container, false)
}
}
//-----------------------//
// your package/ui/MainViewModel.kt
//-----------------------//
interface MainViewModel {
val repo: LiveData<Repo>
fun getRepo()
}
class MainViewModelImpl @Inject constructor(private val repository: MainRepository) : ViewModel(),
MainViewModel {
override val repo = MutableLiveData<Repo>()
override fun getRepo() {
val data = repository.getRepo()
repo.postValue(data)
}
}
//-----------------------//
// your package/di/RepositoryModule.kt
//-----------------------//
@Module
internal object RepositoryModule {
@Singleton
@Provides fun provideMainRepository(): MainRepository =
MainRepositoryImpl()
}
//-----------------------//
// your package/MainRepository.kt
//-----------------------//
interface MainRepository {
fun getRepo(): Repo
}
class MainRepositoryImpl : MainRepository {
override fun getRepo(): Repo {
return Repo(id = 1, title = "タイトル", content = "内容")
}
}
data class Repo(
val id: Int,
val title: String,
val content: String
)
//-----------------------//
// your package/di/viewmodel/ViewModelKey.kt
//-----------------------//
@MustBeDocumented
@Target(
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY_GETTER,
AnnotationTarget.PROPERTY_SETTER
)
@Retention(AnnotationRetention.RUNTIME)
@MapKey internal annotation class ViewModelKey(val value: KClass<out ViewModel>)
//-----------------------//
// your package/di/viewmodel/ViewModelFactory.kt
//-----------------------//
class ViewModelFactory @Inject constructor(
private val creators: Map<Class<out ViewModel>, @JvmSuppressWildcards Provider<ViewModel>>
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
var creator: Provider<out 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 {
@Suppress("UNCHECKED_CAST")
return creator.get() as T
} catch (e: Exception) {
throw RuntimeException(e)
}
}
}
HiltやKoin使うともっと簡単に設定ができる!
HiltやKoin使おう!
0 comments:
댓글 쓰기