current position:Home>Android dependency injection (hilt, Koin)

Android dependency injection (hilt, Koin)

2022-01-27 03:50:43 prime

λ:

Warehouse address : github.com/lzyprime/an…

Development branch dev Joined the compose, The picture library consists of glide Replace with coil, DataStore Instead of SharedPreference. At the same time, eliminate LiveData, use Flow Instead of .

I wanted to use it completely compose complete UI Realization . But at the moment compose Poor components , The cooperation with other libraries is not stable . It takes a lot of effort to realize in some scenes . So open two branches :

  • dev: compose Only do part of the control implementation , The main body still retains the traditional library and other ways .
  • dev_compose: view The layer is completely compose Realization , Including routing and navigation . Delete layout, navgation, menu Etc , Delete compose outside Dependence .

hilt perhaps koin Dependency injection runs through the whole situation . So you have to know this first

android Dependency injection Official document

hilt Official website

koin Official website

About documents , Try to read the original English version . The original document itself has a certain delay , The translation of Chinese documents will be delayed for a period . It leads to... In the document api May be out of date , Content obsolescence and other issues . such as hilt stay android Documents on the official website . The Chinese version is still alpha Version of ,@ViewModelInject The interface has been abandoned, etc .

Of course, the introduction and principles in Chinese documents can still be referred to , Even if api Changed the , This kind of thing usually doesn't change .

Generality :

No matter which library , The principle is the same :

  1. stay Application, Activity, Fragment Open a container Container

  2. The dependent instance is from Container gain , And exist as a single example in Container in

Android Manual dependency injection documentation

//example
    class MyApplication : Application() {
        val appContainer = AppContainer()
        ...
    }

    class LoginActivity: Activity() {

        private lateinit var loginViewModel: LoginViewModel

        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)

            val appContainer = (application as MyApplication).appContainer
            loginViewModel = LoginViewModel(appContainer.userRepository)
        }
    }
 Copy code 

Manual dependency injection problem :

  • It has to be managed by itself Container, Manually create instances for dependencies , Remove instances based on lifecycle
  • Sample code
  • ViewModel Dependency injection , Depend on ViewModelProvider.Factory To reuse Jetpack The content in . Or put it in a container , Self maintenance ViewModel Life cycle .

What are the benefits of dependency injection . Of course not , General object, Just a single case , Can be used everywhere .

Dependency injection advantage , The summary given by the official website :

  • Reusing classes and separating dependencies : It's easier to replace the implementation of dependencies . Because of the reversal of control , Code reuse is improved , And classes no longer control how their dependencies are created , It supports any configuration .
  • Easy to refactor : The dependency becomes API Surface The verifiable part of , So you can check when you create an object or when you compile , Instead of hiding as implementation details .
  • Easy to test : Class does not manage its dependencies , So when testing , You can pass in different implementations to test all the different use cases .

Hilt

Hilt The approach is to change and expand the base class , For example, inherited from Application Of , By generated Hilt_XXApplication, Implement container maintenance inside . You can see the generated code .

Open the container

adopt @HiltAndroidApp, @AndroidEntryPoint Mark , Generate the corresponding base class , Implement the maintenance logic of the container in the base class .

@HiltAndroidApp
class UnsplashApplication : Application()

@AndroidEntryPoint
class MainActivity : AppCompatActivity()
 Copy code 

@Inject The tag needs to be injected

class UnsplashRepository @Inject constructor(private val service: UnsplashService)

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject lateinit var service: XXService
    ...
}
 Copy code 

How to obtain the registered instance

establish @Module, adopt @Provides and @Binds How to get the tag instance , And pass @InstallIn Mark the container in which the instance is placed .

@Binds

interface XXService { ... }

class XXServiceImpl @Inject constructor(
  ...
) : XXService { ... }

@Module
@InstallIn(ActivityComponent::class)
abstract class XXModule {

  @Binds
  abstract fun bindAnalyticsService( xxServiceImpl: XXServiceImpl ): XXService
}
 Copy code 

@Provides

interface XXService {
    ...
    companion object : XXService {
        ...
    }
}

@Module
@InstallIn(ActivityComponent::class)
object XXModule {
  @Provides
  fun provideXXService(): XXService = XXService
}
 Copy code 

Both methods can mark the acquisition method of an instance .@InstallIn(ActivityComponent) The marks are stored in Activity The container of Container in . Reference table :

component Injection into create at destroyed at
SingletonComponent Application Application#onCreate() Application#onDestroy()
ActivityRetainedComponent N/A Activity#onCreate() Activity#onDestroy()
ViewModelComponent ViewModel ViewModel created ViewModel destroyed
ActivityComponent Activity Activity#onCreate() Activity#onDestroy()
FragmentComponent Fragment Fragment#onAttach() Fragment#onDestroy()
ViewComponent View View#super() View destroyed
ViewWithFragmentComponent View annotated with @WithFragmentBindings View#super() View destroyed
ServiceComponent Service Service#onCreate() Service#onDestroy()

besides , You can also mark the scope , Mark in which container , Such as :

@Singleton //  Put in Application In the container , Equivalent to global singleton 
class UserRepository @Inject constructor(...)
 Copy code 

Comparison table :

