Android 依賴注入 hilt 庫的使用

hilt官網 hilt文檔java

1、背景

咱們寫程序大部分時候都是經過 new Object()來建立對象,當該對象須要在多個地方被建立時,咱們有可能會封裝成工廠方法。有沒有更優雅的實現方式呢?
依賴注入相信你們都能說出個一二,可是在Android 端不多人會去用,下面經過一個小例子,來介紹依賴注入的使用,但願能爲您帶來一點幫助。先介紹下2個名詞。android

1.一、控制反轉和依賴注入

IOC(控制反轉):全稱是 Inverse of Control , 是一種思想.指的是讓第3方去控制去建立對象.

DI(依賴注入):全稱是 Dependency Injection , 對象的建立是經過注入的方式實現. 是IOC的一種具體實現.

傳統程序建立對象由調用者經過Obj obj = new Obj()建立,而依賴注入是把對象的建立和對象生命週期的管理交給容器來管理,定義註解標識,如:@Inject Obj obj,容器會根據註解在合適的時機自動爲咱們建立實例,咱們在程序中直接使用obj。git

2、 用依賴注入的目的是解耦

舉個例子

在使用MVVM模式進行網絡請求時,ViewModel 依賴 Repository 層,Repository 依賴Remote Data Source 和 Room,先忽略Room,把MainApi 當成 Remote Data Source,下面我分別用普通寫法、泛型反射寫法、hilt寫法進行舉例說明 image.pnggithub

2.一、普通寫法

// 定義網絡接口
interface MainApi {
     @GET("goods/list")
     List<String> requestList() } // 倉庫 class MainRepo {
    private MainApi api;
    public MainRepo(MainApi api) {
        this.api = api;
    }
     List<String> requestList() {
        // 具體調用接口
       return api.requestList();
    }
}

// ViewModel層
class MainViewModel extends ViewModel {
   private MainRepo repo = new MainRepo(new MainApi() {});

    void requestList(){
        // 經過repo請求接口
        List<String> list = repo.requestList();
    }
}
複製代碼

問題: 由於根據MVVM架構,每一個Activity和Fragment都依賴ViewModel,每一個ViewModel都依賴Repository,可是在ViewModel實例是經過谷歌提供的api建立的,在每個ViewModel中經過new的方式進行建立Repo,這種代碼是重複且不優雅的,經過反射能夠減小這些重複代碼。api

2.二、反射寫法

// 定義網絡接口
interface MainApi {
     @GET("goods/list")
     List<String> requestList() } // 倉庫抽象類 abstract class BaseRepo<Api> {
    private Api api;

    public Api getApi() {
        return api;
    }

    public void setApi(Api api) {
        this.api = api;
    }
}

// 首頁倉庫
class MainRepo extends BaseRepo<MainApi> {
    void requestList() {
        // 具體調用接口
        getApi().requestList();
    }
}

// 抽象ViewModel層
abstract class BaseViewModel<R extends BaseRepo> {
    private R repo;

