安卓架構元件-依賴注入

laomuji666發表於2024-06-06

安卓依賴注入

什麼是依賴注入

依賴注入(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  
)

在需要的位置手動注入

在需要的地方建立依賴,缺點比較明顯:

  1. 大量的樣板程式碼
  2. 必須需要按照順序宣告依賴
  3. 複用困難。
/**
* 在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需要依賴,其它位置不需要依賴:

  1. AppContainer 內部需要新增一個LoginContainer,用來存放UserRepository。
  2. 在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()
	}
}

相關文章