安卓依賴注入
什麼是依賴注入
依賴注入(DI,Dependency Injection)是一種廣泛的程式設計技術。把依賴(所需物件)傳遞給其它物件建立,好處是類的耦合更加鬆散,遵循依賴倒置的原則。
類獲取所需物件
class Engine {
fun start() {
println("engine start")
}
}
class Car {
private val engine: Engine = Engine()
fun start(){
engine.start()
}
}
Car對Engine強依賴,如果需要其它型別的Engine,需要增加一個新的Engine,必須對Car進行改動。
依賴注入獲取所需物件
interface Engine {
fun start()
}
class VEngine : Engine{
override fun start() {
println("VEngine start")
}
}
class WEngine : Engine{
override fun start() {
println("WEngine start")
}
}
class Car(private val engine: Engine) {
fun start(){
engine.start()
}
}
在建構函式中接收Engine物件作為引數,而不是初始化時構造自己的Engine物件,這就叫做依賴注入。
依賴注入還有很多其它的方式,如變數的get/set,就一一不介紹了。
安卓中手動實現依賴注入
手動實現依賴注入,就是依賴注入的原理。依賴注入框架會生成同樣功能的樣板程式碼。
假設有個登入場景,流程大概是這樣:
LoginActivity->LoginViewModel->UserRepository
class UserRepository(
private val localDataSource: UserLocalDataSource,
private val remoteDataSource: UserRemoteDataSource
)
class UserLocalDataSource()
class UserRemoteDataSource(
private val loginService: LoginRetrofitService
)
在需要的位置手動注入
在需要的地方建立依賴,缺點比較明顯:
- 大量的樣板程式碼
- 必須需要按照順序宣告依賴
- 複用困難。
/**
* 在LoginActivity的onCreate函式里:
*/
//建立UserRemoteDataSource需要的依賴
val retrofit = Retrofit.Builder()
.baseUrl("https://example.com")
.build()
.create(LoginService::class.java)
//建立Repository需要的依賴
val remoteDataSource = UserRemoteDataSource(retrofit)
val localDataSource = UserLocalDataSource()
//建立ViewModel需要的依賴
val userRepository = UserRepository(localDataSource, remoteDataSource)
//建立ViewModel
loginViewModel = LoginViewModel(userRepository)
使用容器管理依賴
如果想要複用物件,可以建立一個類來初始化所需的依賴。
class AppContainer {
private val retrofit = Retrofit.Builder()
.baseUrl("https://example.com").build()
.create(LoginService::class.java)
private val remoteDataSource = UserRemoteDataSource(retrofit)
private val localDataSource = UserLocalDataSource()
val userRepository = UserRepository(localDataSource, remoteDataSource)
}
如果需要在整個app中使用,可以放到application中:
class MyApplication : Application(){
val appContainer = AppContainer()
}
/**
* 在LoginActivity的onCreate函式里:
*/
val appContainer = (application as MyApplication).appContainer
loginViewModel = LoginViewModel(appContainer.userRepository)
使用容器來管理依賴還是有樣板程式碼,且需要手動為依賴項建立例項物件。
管理依賴項的宣告週期
之前把UserRepository的生命週期放在了application,在app被關閉前永遠不會被釋放。如果資料非常大,會導致記憶體佔用過高。
假如,只有在LoginActivity需要依賴,其它位置不需要依賴:
- AppContainer 內部需要新增一個LoginContainer,用來存放UserRepository。
- 在LoginActivity:onCreate時手動建立LoginContainer並放到AppContainer,onDestory時把AppContainer設定為null,主動釋放引用。
根據生命週期來管理依賴項,這樣時比較合理的。
Dagger實現依賴注入
什麼是Dagger
Dagger是一個依賴注入的庫,透過自動生成程式碼的方式,實現依賴注入。由於是在編譯時生成的程式碼,效能會高於基於反射的方案。Dagger生成的程式碼和手動實現依賴注入生成的程式碼相似。
- 每次呼叫函式都重新建立物件
//@Inject 註解會告訴Dagger,需要注入.
class UserRepository @Inject constructor(
private val localDataSource: UserLocalDataSource,
private val remoteDataSource: UserRemoteDataSource
){
init {
println("UserRepository Created")
}
}
class UserLocalDataSource @Inject constructor() {
init {
println("UserLocalDataSource Created")
}
}
class UserRemoteDataSource @Inject constructor(){
init {
println("UserRemoteDataSource Created")
}
}
//DaggerApplicationGraph.create() 會建立新物件
private val applicationGraph:ApplicationGraph = DaggerApplicationGraph.create()
//applicationGraph.repository() 會建立新物件
private val repository1 = applicationGraph.repository()
private val repository2 = applicationGraph.repository()
/**
輸出如下:
UserLocalDataSource Created
UserRemoteDataSource Created
UserRepository Created
UserLocalDataSource Created
UserRemoteDataSource Created
UserRepository Created
*/
- 首次建立物件,之後全域性複用這個單例物件.
@Singleton
class UserRepository @Inject constructor(
private val localDataSource: UserLocalDataSource,
private val remoteDataSource: UserRemoteDataSource
){
init {
println("UserRepository Created")
}
}
class UserLocalDataSource @Inject constructor() {
init {
println("UserLocalDataSource Created")
}
}
class UserRemoteDataSource @Inject constructor(){
init {
println("UserRemoteDataSource Created")
}
}
/**
* @Component 註解,用於 interface
* Dagger會生成一個對應的類,以Dagger開頭,ApplicationGraph就是DaggerApplicationGraph
* 呼叫函式會返回對應的物件, Dagger會自動新增依賴
* @Singleton 註解,用於標識為全域性單例
*/
@Singleton
@Component
interface ApplicationGraph {
fun repository(): UserRepository
}
@Singleton
@Component
interface LoginGraph {
fun repository(): UserRepository
}
class One{
//DaggerApplicationGraph.create() 會建立新物件
private val applicationGraph:ApplicationGraph = DaggerApplicationGraph.create()
//applicationGraph.repository() 會建立新物件
private val repository1 = applicationGraph.repository()
private val repository2 = applicationGraph.repository()
init {
println("One Created")
}
}
class Two{
private val loginGraph:LoginGraph = DaggerLoginGraph.create()
private val repository1 = loginGraph.repository()
private val repository2 = loginGraph.repository()
init {
println("Two Created")
}
}
/**
One 和 Two 使用同一個物件,因為@Singleton註解是全域性單例
輸出如下:
UserLocalDataSource Created
UserRemoteDataSource Created
UserRepository Created
One Created
UserLocalDataSource Created
UserRemoteDataSource Created
UserRepository Created
Two Created
*/
在安卓中使用Dagger
- 對Activity中的欄位進行注入
/**
* 為了演示方便,這裡沒有繼承ViewModel
*/
class LoginViewModel @Inject constructor(
private val userRepository: UserRepository
) {
init {
println("LoginViewModel Created")
}
}
/**
* Activity 是用安卓系統例項化的,所以無法被Dagger建立
* 初始化的程式碼需要放在onCreate方法中
* 使用手動呼叫inject的方式,對欄位進行注入,需要被注入的欄位必須有@Inject註解
*/
class LoginActivity : AppCompatActivity() {
@Inject lateinit var loginViewModel: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
//呼叫inject,告訴Dagger,可以對當前物件的@Inject欄位進行注入了
(applicationContext as MyApplication).applicationGraph.inject(this)
//呼叫完成,loginViewModel可以使用了
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
}
}
/**
* @Component 註解,用於 interface * Dagger會生成一個對應的類
* 呼叫函式會返回對應的物件, Dagger會自動新增依賴
* @Singleton 註解,用於標識為全域性單例
* inject 呼叫函式手動注入帶有@Inject註解的欄位,函式名稱是任意的,引數是需要注入的物件.
*/
@Singleton
@Component
interface ApplicationGraph {
fun repository(): UserRepository
fun inject(activity: LoginActivity)
}
class MyApplication : Application() {
val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()
}
- 主動告知如何提供例項
//增加一個引數
class UserRemoteDataSource @Inject constructor(private val loginService: LoginService){
init {
println("UserRemoteDataSource Created")
}
fun login(){
loginService.login()
}
}
//LoginService 只能透過Builder.create()建立
interface LoginService {
private class LoginServiceImpl : LoginService{
init {
println("LoginServiceImpl Created")
}
}
fun login() = println("login")
class Builder{
fun create(): LoginService {
return LoginServiceImpl()
}
}
}
/**
* @DisableInstallInCheck 用於遮蔽繫結生命週期,這個是hilt的內容.
* @Module 是dagger的註解,用來告訴dagger可以提供例項物件.
* @Provides 用於@Module註解內,提供對應型別的例項物件,函式名任意.也可以新增@Singleton註解建立單例.
*/
@DisableInstallInCheck
@Module
class LoginModule {
@Provides
fun providerLoginService(): LoginService{
return LoginService.Builder().create()
}
}
/**
* @Component 註解,用於 interface * Dagger會生成一個對應的類,以Dagger開頭,ApplicationGraph就是DaggerApplicationGraph
* modules 引數用來指定物件該如何提供,必須帶有@Module註解
* 呼叫函式會返回對應的物件, Dagger會自動新增依賴
* @Singleton 註解,用於標識為全域性單例
* inject 呼叫函式手動注入帶有@Inject註解的欄位,函式名稱是任意的,引數是需要注入的物件.
*/
@Singleton
@Component(modules = [LoginModule::class])
interface ApplicationGraph {
fun repository(): UserRepository
fun inject(activity: LoginActivity)
}
- 子元件和作用域,限定作用域為生命週期
class MyApplication : Application() {
val applicationGraph: ApplicationComponent = DaggerApplicationComponent.create()
}
@Singleton
@Component(modules = [LoginModule::class, Subcomponent::class])
interface ApplicationComponent {
fun loginComponent(): LoginComponent.Factory
}
@Scope
@Retention(value = AnnotationRetention.RUNTIME)
annotation class ActivityScope
@ActivityScope
class LoginViewModel @Inject constructor(
private val userRepository: UserRepository,
) {
init {
println("LoginViewModel Created")
}
fun login(){
userRepository.login()
}
}
@ActivityScope
@Subcomponent
interface LoginComponent {
@Subcomponent.Factory
interface Factory{
fun create(): LoginComponent
}
fun inject(activity: LoginActivity)
fun inject(fragment: LoginFragment)
fun repository(): UserRepository
}
@DisableInstallInCheck
@Module
class LoginModule {
@Singleton
@Provides fun providerLoginService(): LoginService{
return LoginService.Builder().create()
}
}
@ActivityScope
class UserRepository @Inject constructor(
private val localDataSource: UserLocalDataSource,
private val remoteDataSource: UserRemoteDataSource
){
init {
println("UserRepository Created")
}
fun login(){
remoteDataSource.login()
}
}
class UserRemoteDataSource @Inject constructor(
private val loginService: LoginService,
private val loginService2: LoginService,
){
init {
println("UserRemoteDataSource Created")
}
fun login(){
loginService.login()
}
}
class UserLocalDataSource @Inject constructor() {
init {
println("UserLocalDataSource Created")
}
}
class LoginActivity : AppCompatActivity() {
lateinit var loginComponent: LoginComponent
@Inject lateinit var loginViewModel: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
loginComponent = (application as MyApplication).applicationGraph.loginComponent().create()
loginComponent.inject(this)
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
findViewById<Button>(R.id.activity_login_bt_open).setOnClickListener {
startActivity(Intent(this, LoginActivity::class.java))
}
}
}
class LoginFragment : Fragment() {
@Inject
lateinit var loginViewModel: LoginViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
(activity as LoginActivity).loginComponent.inject(this)
val view = inflater.inflate(R.layout.fragment_login, container, false)
view.findViewById<Button>(R.id.fragment_login_bt_login).setOnClickListener {
loginViewModel.login()
}
return view
}
}
@ActivityScope
class LoginViewModel @Inject constructor(
private val userRepository: UserRepository,
) {
init {
println("LoginViewModel Created")
}
fun login(){
userRepository.login()
}
}
interface LoginService {
private class LoginServiceImpl : LoginService{
init {
println("LoginServiceImpl Created")
}
}
fun login() = println("login")
class Builder{
fun create(): LoginService {
return LoginServiceImpl()
}
}
}
@ActivityScope
@Subcomponent
interface LoginComponent {
@Subcomponent.Factory
interface Factory{
fun create(): LoginComponent
}
fun inject(activity: LoginActivity)
fun inject(fragment: LoginFragment)
fun repository(): UserRepository
}
透過限定作用域的方式,把依賴注入和生命週期繫結,Activity和Activity中的Fragment使用同一個ViewModel。
Dagger雖然可以實現精細的依賴注入,但是使用起來非常繁瑣。
Hilt實現依賴注入
什麼是Hilt
Hilt是基於Dagger構建的用於安卓的依賴注入庫,簡化在安卓上實現依賴注入。
把依賴項注入安卓類
Hilt可以很方便的注入到安卓類中
比如把ViewModel
class UserRepository @Inject constructor(
private val localDataSource: UserLocalDataSource,
private val remoteDataSource: UserRemoteDataSource
){
init {
println("UserRepository Created")
}
fun login(){
remoteDataSource.login()
}
}
class UserLocalDataSource @Inject constructor() {
init {
println("UserLocalDataSource Created")
}
}
class UserRemoteDataSource @Inject constructor(){
init {
println("UserRemoteDataSource Created")
}
fun login(){
println("login")
}
}
class LoginViewModel @Inject constructor(
private val userRepository: UserRepository,
) {
init {
println("LoginViewModel Created")
}
fun login(){
userRepository.login()
}
}
/**
* @AndroidEntryPoint 註解,可以被Hilt注入
*/
@AndroidEntryPoint
class LoginActivity : AppCompatActivity() {
@Inject lateinit var viewModel: LoginViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
findViewById<Button>(R.id.activity_login_bt_open).setOnClickListener {
startActivity(Intent(this, LoginActivity::class.java))
}
}
}
class LoginFragment : Fragment() {
lateinit var viewModel: LoginViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(R.layout.fragment_login, container, false)
viewModel = (activity as LoginActivity).viewModel
view.findViewById<Button>(R.id.fragment_login_bt_login).setOnClickListener {
viewModel.login()
}
return view
}
}
/**
* @HiltAndroidApp 註解:
* 必須在Application 中新增, Hilt會生成一個類作為依賴的容器, 作為依賴注入的入口.
*/@HiltAndroidApp
class MyApplication : Application()
Hilt模組
模組的Bind
可以被直接構造的介面實現可以透過Bind注入
class UserRemoteDataSource @Inject constructor(
private val analyticsService: AnalyticsService
){
init {
println("UserRemoteDataSource Created")
}
fun login(){
println("login")
analyticsService.logEvent("login")
}
}
interface AnalyticsService {
fun logEvent(eventName: String)
}
class AnalyticsServiceImpl @Inject constructor(
@ApplicationContext val context: Context
) : AnalyticsService {
override fun logEvent(eventName: String) {
println("Context: $context LogEvent: $eventName")
}
}
@Module
@InstallIn(ActivityComponent::class)
abstract class AnalyticsServiceModule {
@Binds
abstract fun bindAnalyticsService(analyticsService: AnalyticsServiceImpl): AnalyticsService
}
模組的Provider
無法被直接構造的介面實現可以透過Provider注入
@Module
@InstallIn(ActivityComponent::class)
class LoginServiceModule {
@Provides
fun provideLoginService(): LoginService {
return LoginServiceImpl.Builder().build()
}
}
interface LoginService {
fun login()
}
class LoginServiceImpl private constructor(): LoginService {
class Builder {
fun build(): LoginService {
return LoginServiceImpl()
}
}
init {
println("LoginServiceImpl Created")
}
override fun login() {
println("login")
}
}
class UserRemoteDataSource @Inject constructor(
private val analyticsService: AnalyticsService,
private val loginService: LoginService
){
init {
println("UserRemoteDataSource Created")
}
fun login(){
loginService.login()
analyticsService.logEvent("login")
}
}
同一型別提供多個繫結
Provides如果不帶限定標籤,只會返回一種型別的實現。
可以透過限定標籤來區分,返回對應的實現。
Hilt限定符預設提供了 @ApplicationContex 和 @ActivityContext,用來提供兩種不同型別的Context
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class DebugLog
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class ErrorLog
@Module
@InstallIn(SingletonComponent::class)
object LogServiceModule {
@DebugLog
@Provides
fun provideDebugLogger(): LogService {
return LogServiceDebugImpl()
}
@ErrorLog
@Provides
fun provideErrorLogger(): LogService {
return LogServiceErrorImpl()
}
}
@Module
@InstallIn(ActivityComponent::class)
class LoginServiceModule {
@Provides
fun provideLoginService(@DebugLog logService: LogService): LoginService {
return LoginServiceImpl.Builder().build(logService)
}
}
class LoginServiceImpl private constructor(val logService:LogService): LoginService {
class Builder {
fun build(logService:LogService): LoginService {
return LoginServiceImpl(logService)
}
}
init {
logService.log("LoginServiceImpl Created")
}
override fun login() {
logService.log("login")
}
}
class AnalyticsServiceImpl @Inject constructor(
@ApplicationContext val context: Context,
@ErrorLog val logService: LogService
) : AnalyticsService {
override fun logEvent(eventName: String) {
logService.log("Context: $context LogEvent: $eventName")
}
}
Hilt為安卓類生成的元件
元件生命週期和作用域
Hilt元件 | 建立時機 | 銷燬時機 | 作用域 | 備註 |
---|---|---|---|---|
SingletonComponent | Application#onCreate() | Application被銷燬 | @Singleton | 相當於是單例的 |
ActivityComponent | Activity#onCreate() | Activity#onDestroy() | @ActivityScoped | 會隨著生命週期注入 |
ActivityRetainedComponent | 首次Activity#onCreate() | 最後一次Activity#onDestroy() | @ActivityRetainedScoped | Fragment的ViewModel會隨著Fragment回收,但ActivityRetainedComponent只會隨著Activity回收,比ViewModel生命週期更長。 |
ViewModelComponent | ViewModel 已建立 | ViewModel 已銷燬 | @ViewModelScoped | 和ViewModel的生命週期相同 |
ViewComponent | View#super() | View 已銷燬 | @ViewScoped | 和View的生命週期相同 |
ViewWithFragmentComponent | View#super() | View的擁有者被銷燬 | @ViewScoped帶有 @WithFragmentBindings註解的View | 比如Fragment導航離開螢幕,Fragment還在,但View被銷燬時仍然保留。 |
FragmentComponent | Fragment#onAttach() | Fragment#onDestroy() | @FragmentScoped | 和Fragment的生命週期相同 |
ServiceComponent | Service#onCreate() | Service#onDestroy() | @ServiceScoped | 和Service的生命週期相同 |
元件預設繫結
可以使用Model安裝到預設繫結,實現注入。比如上面提到的"同一型別提供多個繫結"
安卓元件 | 預設繫結 |
---|---|
SingletonComponent | Application |
ActivityRetainedComponent | Application |
ViewModelComponent | SavedStateHandle |
ActivityComponent | Application、Activity |
FragmentComponent | Application、Activity、Fragment |
ViewComponent | Application、Activity、View |
ViewWithFragmentComponent | Application、Activity、Fragment、View |
ServiceComponent | Application、Service |
在Hilt不支援的類中注入依賴項
使用@EntryPoint讓任意介面可以被注入.
使用@EntryPointAccessors獲取被注入的物件.
因為是SingletonComponent,所以要使用Application的Context 。如果是ActivityComponent就需要使用Activity的Context。
class ExampleContentProvider : ContentProvider() {
@EntryPoint
@InstallIn(SingletonComponent::class)
interface ExampleContentProviderEntryPoint {
fun analyticsService(): AnalyticsService
}
fun doSomeThing(){
val appContext = context?.applicationContext ?: throw IllegalStateException()
val hiltEntryPoint = EntryPointAccessors.fromApplication(appContext, ExampleContentProviderEntryPoint::class.java)
val analyticsService = hiltEntryPoint.analyticsService()
}
}