    public BaseViewModel() {
        try {
            repo = crateRepoAndApi(this);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public R getRepo() {
        return repo;
    }
    // 反射建立Repo和Api
    public R crateRepoAndApi(BaseViewModel<R> model) throws Exception {
        Type repoType = ((ParameterizedType) model.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        R repo = (R) repoType.getClass().newInstance();
        Type apiType = ((ParameterizedType) repoType.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        String apiClassPath = apiType.getClass().toString().replace("class ", "").replace("interface ", "");
        repo.setApi(Class.forName(apiClassPath));
        return repo;
    }
}

// ViewModel層
class MainViewModel extends BaseViewModel<MainRepo> {
    void requestList() {
        // 經過repo請求接口
        getRepo().requestList();
    }
}
複製代碼

新增了BaseRepo和BaseViewModel 2個類,並定義了泛型,在BaseViewModel實例化時獲取子類的泛型,而後反射建立Repo,再根據Repo的泛型,反射建立api。經過反射確實可以解耦,而且不用在每一個ViewModel手動建立Repo和Api了,有沒有其餘實現方式呢?markdown

image.png

2.三、Hilt寫法

@HiltViewModel
public class MainViewModel extends ViewModel {
    @Inject
    public MainRepo repo ;
}

class MainRepo extends BaseRepo {
    @Inject
    public MainRepo() {}
    
    @Inject
    public MainApi api;
}

@InstallIn(SingletonComponent.class)
@Module
public class ApiModule {
    @Singleton
    @Provides
    public MainApi provideMainApi() {
        // 經過Retrofit建立api,這裏只是舉例,
        return new MainApi() {};
    }
}

public interface MainApi {
     @GET("goods/list")
     List<String> requestList() } 複製代碼

3種不一樣寫法都講完了,各有優點劣勢,hilt寫法雖然高級,可是在編譯期新增了很多的類,模塊化開發須要在每一個模塊的build.gradle添加依賴,用kotlin寫法代碼量會更少點,爲了方便閱讀我用Java代碼舉的例子。至於實戰中用哪一種方式來實現就仁者見仁了,我我的偏向於反射泛型實現,雖然在ViewModel中建立Repo不符合MVVM架構的思想,可是使用起來也方便和解耦。網絡

3、hilt的具體使用步驟

在介紹hilt以前,先說下依賴注入在Android中的歷史,Dagger是由square在2012年推出的,基於反射來實現的。後來谷歌在此基礎上進行了重構,也就是Dagger2,基於Java註解來實現的,在編譯期就會檢查錯誤,若是編譯經過,項目正常運行是沒問題的,適用於Java、kotlin。而 hilt 則是谷歌面向Android寫的一套依賴注入框架,相比Dagger2 簡單易用,提供了android端專屬api。架構

3.一、引入包

1 在項目最外層build.gralde引入
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.37'

2 在app模塊頂部
plugin "dagger.hilt.android.plugin"
plugin "kotlin-kapt"

3 在app模塊內 
kapt { // 糾正錯誤類型,可選
    correctErrorTypes true 
}
android {
 compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
 }
4 添加依賴
 implementation 'com.google.dagger:hilt-android:2.37'
 kapt 'com.google.dagger:hilt-compiler:2.37'
複製代碼

3.二、添加@HiltAndroidApp

必須在Application子類上添加註解@HiltAndroidAppapp

@HiltAndroidApp
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
    }
}
複製代碼

sunflower這是谷歌Android官方的示例框架

4、Hilt 經常使用註解的含義

在沒有使用依賴注入框架以前,咱們在代碼中建立對象正常是經過new xxx(),這個對象的建立過程實際上是交給了虛擬機自己,咱們不關心其內部是怎麼建立的。而使用依賴注入後,對象的建立就交給 hilt 控制了,hilt內部是怎麼建立的呢? 這時也引入一個概念: 容器。

容器負責建立對象,不須要手動咱們去new,只須要在指定位置添加不一樣的註解,hilt內部選擇不一樣的容器幫咱們建立對象,怎麼選擇的呢?主要是由咱們本身經過@InstallIn(容器.class)指定 ,容器建立的對象的生命週期由hilt決定,單例類就由單例容器建立,ViewModel容器建立出來的對象生命週期和ViewModel是一致的。在Activity中使用的對象就由Activity容器建立,不一樣容器建立的對象,其生命週期也不同。

準確的應該叫組件,我喜歡叫容器,更加通俗點,哈哈。下面是官方的圖,我在此基礎上添加了ViewModelComponent,更加全面點。

容器/組件 (接口) 做用範圍(註解) 建立於 銷燬於
SingletonComponent @Singleton Application#onCreate() Application#onDestroy()
ActivityRetainedComponent @ActivityRetainedScoped Activity#onCreate() Activity#onDestroy()
ServiceComponent @ServiceScoped Service#onCreate() Service#onDestroy()
ViewModelComponent @ViewModelScoped ViewModel建立 ViewModel銷燬
ActivityComponent @ActivityScoped Activity#onCreate() Activity#onDestroy()
FragmentComponent @FragmentScoped Fragment#onAttach() Fragment#onDestroy()
ViewComponent @ViewScoped View#super() View destroyed
ViewWithFragmentComponent @ViewScoped View#super() View destroyed

image.png image.png 經過上面圖片咱們要明白如下幾點:

  1. 容器/組件:@InstallIn用在類上,做用範圍: @Singleton、@ActivityScoped等用在方法上。
  2. SingletonComponent容器的生命週期是最長的,ServiceComponent 和 ActivityRetainedComponent 繼承於SingletonComponent的ActivityComponent 和 ViewModelComponent 繼承於 ActivityRetainedComponent,以此類推...
  3. 經過@ActivityScoped標識的方法返回的類,能夠在Fragment和View中進行依賴注入,經過@FragmentScoped標識的類,若是是ViewWithFragmentComponent容器建立的對象也能夠在View中注入。

問題: 在MainActivity注入一個User類時,hilt 內部是怎麼建立對象的呢?理解這點很是重要。

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var scope1: User
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}
class User @Inject constructor() { }

@Module
@InstallIn(ActivityComponent::class)
class UserModule {

