Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Android Constructor Injection

Avatar for nlgtuankiet nlgtuankiet
May 30, 2019
120

Android Constructor Injection

Dependency injection framework works best if it can create all the injected objects, meanwhile many Android framework classes are instantiated by the OS itself. This talk shows you how to overcome this limitation to perform constructor injection on Android Framework classes.

Avatar for nlgtuankiet

nlgtuankiet

May 30, 2019

Transcript

  1. contents.forEach { println("How to do it?") println("Dagger implementation") println("In context

    of multi-module project") println("In context of dynamic feature module") }
  2. ViewModel class TaskDetailViewModel : ViewModel() class TaskDetailFragment : Fragment() {

    private lateinit var viewModel: TaskDetailViewModel override fun onCreateView("/* bla bla "*/): View? { viewModel = ViewModelProviders .of(this) .get(TaskDetailViewModel"::class.java) "// bla bla } }
  3. ViewModel class TaskDetailViewModel @Inject constructor( private val getTask: GetTask, private

    val updateComplete: UpdateComplete, private val deleteTask: DeleteTask ) : ViewModel()
  4. ViewModel /** * Creates a {@link ViewModelProvider}, which retains ViewModels

    while a scope of given * {@code fragment} is alive. More detailed explanation is in {@link ViewModel}. * <p> * It uses the given {@link Factory} to instantiate new ViewModels. * * @param fragment a fragment, in whose scope ViewModels should be retained * @param factory a {@code Factory} to instantiate new ViewModels * @return a ViewModelProvider instance "*/ @NonNull @MainThread public static ViewModelProvider of(@NonNull Fragment fragment, @Nullable Factory factory) { "// bla bla }
  5. ViewModel class TaskDetailFragment : Fragment() { private lateinit var viewModel:

    TaskDetailViewModel @Inject lateinit var viewModelFactory: ViewModelProvider.Factory override fun onCreateView(): View? { taskDetailViewModel = ViewModelProviders .of(this, viewModelFactory) .get(TaskDetailViewModel"::class.java) } }
  6. ViewModel /** * Implementations of {@code Factory} interface are responsible

    to * instantiate ViewModels. "*/ public interface Factory { /** * Creates a new instance of the given {@code Class}. * <p> * * @param modelClass a {@code Class} whose instance is requested * @param <T> The type parameter for the ViewModel. * @return a newly created ViewModel "*/ @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass); }
  7. ViewModel class TaskDetailViewModel @Inject constructor( private val getTask: GetTask, private

    val updateComplete: UpdateComplete, private val deleteTask: DeleteTask ) : ViewModel() class TaskDetailViewModelFactory @Inject constructor( private val viewModelProvider: Provider<TaskDetailViewModel> ) : ViewModelProvider.Factory { override fun <T : ViewModel?> create(modelClass: Class<T>): T { return viewModelProvider.get() as T } }
  8. ViewModel @Module interface Binding { @Binds @IntoMap @ViewModelKey(TaskDetailViewModel"::class) fun viewModel(instance:

    TaskDetailViewModel): ViewModel } class TodoViewModelFactory @Inject constructor( private val providers: Map<Class<out ViewModel>, Provider<ViewModel">> ) : ViewModelProvider.Factory { override fun <T : ViewModel?> create(modelClass: Class<T>): T { return providers[modelClass]"?.get() as? T "?: throw IllegalArgumentException(“Unknown class: ${modelClass.name}") } } NOTE: remember to add @JvmSuppressWildcards on Provider<ViewModel>>
  9. Multi-module context @Subcomponent( modules = [ TaskDetailComponent.Binding"::class ] ) @FragmentScope

    interface TaskDetailComponent { @Module interface Binding { @Binds @IntoMap @ViewModelKey(TaskDetailViewModel"::class) fun viewModel(instance: TaskDetailViewModel): ViewModel } }
  10. Multi-module context class TaskDetailFragment : Fragment() { private lateinit var

    viewModel: TaskDetailViewModel @Inject lateinit var viewModelFactory: ViewModelProvider.Factory override fun onCreateView(): View? { taskDetailViewModel = ViewModelProviders .of(this, viewModelFactory) .get(TaskDetailViewModel"::class.java) } } class TodoViewModelFactory @Inject constructor( private val providers: Map<Class<out ViewModel>, Provider<ViewModel">> ) : ViewModelProvider.Factory
  11. Dynamic Feature context @Component( modules = [ SeedDatabaseActivityComponent.Binding"::class ] )

    @ActivityScope interface SeedDatabaseActivityComponent { fun inject(target: SeedDatabaseActivity) @Module( includes = [ ViewModelFactoryBindingModule"::class ] ) interface Binding { @Binds @ViewModelKey(SeedDatabaseViewModel"::class) @IntoMap fun viewModel(instance: SeedDatabaseViewModel): ViewModel } }
  12. Dynamic Feature context class SeedDatabaseActivity : AppCompatActivity() { @Inject lateinit

    var viewModelFactory: ViewModelProvider.Factory private lateinit var viewModel: SeedDatabaseViewModel override fun onCreate(savedInstanceState: Bundle?) { viewModel = ViewModelProviders .of(this, viewModelFactory) .get(SeedDatabaseViewModel"::class.java) } }
  13. Fragment class StatisticsFragment : Fragment() { @Inject lateinit var viewModelFactory:

    ViewModelProvider.Factory override fun onCreateView(): View? { AndroidSupportInjection.inject(this) } }
  14. Fragment package androidx.fragment.app; /** * * <p>The main differences when

    using this support version instead of the framework version are: * <ul> * <li>Your activity must extend {@link FragmentActivity} * <li>You must call {@link FragmentActivity#getSupportFragmentManager} to get the * {@link FragmentManager} * "</ul> * "*/ public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener, LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner { }
  15. FragmentFactory class MainActivity : DaggerAppCompatActivity() { @Inject lateinit var fragmentFactory:

    FragmentFactory override fun onCreate(savedInstanceState: Bundle?) { AndroidInjection.inject(this) supportFragmentManager.fragmentFactory = fragmentFactory } }
  16. FragmentFactory class TodoFragmentFactory @Inject constructor() : FragmentFactory() { override fun

    instantiate( classLoader: ClassLoader, className: String ): Fragment { "// instantiate your fragment here } }
  17. Circular dependency @FragmentScope class TaskDetailNavigator @Inject constructor( private val fragment:

    TaskDetailFragment ) class TaskDetailFragment @Inject constructor( private val navigator: TaskDetailNavigator ) : Fragment()
  18. Circular dependency class TaskDetailFragment : Fragment { @Inject lateinit var

    navigator: TasksNavigator override fun onCreate(savedInstanceState: Bundle?) { "// subcomponent creation time "// "-> got fragment instance AndroidSupportInjection.inject(this) "// use fragment instance to create navigator } }
  19. Circular dependency @FragmentScope class TaskDetailNavigator @Inject constructor( private val )

    class TaskDetailFragment @Inject constructor( private val navigator: TaskDetailNavigator ) : Fragment() fragment: TaskDetailFragment
  20. holder: Holder<TaskDetailFragment> Circular dependency @FragmentScope class TaskDetailNavigator @Inject constructor( private

    val ) class TaskDetailFragment @Inject constructor( private val navigator: TaskDetailNavigator ) : Fragment() class Holder<T : Any> { private var _instance: T? = null var instance: T get() = requireNotNull(_instance) set(value) { require(_instance "== null) _instance = value } }
  21. Holder Injection @Module interface Module { @Provides @FragmentScope fun fragment(

    holder: Holder<TaskDetailFragment>, navigator: TaskDetailNavigator ) = TaskDetailFragment( navigator = navigator ).also { holder.instance = it } @Provides @FragmentScope fun navigator(holder: Holder<TaskDetailFragment>): TaskDetailNavigator { return TaskDetailNavigator(holder) } @Provides @FragmentScope fun holder() = Holder<TaskDetailFragment>() }
  22. Scoping @Subcomponent @FragmentScope interface TaskDetailComponent { fun fragment(): TaskDetailFragment @Subcomponent.Factory

    interface Factory { fun create(): TaskDetailComponent } } @Module(subcomponents = [TaskDetailComponent"::class]) interface TaskDetailModule @Module interface MainActivityModule { @ActivityScope @ContributesAndroidInjector(modules = [TaskDetailModule"::class]) fun activity(): MainActivity }
  23. FragmentFactory @Subcomponent @FragmentScope interface TaskDetailComponent : FragmentComponent<TaskDetailFragment> { @Subcomponent.Factory interface

    Factory : FragmentComponent.Factory<TaskDetailComponent> } interface FragmentComponent<T : Fragment> { fun fragment(): T interface Factory<T : FragmentComponent<out Fragment">> { fun create(): T } }
  24. Fragment @Module(includes = [MainActivityModule.FragmentComponentFactories"::class]) interface MainActivityModule { @Module interface FragmentComponentFactories

    { @Binds @IntoMap @FragmentKey(TaskDetailFragment"::class) fun TaskDetailComponent(impl: TaskDetailComponent.Factory): FragmentComponent.Factory""<*> @Binds @IntoMap @FragmentKey(TasksFragment"::class) fun TasksComponent(impl: TasksComponent.Factory): FragmentComponent.Factory""<*> } }
  25. Fragment class TodoFragmentFactory @Inject constructor( private val providers: Map<Class<out Fragment>,

    Provider<FragmentComponent.Factory""<*>">> ) : FragmentFactory() { override fun instantiate(classLoader: ClassLoader, className: String): Fragment { val fragmentClass = classLoader.loadClass(className) return providers.entries.find { it.key.isAssignableFrom(fragmentClass) } "?.value"?.get()"?.create()"?.fragment() "?: super.instantiate(classLoader, className) } } NOTE: remember to add @JvmSuppressWildcards on Provider<FragmentComponent.Factory<*>>>
  26. Multi-module context class TodoFragmentFactory @Inject constructor( private val providers: Map<Class<out

    Fragment>, Provider<FragmentComponent.Factory""<*>">> ) : FragmentFactory() @Module interface FragmentFactoryBindingModule { @Binds fun fragmentFactory(instance: TodoFragmentFactory): FragmentFactory } @AppScope @Component( modules = [ MainActivityModule"::class, FragmentFactoryBindingModule"::class ] ) interface AppComponent
  27. Dynamic Feature Context @Component( modules = [ FragmentFactoryBindingModule"::class ] )

    @ActivityScope interface SeedDatabaseActivityComponent { fun inject(target: SeedDatabaseActivity) }
  28. The graph Aka dependency graph Database Api DataRepository ApplicationComponent Context

    @Component( modules = [FooActivityModule"::class] ) interface ApplicationComponent @Module( subcomponents = [FooActivityComponent"::class] ) interface FooActivityModule @Subcomponent interface FooActivityComponent { @Subcomponent.Factory interface Factory { fun create( @BindsInstance activity: FooActivity ) } }
  29. The graph Aka dependency graph Database Api DataRepository ApplicationComponent Context

    FooActivityComponent FooActivity @Component( modules = [FooActivityModule"::class] ) interface ApplicationComponent @Module( subcomponents = [FooActivityComponent"::class] ) interface FooActivityModule @Subcomponent interface FooActivityComponent { @Subcomponent.Factory interface Factory { fun create( @BindsInstance activity: FooActivity ) } }
  30. The graph Aka dependency graph Database Api DataRepository ApplicationComponent Context

    FooActivityComponent FooActivity FooFragmentComponent FooFragment @Component( modules = [FooActivityModule"::class] ) interface ApplicationComponent @Module( subcomponents = [FooActivityComponent"::class] ) interface FooActivityModule @Subcomponent interface FooActivityComponent { @Subcomponent.Factory interface Factory { fun create( @BindsInstance activity: FooActivity ) } }
  31. @Subcomponent.Factory interface Factory { fun create( @BindsInstance activity: FooActivity )

    } } Dependency Request @Module object Provision { @Provides fun viewModel(activity: FooActivity): FooViewModel = activity.viewModel @Provides fun args(activity: FooActivity): FooActivityArgs { return FooActivityArgs.fromBundle(activity.intent.extras) } @Provides fun navigator(activity: FooActivity): FooNavigator = FooNavigator(activity) } @Subcomponent(modules = [Provision"::class]) interface FooActivityComponent {
  32. @Subcomponent.Factory interface Factory { fun create( @BindsInstance activity: FooActivity )

    } } Dependency Request @Module object Provision { @Provides fun viewModel(activity: FooActivity): FooViewModel = activity.viewModel @Provides fun args(activity: FooActivity): FooActivityArgs { return FooActivityArgs.fromBundle(activity.intent.extras) } @Provides fun navigator(activity: FooActivity): FooNavigator = FooNavigator(activity) } Extended dependency @Subcomponent(modules = [Provision"::class]) interface FooActivityComponent {
  33. @Subcomponent.Factory interface Factory { fun create( @BindsInstance activity: FooActivity )

    } } Dependency Request @Module object Provision { @Provides fun viewModel(activity: FooActivity): FooViewModel = activity.viewModel @Provides fun args(activity: FooActivity): FooActivityArgs { return FooActivityArgs.fromBundle(activity.intent.extras) } @Provides fun navigator(activity: FooActivity): FooNavigator = FooNavigator(activity) } Extended dependency Request Request Request @Subcomponent(modules = [Provision"::class]) interface FooActivityComponent {
  34. One time dependency • Variable that only available at runtime

    • User input • Parameters • Use only 1 time at 1 place
  35. One time dependency class DownloadUserInfoWorker @Inject constructor( private val userId:

    String, private val isSaveToDatabase: Boolean, private val okhttp: OkHttpClient, private val database: Database ) { fun doWork() { "// bla "// bla if (isSaveToDatabase) { "// bla bla } } }
  36. One time dependency class DownloadUserInfoWorker @Inject constructor( private val userId:

    String, private val isSaveToDatabase: Boolean, private val okhttp: OkHttpClient, private val database: Database ) { fun doWork() { "// bla "// bla if (isSaveToDatabase) { "// bla bla } } } Already available 
 in the graph }
  37. One time dependency class DownloadUserInfoWorker @Inject constructor( private val userId:

    String, private val isSaveToDatabase: Boolean, private val okhttp: OkHttpClient, private val database: Database ) { fun doWork() { "// bla "// bla if (isSaveToDatabase) { "// bla bla } } } Not available } Already available 
 in the graph }
  38. One time dependency class DownloadUserInfoWorker @Inject constructor( private val userId:

    String, private val isSaveToDatabase: Boolean, private val okhttp: OkHttpClient, private val database: Database ) { fun doWork() { "// bla "// bla if (isSaveToDatabase) { "// bla bla } } } Not available } Already available 
 in the graph }
  39. Setup @Subcomponent interface DownloadSubcomponent { fun worker(): DownloadUserInfoWorker @Subcomponent.Factory interface

    Factory { fun create( @BindsInstance userId: String, @BindsInstance isSaveToDatabase: Boolean ): DownloadSubcomponent } }
  40. Setup @Subcomponent interface DownloadSubcomponent { fun worker(): DownloadUserInfoWorker @Subcomponent.Factory interface

    Factory { fun create( @BindsInstance userId: String, @BindsInstance isSaveToDatabase: Boolean ): DownloadSubcomponent } } @Module( subcomponents = [ DownloadSubcomponent"::class ] ) interface WorkerModule
  41. Setup @Subcomponent interface DownloadSubcomponent { fun worker(): DownloadUserInfoWorker @Subcomponent.Factory interface

    Factory { fun create( @BindsInstance userId: String, @BindsInstance isSaveToDatabase: Boolean ): DownloadSubcomponent } } @Module( subcomponents = [ DownloadSubcomponent"::class ] ) interface WorkerModule @Component( modules = [ WorkerModule"::class ] ) interface ParentComponent
  42. Setup @Subcomponent interface DownloadSubcomponent { fun worker(): DownloadUserInfoWorker @Subcomponent.Factory interface

    Factory { fun create( @BindsInstance userId: String, @BindsInstance isSaveToDatabase: Boolean ): DownloadSubcomponent } } class Main { fun main() { val isSaveToDatabase: Boolean val userId: String val component = DaggerDownloadSubcomponent .factory() .create(isSaveToDatabase, userId) val worker = component.worker() "// finally! worker.doWork() } } @Module( subcomponents = [ DownloadSubcomponent"::class ] ) interface WorkerModule @Component( modules = [ WorkerModule"::class ] ) interface ParentComponent
  43. The right tool for the right job • Subcomponent advantage:

    • Can request the extended dependencies at
 multiple places
  44. The right tool for the right job • Subcomponent advantage:

    • Can request the extended dependencies at
 multiple places one place
  45. One time dependency class DownloadUserInfoWorker private val userId: String, private

    val isSaveToDatabase: Boolean, private val okhttp: OkHttpClient, private val database: Database ) { fun doWork() { "// bla bla "// bla bla if (isSaveToDatabase) { "// bla bla } } @Inject } constructor(
  46. One time dependency class DownloadUserInfoWorker private val userId: String, private

    val isSaveToDatabase: Boolean, private val okhttp: OkHttpClient, private val database: Database ) { fun doWork() { "// bla bla "// bla bla if (isSaveToDatabase) { "// bla bla } } class Factory constructor( private val okhttp: OkHttpClient, private val database: Database ) { fun create(params: Params, userId: String): DownloadUserInfoWorker { return DownloadUserInfoWorker(params, userId, okhttp, database) } } @Inject } constructor (
  47. One time dependency fun main() { val isSaveToDatabase: Boolean val

    params: Params worker.doWork() } } val component = DaggerDownloadSubcomponent .factory() .create(isSaveToDatabase, params) component.worker() val worker = class Main {
  48. One time dependency fun main() { val isSaveToDatabase: Boolean val

    params: Params worker.doWork() } } val component = DaggerDownloadSubcomponent .factory() .create(isSaveToDatabase, params) component.worker() val worker = class Main {
  49. @Inject lateinit var factory: DownloadSubcomponent.Factory One time dependency fun main()

    { val isSaveToDatabase: Boolean val params: Params worker.doWork() } } val component = DaggerDownloadSubcomponent .factory() .create(isSaveToDatabase, params) component.worker() val worker = class Main { factory.create(params, isSave)
  50. OkHttpClient,
 Database class Factory @Inject constructor( private val okhttp: private

    val database: ) { fun create( params: Params, save: Boolean ): DownloadUserInfoWorker { return DownloadUserInfoWorker( params, save, ) } } Performance matter okhttp, database
  51. Provider<OkHttpClient>,
 Provider<Database> OkHttpClient,
 Database class Factory @Inject constructor( private val

    okhttp: private val database: ) { fun create( params: Params, save: Boolean ): DownloadUserInfoWorker { return DownloadUserInfoWorker( params, save, ) } } Performance matter okhttp, database okhttp.get(), database.get()
  52. In a nutshell • When the extended dependencies are request

    • Multiple time => Subcomponent • One time => Factory
  53. assisted-inject class DownloadUserInfoWorker @AssistedInject constructor( @Assisted private val params: Params,

    @Assisted private val isSaveToDatabase: Boolean, private val okhttp: OkHttpClient, private val database: Database ) { @AssistedInject interface Factory { fun create(params: Params, isSaveToDatabase: Boolean) } } // generate DownloadUserInfoWorker_AssistedFactory // https://github.com/square/AssistedInject
  54. auto-factory @AutoFactory class DownloadUserInfoWorker( private val params: Params, private val

    isSaveToDatabase: Boolean, @Provided private val okhttp: OkHttpClient, @Provided private val database: Database ) // generate DownloadUserInfoWorkerFactory // https://github.com/google/auto/tree/master/factory
  55. View class CartView( context: Context, attrs: AttributeSet, tracker: Tracker, abTesting:

    AbTesting ) : View(context, attrs) { init { setOnClickListener { tracker.userClickOnCartIcon(); if (abTesting.showExpeirimentalCartPage()) { "// open new cart page } else { "// open ole cart page } } } }
  56. View public interface Factory { /** * Hook you can

    supply that is called when inflating from a LayoutInflater. * You can use this to customize the tag names available in your XML * layout files. "*/ public View onCreateView(String name, Context context, AttributeSet attrs); }
  57. View class SeedDatabaseActivity : AppCompatActivity() { @Inject lateinit var layoutInflaterFactory:

    LayoutInflater.Factory override fun onCreate(savedInstanceState: Bundle?) { layoutInflater.factory = layoutInflaterFactory super.onCreate(savedInstanceState) setContentView(R.layout.seed_database_activity) } }
  58. Interceptor class TextUpdatingInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain):

    InflateResult { val result = chain.proceed(chain.request()) if (result.view is TextView) { "// Do something to result.view() "// You have access to result.context() and result.attrs() val textView = result.view as TextView? textView""!!.text = "[Prefix] " + textView.text } return result } }
  59. Interceptor class CustomTextViewInterceptor : Interceptor { override fun intercept(chain: Interceptor.Chain):

    InflateResult { val request = chain.request() if (request.name.endsWith("TextView")) { val view = CustomTextView() "// create view return InflateResult.builder() .view(view) .build() } else { return chain.proceed(request) } } }
  60. Install class TodoApplication : DaggerApplication() { @Inject lateinit var viewFactoryInterceptor:

    ViewFactoryInterceptor override fun onCreate() { super.onCreate() ViewPump.init( ViewPump.builder() .addInterceptor(viewFactoryInterceptor) .build() ) } } class MainActivity : DaggerAppCompatActivity() { override fun attachBaseContext(newBase: Context?) { super.attachBaseContext(ViewPumpContextWrapper.wrap(newBase)) } }
  61. CustomView class CustomView @AssistedInject constructor( @Assisted context: Context, @Assisted attrs:

    AttributeSet?, private val notificationManagerCompat: NotificationManagerCompat ) : View(context, attrs) { @AssistedInject.Factory interface Factory : ViewFactory } interface ViewFactory { fun create(context: Context, attrs: AttributeSet?): View } @Module interface ViewBindingModule { @Binds @IntoMap @ClassKey(CustomView.class) ViewFactory bind(CustomView_AssistedFactory factory); }
  62. Factory Impl class ViewFactoryInterceptor @Inject constructor( private val provider: @JvmSuppressWildcards

    Map<Class<out View>, Provider<ViewFactory">> ) : Interceptor { override fun intercept(chain: Interceptor.Chain): InflateResult { val request = chain.request() val className = request.name val provider = provider.entries.firstOrNull { it.key.name "== className } return provider"?.value"?.get()"?.create(request.context, request.attrs)"?.let { InflateResult.builder() .view(it) .name(it"::class.java.name) .context(request.context) .attrs(request.attrs) .build() } "?: chain.proceed(request) } }
  63. Dynamic Feature class TodoApplication : DaggerApplication() { @Inject lateinit var

    viewFactoryInterceptor: ViewFactoryInterceptor override fun onCreate() { super.onCreate() ViewPump.init( ViewPump.builder() .addInterceptor(viewFactoryInterceptor) .build() ) } }
  64. Dynamic Feature @ApplicationScope class DispatchViewFactoryInterceptor @Inject constructor( ) : Interceptor

    { private val providers = HashMap<Class<out View>, Provider<ViewFactory">>() fun putFactories(input: Map<Class<out View>, Provider<ViewFactory">>) { providers.putAll(input) } override fun intercept(chain: Interceptor.Chain): InflateResult { "// same stuff } }