2021년 5월 6일 목요일

Dagger2 ( Java -> Kotlin )

Dagger2 ( Java -> Kotlin )

Dagger2 ( Java -> Kotlin )

Dagger から Hilt, Koinに 引越しする前に覚えたことを忘れないうちに書いておきたいと思ってここに書き留める。

参考:DIというのは。
DIは外部から機能を注入することでなるべくインスタンスを生成しないようにするのが基本概念だ。
DIを使わないとインタフェースを使って同じく実現できるがそんなことしようとすると作るファイル数が増えるのでめんどくさくなるので普通はDIライブラリーを使う。
プログラミングにてDIの仕組みは外部に自分が定義したモジュール単位のクラスを使いたいインスタンス変数にタイプを合わせて注入して使うことだ。
作ったモジュールがインスタンス変数に注入されるのはDIライブラリーがやってくれるので指定だけやればDIが実現できる。
Android には Dagger, Hilt, Koin などが普通。

Daggerから DI をするためには以下の概念を理解する必要がある。

  1. Component : こいつは病院の役割だ。Inject(注射を受けるやつ)に必要な色んな薬を持っている。格製薬会社が作ったModule(薬)を必要な時に要請しているところに注射をする。
  2. Subcomponent : 大きい病院の役割がデカくなった場合には各分野ことに切り分けて専門にする小さい病院みたいに小分けして親子関係ようにするのが可能だ。
  3. Module & Provides : ModuleはComponentに薬を提供する製薬会社だ。Component(病院)はModule(製薬会社)と契約して指定された薬をProvides(提供)する。ModuleとProvidesは定義するだけで実際に注射するやつはComponentだ。
  4. Scope : DaggerではApplication単位を制御するために@Singletonを指定してアプリケーション全盤の範囲にて活動をしようとしている。なので使っているActivityとかFragmentとかに限られたInjectが必要な場合には余計なメモリを使いたくないから別途でScopeを指定する必要がある。(Dagger2@PerFragment,@PerActivityなどを提供している。無論、自分のScopeも作れる)
  5. 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関連の宣言が必要だ。
宣言は三つぐらいがありそれぞれのパタンーで作れる。

  1. すでにアプリケーションが作られているのでここから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を使わなくてもインスタンスを生成できたのは良いところだ。
しかし、これは依存関係を減らしただけだ。新しいものが追加されたら直接に修正しなければいけないのでこちらはあんまりに使わないかもしれない。

  1. 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  
    }  
}
  1. 最初からアプリケーションを作るなら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:

댓글 쓰기