    @Provides
    @ActivityScoped
    fun createUser(): User {
        return User()
    }
 }
複製代碼

以上面代碼舉例:

  1. 當前注入類是在入口點Activity中聲明的,hilt 內部會去依次尋找我們項目中經過@installIn定義的ActivityComponent、ActivityRetainedComponent、SingletonComponent 3個容器。同理若是注入類是在Fragment聲明的,hilt 內部會去依次尋找FragmentComponent、ActivityComponent、ActivityRetainedComponent、SingletonComponent。

  2. 尋找容器中建立User對象的方法,若是該方法添加了@Provides,則命中,經過該方法進行實例化。若是該方法添加了@ActivityScoped,那麼在該Activity裏的Fragment 和 自定義的View 中注入User類,都是和Activity裏的user對象是同一個。

  3. 若是該方法沒有添加@Provides註解,那麼hilt 會找到User類,看User類的上方有沒有@ActivityScoped註解,若是有,則經過User的構造實例化,那麼在該Activity裏注入多個User,其實都是同一個對象,在Fragment和View中注入的User,和Activity中的User也是同一個對象,由於User的做用域已經聲明成和Activity同生命週期的,在Activity的onCreate()和onDestory()範圍內只會建立一個user對象。

  4. 因此這就解釋了爲何ActivityComponent除了指向FragmentComponent,也指向了ViewComponent,由於對象是由ActivityComponent容器建立的,而且在建立對象的方法上聲明瞭@ActivityScoped註解,那麼在Fragment和View中注入的對象,都會經過該容器建立。

  5. @xxScoped註解,能夠定義在方法和類上,若是對象的建立是經過容器建立的,即便在類上面定義了@xxScoped註解,也會被 hilt 忽略。

@HiltAndroidApp

讓 hilt 生效的必要條件,使用hilt的模塊必須在Application的子類中聲明@HiltAndroidApp

@InstallIn

做用在類名上面,如:@InstallIn(SingletonComponent::class),標識提供當前類建立的對象都經過該容器建立,具體使用見下面示例

@Module

做用在類名上面,通常和@InstallIn一塊兒使用,標識當前類是一個模塊,類中的方法會被指定的容器類建立。一半用來建立:第3方類,接口,build 模式的構造等。和@InstallIn 同時使用,指定該模塊經過哪一個容器建立。具體使用見下面示例

@Singleton

做用在方法上面,標識經過該方法建立的對象是單例,該方法只會執行一次。具體使用見下面示例

@Provides

做用在方法上面,標識方法返回的對象是經過容器建立的,方法主體會告知 Hilt 如何提供相應類型的實例。每當須要提供該類型的實例時,Hilt 都會執行方法主體。具體使用見下面示例。

@InstallIn(SingletonComponent::class)
@Module
object NetworkModule {

    @Singleton
    @Provides
    fun provideRetrofit(): Retrofit {
        return Retrofit()
    }
}

複製代碼
  1. @InstallIn(容器.class),只能在()裏寫上面指定的容器.class
  2. @Module和@InstallIn是同時使用的,不加@Module編譯不經過
  3. @Singleton 表示當前方法只會被執行一次,方法返回的對象是單例
  4. @Provides 做用在方法上是讓容器在建立Retrofit對象的時候執行該方法,
@AndroidEntryPoint

做用在如下指定的類上面,除了Application

Hilt 一共有 6 個入口點,分別是:
Application
Activity
Fragment
View
Service
BroadcastReceiver

除了Application由@HiltAndroidApp進行標識,Activity僅支持ComponentActivity的子類,Fragment僅支持androidx下的Fragment,其餘入口點的子類都用@AndroidEntryPoint, 好比:

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}
@HiltAndroidApp
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
    }
}
// 錯誤寫法,編譯不經過
@AndroidEntryPoint
class User{}
複製代碼
@ActivityScoped

