Dagger Hilt 初探

介紹

Dagger Hilt (這名字起的溜...........)html

官方描述其設計目的:java

  • To simplify Dagger-related infrastructure for Android apps.
  • To create a standard set of components and scopes to ease setup, readability/understanding, and code sharing between apps.
  • To provide an easy way to provision different bindings to various build types (e.g. testing, debug, or release).

簡單說就是Dagger Android的瘦身包,使依賴注入在Android開發中標準化、簡單化。android

集成

首先在項目級別的build.gradle文件中添加如下內容,這將使咱們可以訪問hilt gradle插件:git

classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'
複製代碼

而後在應用程序級別的build.gradle文件並應用此插件:github

apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'
複製代碼

最後,在應用程序級別build.gradle文件中添加所需的hilt依賴項:segmentfault

implementation "com.google.dagger:hilt-android:$hilt_version"
kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha01'
kapt 'androidx.hilt:hilt-compiler:1.0.0-alpha01'
複製代碼

這樣Hilt就可使用了。api

Hilt Application

按照官方要求,首先須要在自定義的Application類中添加@HiltAndroidApp註解:緩存

@HiltAndroidApp
class APP:Application()
複製代碼

這有什麼做用?如下來自官方描述:bash

All apps using Hilt must contain an Application class annotated with @HiltAndroidApp. @HiltAndroidApp kicks off the code generation of the Hilt components and also generates a base class for your application that uses those generated components. Because the code generation needs access to all of your modules, the target that compiles your Application class also needs to have all of your Dagger modules in its transitive dependencies.app

Just like other Hilt Android entry points, Applications are members injected as well. This means you can use injected fields in the Application after super.onCreate() has been called.

Daager2中,須要Application繼承DaggerApplication,而且還須要建立Application的Module。 這裏只須要使用@HiltAndroidApp的註解就能夠完成對Application的依賴注入,由Hilt gradle插件生成對應的文件

構建後咱們看到在 app/build/generated/source/kapt/debug/目錄下生成了一個Hilt_APP的抽象類:

/**
 * A generated base class to be extended by the @dagger.hilt.android.HiltAndroidApp annotated class. If using the Gradle plugin, this is swapped as the base class via bytecode transformation. */
@Generated("dagger.hilt.android.processor.internal.androidentrypoint.ApplicationGenerator")
public abstract class Hilt_APP extends Application implements GeneratedComponentManager<Object> {
  private final ApplicationComponentManager componentManager = new ApplicationComponentManager(new ComponentSupplier() {
    @Override
    public Object get() {
      return DaggerAPP_HiltComponents_ApplicationC.builder()
          .applicationContextModule(new ApplicationContextModule(Hilt_APP.this))
          .build();
    }
  });

  protected final ApplicationComponentManager componentManager() {
    return componentManager;
  }

  @Override
  public final Object generatedComponent() {
    return componentManager().generatedComponent();
  }

  @CallSuper
  @Override
  public void onCreate() {
    // This is a known unsafe cast, but is safe in the only correct use case:
    // APP extends Hilt_APP
    ((APP_GeneratedInjector) generatedComponent()).injectAPP(UnsafeCasts.<APP>unsafeCast(this));
    super.onCreate();
  }
}

複製代碼
  • ApplicationComponentManager的聲明
  • onCreate函數中注入Application類

看下里面涉及到的~

ApplicationComponentManager

public final class ApplicationComponentManager implements GeneratedComponentManager<Object> {
  private volatile Object component;
  private final Object componentLock = new Object();
  private final ComponentSupplier componentCreator;

  public ApplicationComponentManager(ComponentSupplier componentCreator) {
    this.componentCreator = componentCreator;
  }

  @Override
  public Object generatedComponent() {
    if (component == null) {
      synchronized (componentLock) {
        if (component == null) {
          component = componentCreator.get();
        }
      }
    }
    return component;
  }
}
複製代碼

主要用於管理應用程序中的Hilt Component的建立

  • 構造中建立ComponentSupplier實例。

  • generatedComponent():經過對象鎖獲取ComponentSupplier類中的Object,並負責在onCreate函數中將依賴項注入到咱們的應用程序類中。

ComponentSupplier

提供component的接口

/**
 * Interface for supplying a component. This is separate from the Supplier interface so that
 * optimizers can strip this method (and therefore all the Dagger code) from the main dex even if a
 * Supplier is referenced in code kept in the main dex.
 */
public interface ComponentSupplier {
  Object get();
}

複製代碼

ApplicationC

代碼比較長就不貼出來了,看下builder:

import dagger.hilt.android.internal.builders.ActivityComponentBuilder;
import dagger.hilt.android.internal.builders.ActivityRetainedComponentBuilder;
import dagger.hilt.android.internal.builders.FragmentComponentBuilder;
import dagger.hilt.android.internal.builders.ServiceComponentBuilder;
import dagger.hilt.android.internal.builders.ViewComponentBuilder;
import dagger.hilt.android.internal.builders.ViewWithFragmentComponentBuilder;
複製代碼

ComponentSupplier實現的內部,咱們能夠看到對ApplicationC(Application組件)類的引用。DaggerAPP_HiltComponents_ApplicationC是生成的應用程序組件,它充當Hilt在咱們的應用程序中使用的(Activity,Fragment,Service,View等組件)的全局容器。

GeneratedInjector

@OriginatingElement(
    topLevelClass = APP.class
)
@GeneratedEntryPoint
@InstallIn(ApplicationComponent.class)
@Generated("dagger.hilt.android.processor.internal.androidentrypoint.InjectorEntryPointGenerator")
public interface APP_GeneratedInjector {
  void injectAPP(APP aPP);
}
複製代碼

接口類提供injectAPP方法爲外部類提供了一個訪問點,以觸發應用程序Component的注入。

ApplicationContextModule

@Module
@InstallIn(ApplicationComponent.class)
public final class ApplicationContextModule {
  private final Context applicationContext;

  public ApplicationContextModule(Context applicationContext) {
    this.applicationContext = applicationContext;
  }

  @Provides
  @ApplicationContext
  Context provideContext() {
    return applicationContext;
  }

  @Provides
  Application provideApplication() {
    return (Application) applicationContext.getApplicationContext();
  }

}
複製代碼

主要是提供ApplicationContext,經過**@InstalIIn**注入到 ApplicationComponent便於後續使用

這裏有個@ApplicationContext 這是個qualifers 限定符,Hilt還提供了一個@ActivityContext

例如:

class AnalyticsAdapter @Inject constructor(
    @ActivityContext private val context: Context,
    private val service: AnalyticsService
) { ... }
複製代碼
@Singleton
class NetWorkUtils @Inject constructor(@ApplicationContext private val context: Context) {
    fun isNetworkConnected(): Boolean {
        .....
    }
}
複製代碼

能夠直接做爲@Provides方法或@Inject構造的參數使用。

流程

Hilt Components

介紹

在以前Dagger-Android中,咱們必須建立諸如ActivityScope,FragmentScope之類的範圍註釋,以管理對象的生命週期,

而這裏只要使用@InstallIn的註解,就能夠委託Hilt幫咱們管理

組件的生存期:

組件 範圍 建立 銷燬
ApplicationComponent @Singleton Application#onCreate() Application#onDestroy()
ActivityRetainedComponent @ActivityRetainedScope Activity#onCreate()連接 Activity#onDestroy()連接
ActivityComponent @ActivityScoped Activity#onCreate() Activity#onDestroy()
FragmentComponent @FragmentScoped Fragment#onAttach() Fragment#onDestroy()
ViewComponent @ViewScoped View#super() View 銷燬
ViewWithFragmentComponent @ViewScoped View#super() View 銷燬
ServiceComponent @ServiceScoped Service#onCreate() Service#onDestroy()

@InstallIn模塊中肯定綁定範圍時,綁定上的範圍必須與component範圍匹配。例如,@InstallIn(ActivityComponent.class)模塊內的綁定只能用限制範圍@ActivityScoped

例如咱們須要在App中共享OkHttp的配置:

@Module
@InstallIn(ApplicationComponent::class)
class ApplicationModule {

    @Provides
    fun provideBaseUrl() = "...."

    @Provides
    @Singleton
    fun provideOkHttpClient() = if (BuildConfig.DEBUG) {
        val loggingInterceptor = HttpLoggingInterceptor()
        loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY)
        OkHttpClient.Builder()
            .addInterceptor(loggingInterceptor)
            .build()
    } else OkHttpClient
        .Builder()
        .build()


    @Provides
    @Singleton
    fun provideRetrofit(
        okHttpClient: OkHttpClient,
        BASE_URL: String
    ): Retrofit =
        Retrofit.Builder()
            .addConverterFactory(MoshiConverterFactory.create())
            .baseUrl(BASE_URL)
            .client(okHttpClient)
            .build()

    @Provides
    @Singleton
    fun provideApiService(retrofit: Retrofit): ApiService = retrofit.create(ApiService::class.java)

}
複製代碼

相似Dagger中:

@Module
class NetworkModule {
        // Hypothetical dependency on LoginRetrofitService
        @Provides
        fun provideLoginRetrofitService(
            okHttpClient: OkHttpClient
        ): LoginRetrofitService { ... }
}

@Component(modules = [NetworkModule::class])
interface ApplicationComponent {
        ...
}
複製代碼

這裏經過@InstallIn(ApplicationComponent::class)Hilt幫咱們管理ApplicationModule的生命週期

Module

module的使用基本和dagger中同樣, 用來提供一些沒法用構造@Inject的依賴, 好比接口, 第三方庫類型, Builder模式構造的對象等.

  • @Module: 標記一個module, 能夠是一個object.
  • @Provides: 標記方法, 提供返回值類型的依賴.這裏就不須要手動添加到@Component(modules = ...)
  • @Binds: 標記抽象方法, 返回接口類型, 接口實現是方法的惟一參數.
interface AnalyticsService {
  fun analyticsMethods()
}

// Constructor-injected, because Hilt needs to know how to
// provide instances of AnalyticsServiceImpl, too.
class AnalyticsServiceImpl @Inject constructor(
  ...
) : AnalyticsService { ... }

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

  @Binds
  abstract fun bindAnalyticsService(
    analyticsServiceImpl: AnalyticsServiceImpl
  ): AnalyticsService
}
複製代碼

@Provides@Binds的區別:

按照官方說@Binds須要module是一個abstract class,@Provides須要module是一個object.並且@Binds須要在方法參數裏面明確指明接口的實現類

可是@Provides這麼用也是能夠的。

Qualifier

若是要提供同一個接口的不一樣實現, 能夠用不一樣的註解來標記. (相似於dagger中是@Named).

什麼意思? 好比咱們緩存接口有內存和磁盤兩種實現:

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class CacheInMemory

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class CacheInDisk
複製代碼

module中提供的時候用來標記相應的依賴:

@InstallIn(ApplicationComponent::class)
@Module
object CacheModule {

    @CacheInMemory
    @Singleton
    @Provides
    fun getCacheInMemory(memoryImpl: CacheSourceMemoryImpl): CacheSource = memoryImpl
    
    @CacheInDisk
    @Singleton
    @Provides
    fun getCacheInDisk(diskImpl: CacheSourceDiskImpl): CacheSource = diskImpl
}

複製代碼

Android types

@AndroidEntryPoint

Dagger2中,對Activity和Fragment的注入依賴的使用比較麻煩。

@Module
abstract class ActivityModule {

  @ActivityScope
  @ContributesAndroidInjector(modules = [MainActivityFragmentModule::class])
  internal abstract fun contributeMainActivity(): MainActivity

  @ActivityScope
  @ContributesAndroidInjector
  internal abstract fun contributeMovieDetailActivity(): MovieDetailActivity

  @ActivityScope
  @ContributesAndroidInjector
  internal abstract fun contributeTvDetailActivity(): TvDetailActivity

  @ActivityScope
  @ContributesAndroidInjector
  internal abstract fun contributePersonDetailActivity(): PersonDetailActivity
}
複製代碼

Hilt中就比較簡單了,只須要@AndroidEntryPoint的註解。至關於上面的@ContributesAndroidInjector

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    //
    private val mJokesViewModel: JokesViewModel by viewModels()
     private val mBinding: ActivityMainBinding by viewBinding {
        ActivityMainBinding.inflate(layoutInflater)
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(mBinding.root)
        mJokesViewModel.jokes.observe(this, Observer {
        		........
        }

    }
}
複製代碼

咱們不須要編寫AndroidInjection.inject(this)或擴展DaggerAppCompatActivity類。

這裏官方給了限定範圍:

  • Activity
  • Fragment
  • View
  • Service
  • BroadcastReceiver

Hilt currently only supports activities that extend ComponentActivity and fragments that extend androidx library Fragment, not the (now deprecated) Fragment in the Android platform.

Hilt目前不直接支持 content providers.

@EntryPoint

Hilt支持最經常使用的Android組件, 對於默認不支持的類型, 若是要作字段注入, 須要用@EntryPoint.

這裏只是限制了字段注入的狀況, 對於自定義類型咱們通常習慣於用構造注入。

必須與@InstallIn搭配使用,將interface標記爲入口點,這樣就可使用Hilt容器提供的依賴對象們.

若是要content provider使用Hilt

class ExampleContentProvider : ContentProvider() {

  @EntryPoint
  @InstallIn(ApplicationComponent::class)
  interface ExampleContentProviderEntryPoint {
    fun analyticsService(): AnalyticsService
  }