class component Scope
Application SingletonComponent @Singleton
Activity ActivityRetainedComponent @ActivityRetainedScoped
ViewModel ViewModelComponent @ViewModelScoped
Activity ActivityComponent @ActivityScoped
Fragment FragmentComponent @FragmentScoped
View ViewComponent @ViewScoped
View annotated with @WithFragmentBindings ViewWithFragmentComponent @ViewScoped
Service ServiceComponent @ServiceScoped

The final impact of these frills is just the life cycle of the instance , And single case range , If marked Activity Inside , Then each Activity Each will maintain a single instance . And so on

Provide multiple access methods for instances

//  qualifiers 
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class DebugService

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class ReleaseService
 Copy code 
// module
@Module
@InstallIn(ApplicationComponent::class)
object NetworkModule {

  @DebugService
  @Provides
  fun provideDebugService(): XXService = XXService(debug_url)

  @ReleaseService
  @Provides
  fun provideReleaseService(): XXService = XXService(release_url)
}
 Copy code 
// use
class UserRepository @Inject constructor(
    @DebugService service: XXService
)

// or
class Example {
    @Release
    @Inject lateinit var okHttpClient: OkHttpClient
}
 Copy code 

Default qualifier :@ApplicationContext and @ActivityContext

class AnalyticsAdapter @Inject constructor(
    @ActivityContext private val context: Context,
    private val service: AnalyticsService
) { ... }
 Copy code 

ViewModel

Use @HiltViewModel Mark , stay Activity You can still get through by viewModels() To get .

@HiltViewModel
class ExampleViewModel @Inject constructor(
  private val savedStateHandle: SavedStateHandle,
  private val repository: ExampleRepository
) : ViewModel() {
  ...
}

@AndroidEntryPoint
class ExampleActivity : AppCompatActivity() {
  private val exampleViewModel: ExampleViewModel by viewModels()
  ...
}
 Copy code 

Learn how to do it , The first thing to know is by viewModel() How to achieve . The complete function is to pass ViewModel Factory How to get , The default value is empty , When you don't wear anything , Would call Activity#getDefaultViewModelProviderFactory. A default factory will be created .

and Hilt Injection method of implementation , Is to change and expand the base class . So in the generated Activity In the base class ,override Drop this function , Go first Hilt In looking for , If there is no match, the default behavior is returned .

This is a Koin And Hilt One of the obvious differences . Koin It is similar to encapsulating the process of manual injection , therefore by viewModel() yes koin An additional version of the library , With the same name , Are two different functions .

Navigation

You can share a navigation map as a unit ViewModel

val viewModel: ExampleViewModel by hiltNavGraphViewModels(R.id.my_graph)
 Copy code 

gradle plugin The generated code

Thanks to the Hilt adopt Gradle Plugin The generated code , Or we'll :

@HiltAndroidApp(Application.class)
class FooApplication : Hilt_FooApplication

@AndroidEntryPoint(FragmentActivity.class)
class FooActivity : Hilt_FooActivity

...
 Copy code 

Navigation SafeArgs It's the same thing Gradle Plugin Library for generating code , So it's not there dependencies{ implementation "xxx" } But in plugins{ id "xxx" }

Koin

When you understand the concept of container , Look again. Koin, Or other dependencies are easy to understand .Koin The source code in github There are , You can pick it yourself . Or guess how to achieve , See if the library is the same as yourself . Or think the implementation of the library is not elegant or good , Is there a better plan or writing .

Open the container

Koin adopt startKoin

class MainApplication : Application() {

    override fun onCreate() {
        super.onCreate()

        startKoin {
            // Koin Android logger
            androidLogger()
            //inject Android context
            androidContext(this@MainApplication)
            // use modules
            modules(myAppModules)
        }
        
    }
}
 Copy code 

The tag needs to be injected

by inject()

class MySimpleActivity : AppCompatActivity() {
    val firstPresenter: MySimplePresenter by inject()
}
 Copy code 

get()

class MySimpleActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
           val firstPresenter: MySimplePresenter = get()
    }
}
 Copy code 

How to obtain the registered instance

val appModule = module {

    // single instance of HelloRepository
    single<HelloRepository> { HelloRepositoryImpl() }

    // Simple Presenter Factory
    factory { MySimplePresenter(get()) }
}
 Copy code 
startKoin {
    ...
    // use modules
    modules(myAppModules)
}
 Copy code 

module There are detailed documents . As for the life cycle and scope of the instance , It must depend module and startKoin Registration method maintenance .

ViewModel

class MyViewModel(val repo : HelloRepository) : ViewModel() {...}
 Copy code 
val appModule = module {
    // single instance of HelloRepository
    single<HelloRepository> { HelloRepositoryImpl() }

    // MyViewModel ViewModel
    viewModel { MyViewModel(get()) }
}
 Copy code 

Since the injection method is equivalent to manual maintenance of the container , therefore ViewModel You also need to register to get the method .

class MyViewModelActivity : AppCompatActivity() {   
    val myViewModel: MyViewModel by viewModel()
}
 Copy code 

call Koin In the database viewModel function , Get instance .

~λ:

See the documentation and source code for other details . The two libraries have their own advantages and disadvantages , comparison Koin, Hilt Expand the base class , The way the code is generated may be more comfortable .

copyright notice
author[prime],Please bring the original link to reprint, thank you.
https://en.cdmana.com/2022/01/202201270350369428.html

Random recommended