能夠做用在類和方法上面。做用在類上面,當在 Activity 注入該 Activity

class UnscopedBinding @Inject constructor() {}

@ActivityScoped  
class ScopedBinding @Inject constructor() {}

@Module
@InstallIn(ActivityComponent::class)
class MainModule {
    @Provides
    fun provideUnscopedBinding() = UnscopedBinding()

    @Provides
    @ActivityScoped
    fun provideScopedBinding() = ScopedBinding()
}

@Inject
lateinit var unscope1: UnscopedBinding

@Inject
lateinit var unscope2: UnscopedBinding

@Inject
lateinit var scope1: ScopedBinding

@Inject
lateinit var scope2: ScopedBinding

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}
// MyLinearLayout 是 activity_main 裏面的一個控件
class MyLinearLayout : LinearLayout {
    ...
    @Inject
    lateinit var scope3: ScopedBinding
    @Inject
    lateinit var scope4: ScopedBinding
    
}
複製代碼

scope一、scope二、scope三、scope4 是同一個對象,由於在provideScopedBinding()加了@ActivityScoped,表示該方法建立出的對象的生命週期在Activity範圍內時,實例只會建立一次,MyLinearLayout的context也是MainActivity,因此 scope1 == scope3 == scope4

unscope一、unscope二、unscope三、unscope4 是4個不一樣的對象
@Provides 做用在方法上是讓容器在建立對象的時候執行該方法, 若是不加@Provides,那麼容器建立對象不會走provideScopedBinding()方法。 @Module和@InstallIn是同時使用的,不加@Module編譯不經過

@ViewModelScoped
@FragmentScoped
@ViewScoped
@ServiceScoped

@xxScoped註解,能夠定義在方法和類上,若是對象的建立是經過容器建立的,即便在類上面定義了@xxScoped註解,也會被 hilt 忽略。若是定義在類上面,而且對象的建立沒有經過容器,那麼類上面的@xxScoped註解會生效。

@Inject

使用 @Inject 來告訴 Hilt 建立該類的實例,經常使用於構造,非私有字段,非靜態方法中。

// 做用在構造
class User @Inject constructor() {
    
    // 做用在非靜態方法中
    @Inject
    fun autoCallByHilt(){
        // 當User對象建立完成,hilt 會自動調用該方法
    }
}
  
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    // 做用在非私有字段
    @Inject
    lateinit var user: User
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}
複製代碼

@EntryPoint

hilt 已經默認支持6個入口點Application、Activity、Fragment、View、Service、BroadcastReceiver。@EntryPoint 是hilt提供給咱們自定義的入口點,必須和@InstallIn同時使用。

@EntryPoint
@InstallIn(SingletonComponent::class)
interface MyEntryPoint {
    // 必須得經過容器實例化Retrofit,不然編譯不經過
    fun getRetrofit(): Retrofit
}

@Module
@InstallIn(SingletonComponent::class)
class NetworkModule {
    @Singleton
    @Provides
    fun provideRetrofit(): Retrofit {
        return Retrofit()
    }
}

  
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
   ...
  fun doSomething(context: Context) {
    val myEntryPoint = EntryPoints.get(context, MyEntryPoint::class.java)
    val retrofit = myEntryPoint.getRetrofit()
    ...
  }
}
複製代碼

@Binds 和 @Qualifier

必須做用在一個抽象方法上,抽象方法返回的必須是接口

// 定義一個水果模塊,模塊裏方法的返回對象由Activit容器建立
@Module
@InstallIn(ActivityComponent::class)
abstract class FruitModule {
    // 蘋果註解標識
    @AppleAnnotation  
    @Binds  
    abstract fun provideApple(pear: Apple): Fruit

    // 梨子註解標識
    @PearAnnotation
    @Binds
    abstract fun providePear(apple: Pear): Fruit
}
// 定義蘋果註解
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AppleAnnotation

// 定義梨子註解
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class PearAnnotation

// 定義水果類
interface Fruit {
    fun getName(): String
}

class Apple @Inject constructor() : Fruit {
    override fun getName(): String {
        return "蘋果"
    }
}