  ...
}
複製代碼

要訪問@EntryPoint,使用靜態方法 EntryPointAccessors

class ExampleContentProvider: ContentProvider() {
    ...

  override fun query(...): Cursor {
    val appContext = context?.applicationContext ?: throw IllegalStateException()
    val hiltEntryPoint =
      EntryPointAccessors.fromApplication(appContext, ExampleContentProviderEntryPoint::class.java)

    val analyticsService = hiltEntryPoint.analyticsService()
    ...
  }
}
複製代碼

appContext參數要與@InstallIn(ApplicationComponent::class)保持一致。

@EntryPoint除了解決上述字段注入的 問題,還有什麼場景能夠發揮用處?

生命週期匹配

咱們利用FragmentFactoryActivityFragment之間用構造函數傳遞數據 :

class ContainerActivity : AppCompatActivity() {
    
    private var fragmentDataTest = FragmentDataTest()
    private val mBinding: ActivityContainerBinding by viewBinding {
        ActivityContainerBinding.inflate(layoutInflater)
    }
    override fun onCreate(savedInstanceState: Bundle?) {
        supportFragmentManager.fragmentFactory = EntryPointFragmentFactory(fragmentDataTest)
        super.onCreate(savedInstanceState)
        setContentView(mBinding.root)
        setSupportActionBar(mBinding.toolbar)
    }
}
複製代碼

看上述代碼,在沒有用Hilt以前fragmentFactory的設置應該是在 super.onCreate()以前,可是若是用Hilt就不能這麼寫了,由於在

以前Hilt Application中已經說過,Hilt是在super.onCreate()中進行依賴注入的,在Hilt_ContainerActivity類中:

@CallSuper
  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
    inject();
    super.onCreate(savedInstanceState);
  }
複製代碼

Injection happens in super.onCreate().

因此,若是咱們要在ContainerActivity中使用FragmentFactory就該在super.onCreate()以後,那麼問題來了...咱們知道FragmentFactory 負責在 Activity 和 parent Fragment 初始化 Fragment,應該在 super.onCreate() 以前關聯 FragmentFactory 和 FragmentManager設置。若是在使用Hilt注入以後仍是放在編譯會報錯:

UninitializedPropertyAccessException: lateinit property mFragmentFactory has not been initialized
複製代碼

因此咱們將FragmentManager綁定FragmentFactory的動做放在super.onCreate()以後:

@AndroidEntryPoint
class ContainerActivity : AppCompatActivity() {

    @Inject lateinit var mFragmentFactory: EntryPointFragmentFactory

    private val mBinding: ActivityContainerBinding by viewBinding {
        ActivityContainerBinding.inflate(layoutInflater)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        supportFragmentManager.fragmentFactory = mFragmentFactory
        setContentView(mBinding.root)
        mFragmentFactory.fragmentDataTest.setData("xxx")
    }
}
複製代碼

這樣就能夠了,可是要注意了:

若是上面的ContainerActivity被意外終止而觸發重建的話是會報錯的 :

java.lang.RuntimeException: Unable to start activity ComponentInfo{tt.reducto.daggerhiltsample/tt.reducto.daggerhiltsample.ui.entry.ContainerActivity}: androidx.fragment.app.Fragment$InstantiationException: Unable to instantiate fragment tt.reducto.daggerhiltsample.ui.entry.EntryPointFragment: could not find Fragment constructor
 ...
Caused by: androidx.fragment.app.Fragment$InstantiationException: Unable to instantiate fragment tt.reducto.daggerhiltsample.ui.entry.EntryPointFragment: could not find Fragment constructor
  
複製代碼

能夠定位 Hilt_ContainerActivity中的super.onCreate(savedInstanceState);這裏就涉及到FragmentManager的狀態保存與恢復

Fragment依附於Activity,而Fragment的狀態保存與恢復機制也是由Activity的相關方法觸發。Activity的方法onSaveInstanceState(Bundle outState)的參數outState是系統在狀態須要保存時用來提供存放持久化狀態的容器,當系統觸發狀態保存時,Activity下的Fragment的全部狀態便經過mFragments的saveAllState方法保存在了 FRAGMENTS_TAG 鍵中,在Activity重建 的 時候經過mFragments.restoreAllState入口將狀態恢復

在此期間Fragment的都會交由FragmentManager管理,包括咱們須要注意的如何新建一個Fragment對象:

Fragment.instantiate(…)方法會根據所給的class name加載對應的Class類,調用clazz.newInstance()新建一個全新的Fragment對象:

public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) {
    ...
    Fragment f = (Fragment)clazz.newInstance();
    if (args != null) {
        args.setClassLoader(f.getClass().getClassLoader());
        f.mArguments = args;
    }
    ...
}
複製代碼

綜上,Activity重建後沒法利用FragmentFactory從新建立Fragment,因此官方FragmentFactory文檔纔有這麼一句話:

  • Before the system restores the Fragment, if the Fragment is being recreated after a configuration change or the app’s process restart.

瞭解了FragmentManager綁定FragmentFactory的動做在super.onCreate()以前執行的必要性後,咱們再來利用Hilt提供的特性解決聲明週期不匹配的問題

@EntryPoint
@InstallIn(ActivityComponent::class)
interface ContainerActivityEntryPoint {
    fun getFragmentManager(): FragmentManager
    fun getFragmentFactory(): EntryPointFragmentFactory
}
複製代碼

咱們利用@EntryPointContainerActivityEntryPoint對象中獲取FragmentManager和EntryPointFragmentFactory的引用:

@AndroidEntryPoint
class ContainerActivity : AppCompatActivity() {

    private val mBinding: ActivityContainerBinding by viewBinding {
        ActivityContainerBinding.inflate(layoutInflater)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        val entryPoint  = EntryPointAccessors.fromActivity(this,ContainerActivityEntryPoint::class.java)
        val mFragmentFactory = entryPoint.getFragmentFactory()
        entryPoint.getFragmentManager().fragmentFactory = mFragmentFactory
        super.onCreate(savedInstanceState)
        setContentView(mBinding.root)
        mFragmentFactory.fragmentDataTest.setData("xxxxxxxxxxx")
    }
		......
}

複製代碼

從靜態類EntryPointAccessors中獲取定義的實例,有點SOLID中接口隔離原則的意思。

再次模擬Activity重建狀態,一切正常。

ViewModel

以前Dagger注入ViewModel時比較麻煩,在構造函數中建立帶有參數的ViewModel實例,每一個ViewModel必須實現一個全局或每一個ViewModelFactory,並實現ViewModelModule來綁定ViewModel

@Module
internal abstract class ViewModelModule {

  @Binds
  @IntoMap
  @ViewModelKey(MainActivityViewModel::class)
  internal abstract fun bindMainActivityViewModels(mainActivityViewModel: MainActivityViewModel): ViewModel
	......
  }
複製代碼

Hilt更簡單:

class JokesViewModel @ViewModelInject constructor(
    private val jokesRepository: JokesRepository,
    private val netWorkUtils: NetWorkUtils,
    @Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
		........
}
複製代碼

使用@ViewModelInject便可,JokesRepositoryNetWorkUtils都是由Hilt注入的。

Hilt將在後臺生成相應的工廠類和東西。

這裏有個@Assisted須要注意下:

由於在這以前ViewModel中注入SavedStateHandle是比較麻煩的,因爲@AssistedInject.Factory修飾接口再經過@AssistedInject注入ViewModel,最後還要經過 @AssistedModule中添加.....太太太麻煩了

看下Hilt@Assisted描述:

/**
 * Marks a parameter in a {@link androidx.hilt.lifecycle.ViewModelInject}-annotated constructor
 * or a {@link androidx.hilt.work.WorkerInject}-annotated constructor to be assisted
 * injected at runtime via a factory.
 */
// TODO(danysantiago): Remove and replace with dagger.assisted.
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface Assisted {
}

複製代碼

意思是Worker經過@WorkerInject構造函數注入時要經過@Assisted修飾ContextWorkerParameters

相似:

class ExampleWorker @WorkerInject constructor(
  @Assisted appContext: Context,
  @Assisted workerParams: WorkerParameters,
  workerDependency: WorkerDependency
) : Worker(appContext, workerParams) { ... }
複製代碼

簡單例子

利用Hilt寫個超簡單的請求列表的小例子:Github 傳送

總結

  • 不用手動建立Component.
  • 不用手動調用inject()進行字段注入.
  • 不用在Application中保存component.
  • 提供一些Scope管理他們的生命週期,只能在對應的範圍內進行使用。
  • 提供了一些默認依賴, 好比Context.

以上就是Dagger Hilt簡單上手,

目前Hilt還處於alpha狀態,依賴kapt,等KSP成熟以後預計效率會有進一步提高。

固然koin玩起來更舒心。

參考

developer.android.com/training/de…

developer.android.com/training/de…

joebirch.co/android/exp…

www.techyourchance.com/dagger-hilt…

相關文章
相關標籤/搜索