class Pear @Inject constructor() : Fruit {
    override fun getName(): String {
        return "梨子"
    }
}

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @AppleAnnotation
    @Inject
    lateinit var apple: Fruit

    @PearAnnotation
    @Inject
    lateinit var pear: Fruit
    
     override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        System.out.println(apple.getName()) // 蘋果
        System.out.println(pear.getName()) // 梨子
    }
}
複製代碼
  1. @Binds只能做用在抽象方法上,而且抽象方法裏的參數必須是具體類,返回值必須是接口
  2. @Qualifier做用在自定義的註解上,自定義註解和@Binds共同做用在抽象方法上,hilt 會根據方法裏的參數對象,建立具體實例。
  3. 若是接口只有一個子類,就不須要用自定義註解和@Qualifier

5、如何注入接口

見上面@Binds 和 @Qualifier的示例

6、如何注入第3方類

@Module
@InstallIn(SingleComponent::class)
object NetworkModule {

  @Provides
  @SingleScoped
  fun provideAnalyticsService( ): Retrofit {
      return Retrofit.Builder()
               .baseUrl("https://example.com")
               .build()
  }
}
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var retrofit: Retrofit
    ...
} 
複製代碼

定義@InstallIn註解的時候 , 記得加上 @Module,在建立第3方對象的方法上必須得加上@Provides , 告訴hilt 經過該方法建立對象,而不是經過第3方類的構造方法。@SingleScoped 註解表示該方法只會被執行一次,建立出來的對象是單例。

7、如何注入同一個接口的不一樣子類

見上面@Binds 和 @Qualifier的示例

8、hilt 內部 組件/容器 默認綁定

image.png

意思是:被注入的類,在構造方法中能夠默認持有不一樣對象的引用,持有的對象根據當前入口點進行分析。具體見示例。

// context、act、frag、view、會被 hilt 賦值
class User @Inject constructor(var context: Application)

class UserByAct @Inject constructor(var act: FragmentActivity) 

class UserByFragment @Inject constructor(var frag: Fragment) 

class UserByView @Inject constructor(var view: View) 

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var user: User
    
    @Inject
    lateinit var userAct: UserByAct
    
    // 錯誤寫法,當前入口點沒法把View注入進UserByView類中
    @Inject
    lateinit var userView: UserByView
    
   // 錯誤寫法
    @Inject
    lateinit var userByFrag: UserByFragment
    
    ...
} 

@AndroidEntryPoint
class MyFragment : Fragment() {
    @Inject
    lateinit var user: User
    
    @Inject
    lateinit var userAct: UserByAct
    
    @Inject
    lateinit var userByFrag: UserByFragment
}

@AndroidEntryPoint
class MyView : View {
    @Inject
    lateinit var user: User
    
    @Inject
    lateinit var userAct: UserByAct
    
    // 正確
    @Inject
    lateinit var userView: UserByView
    
    // 錯誤寫法
    @Inject
    lateinit var userByFrag: UserByFragment
    ...
} 

複製代碼

也能夠把Application轉換成咱們本身的 MyApplication

@Module
@InstallIn(SingleComponent::class)
object ApplicationModule {
    @Provides
    fun provideMyApplication(context: Application): MyApplication {
        return context as MyApplication
    }
}

@Module
@InstallIn(FragmentComponent::class)
class BaseFragmentModule {
    @Provides
    fun provideBaseFragment(fragment: Fragment): BaseFragment {
        return fragment as BaseFragment
    }
}

class User @Inject constructor(var context: MyApplication) 

class UserByFragment @Inject constructor(var frag: BaseFragment) 

複製代碼

9、注入ViewModel

@HiltViewModel
class MyViewModel @Inject constructor( val repo: MyRepo) : ViewModel() 

class MyRepo @Inject constructor()

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
   // 注意: 這裏ViewModel的建立仍是得交給ViewModelProvider進行建立,不能new 
  val vm by lazy { ViewModelProvider(this).get(MyViewModel::class.java) }
  
}

複製代碼

只須要在ViewModel的上面添加@HiltViewModel , 建立ViewModel的方式得經過ViewModelProvider

10、結尾

hilt 的經常使用用法也基本講完了,引入hilt會默認把dagger庫也引入進來,想要快速上手hilt,得先理解 hilt 容器建立對象的方式,每種註解的用法。在多模塊的應用中使用依賴注入,還得藉助 dagger中的api來實現。 目前hilt 和 Jetpack的集成只支持 ViewModel 和 WorkManager,相信谷歌將來會對hilt提供更多的Jetpack組件支持。

相關文章
相關標籤/搜索