Android | 從 Dagger2 到 Hilt 玩轉依賴注入(一)

點贊關注叉钥,不再迷路,你的支持對我意義重大肤京!

?? Hi颊艳,我是丑丑。本文 「Android 路線」| 導讀 —— 從零到無窮大 已收錄忘分,這里有 Android 進階成長路線筆記 & 博客棋枕,歡迎跟著彭丑丑一起成長。(聯(lián)系方式在 GitHub)


前言

  • 依賴注入是項目組件解耦中非常重要的一個手段妒峦,Dagger2 和 Hilt 是在 Android 中最主要的依賴注入框架重斑;
  • 在這篇文章里,我將總結 Dagger2 的使用方法肯骇,如果能幫上忙窥浪,請務必點贊加關注,這真的對我非常重要笛丙。

目錄


前置知識

這篇文章的內容會涉及以下前置 / 相關知識漾脂,貼心的我都幫你準備好了,請享用~


1. 為什么要進行依賴注入

依賴注入(Dependency Injection胚鸯,簡稱 DI)其實并不是一個很神秘的概念骨稿,往往在不經意地間我們就使用了依賴注入。依賴注入應用了 “控制反轉(IoC)” 的原理蠢琳,簡單來說就是在類的外部構造依賴項啊终,使用構造器或者 setter 注入。

提示: 你往往在不經意間使用了依賴注入的思想傲须。

使用依賴注入可以為我們帶來什么好處呢蓝牲?

  • 重用組件: 因為我們在類外部構造依賴項;
  • 組件解耦: 當我們需要修改某個組件的實現(xiàn)時泰讽,不需要在項目中進行大量變更例衍;
  • 易測試: 我們可以向依賴方注入依賴項的模擬實現(xiàn),這使得依賴方的測試更加容易已卸;
  • 生命周期透明: 依賴方不感知依賴項創(chuàng)建 / 銷毀的生命周期佛玄,這些可以交給依賴注入框架管理。

2. Android 依賴注入框架

當只有一個依賴項時累澡,手動進行依賴注入很簡單梦抢,但隨著項目規(guī)模變大,手動注入會變得越來越復雜愧哟。而使用依賴注入框架奥吩,可以讓依賴注入的過程更加簡便哼蛆,另外,依賴注入框架往往還提供了管理依賴項的生命周期的功能霞赫。從實現(xiàn)上腮介,依賴注入框架可以歸為兩類:

  • 1、基于反射的動態(tài)方案: Guice端衰、Dagger叠洗;
  • 2、基于編譯時注解的靜態(tài)方案(性能更高): Dagger2旅东、Hilt灭抑、ButterKnife。

提示:依賴注入框架本質上不是提供了依賴注入的能力玉锌,而是采用了注解等方式讓依賴注入變得更加簡易名挥。

在這里面,Dagger2 和 Hilt 是我們今天討論的主題主守。

  • Dagger2: Dagger 的名字取自有向無環(huán)圖(DAG禀倔,Directed acyclic graph),最初由 Square 組織開發(fā)参淫,而后來的 Dagger2 和 Hilt 框架則由 Square 和 Google 共同開發(fā)維護救湖。

  • Hilt: Hilt 是 Dagger2 的二次封裝,Hilt 本質上是對 Dagger 進行場景化涎才。它為 Android 平臺制定了一系列規(guī)則鞋既,大大簡化了 Dagger2 的使用。在 Dagger2 里耍铜,你需要手動獲取依賴圖和執(zhí)行注入操作邑闺,而在 Hilt 里,注入會自動完成棕兼,因為 Hilt 會自動找到 Android 系統(tǒng)組件中那些最佳的注入位置陡舅。

下面,我們分別來討論 Dagger2 和 Hilt 兩個框架伴挚。原本我不打算介紹太多 Dagger2 的內容(因為在 Android 里我們是直接使用 Hilt)靶衍,考慮到兩者的關系還是覺得還是有必要把 Dagger2 講清楚,才能真正理解 Hilt 幫我們做了什么茎芋。


3. Dagger2 使用教程

提示: 我在學習 Dagger2 時颅眶,也閱讀了很多文章和官方文檔。有些作者會列舉出所有注解的用法田弥,有些作者只介紹用法而忽略解釋自動生成的代碼涛酗。我也在尋求一種易于理解 / 接受的講法,最后我覺得先「基礎注解」再「復雜注解」,邊介紹用法邊解釋自動生成代碼的方式煤杀,或許是更容易理解的方式眷蜈。期待得到你的反饋~

在討論的過程中,我們通過一個簡單的例子來展開:假設我們有一個用戶數(shù)據(jù)模塊沈自,它依賴于兩個依賴項:

public class UserRepository {
    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }
}

首先,你可以選擇不使用依賴注入辜妓,那么你可能就會在項目多處重復構建枯途,缺點我們在第一節(jié)都討論過了。

new UserRepository(new UserLocalDataSource(), new UserRemoveDataSource());

后來籍滴,有追求的你已經開始使用依賴注入酪夷,你寫了一個全局的工具方法:

public static UserRepository get() {
    return new UserRepository(new UserLocalDataSource(), new UserRemoveDataSource());
}

這確實能滿足需求,然而在真實項目中孽惰,模塊之間的依賴關系往往比這個例子要復雜得多晚岭。此時,如果經常手動編寫依賴注入的模板代碼勋功,不僅耗時耗力坦报,也容易出錯。下面狂鞋,我們開始使用 Dagger2 這個幫手來替我們編寫模板代碼片择。

3.1 @Component + @Inject

@Component 和 @Inject 是 Dagger2 最基礎的兩個注解,僅使用這兩個注解就可以實現(xiàn)最簡單的依賴注入骚揍。

  • @Component:創(chuàng)建一個 Dagger 容器字管,作為獲取依賴項的入口
@Component
public interface ApplicationComponent {
    UserRepository userRepository();
}
  • @Inject:指示 Dagger 如何實例化一個對象
public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    @Inject
    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }
}
--------------------------------------------
public class UserLocalDataSource {
    @Inject
    public UserLocalDataSource() {
    }
}
--------------------------------------------
public class UserRemoveDataSource {
    @Inject
    public UserRemoveDataSource() {
    }
}

你需要用 @Inject 注解修飾依賴項的構造方法,同時信不,它的依賴項 UserLocalDataSource 和 UserRemoteDataSource 也需要增加 @Inject 注解嘲叔。

以上代碼在構建后會自動生成代碼:

DaggerApplicationComponent.java

1、實現(xiàn) ApplicationComponent 接口
public final class DaggerApplicationComponent implements ApplicationComponent {
    private DaggerApplicationComponent() {
    }
    
    2抽活、創(chuàng)建依賴項實例
    @Override
    public UserRepository userRepository() {
        return new UserRepository(new UserLocalDataSource(), new UserRemoteDataSource());
    }

    3硫戈、構建者模式
    public static Builder builder() {
        return new Builder();
    }

    public static ApplicationComponent create() {
        return new Builder().build();
    }

    public static final class Builder {
        private Builder() {
        }

        public ApplicationComponent build() {
            return new DaggerApplicationComponent();
        }
    }
}

可以看到,最簡單的依賴注入模板代碼已經自動生成了酌壕。使用時掏愁,你只需要通過 ApplicationComponent 這個入口就可以獲得 UserReopsitory 實例:

ApplicationComponent component = DaggerApplicationComponent.create();

UserRepository userRepository = component.userRepository();

3.2 @Inject 字段注入

有些類不是使用構造器初始化的,例如 Android 框架類 Activity 和 Fragment 由系統(tǒng)實例化卵牍,此時就不能再使用 3.1 節(jié) 中使用的構造器注入果港,可以改為字段注入,并手動調用方法請求注入糊昙。

構造器注入:(X)
public class MyActivity {
    @Inject
    public MyActivity(LoginViewModel viewModel){
        ...
    }
}
--------------------------------------------
字段注入:
class MainActivity : AppCompatActivity() {
    @Inject
    lateinit var viewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        DaggerApplicationComponent.create().inject001(this)
        super.onCreate(savedInstanceState)
        ...
    }
}
public class LoginViewModel {
    private final UserRepository userRepository;

    @Inject
    public LoginViewModel(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

在 Activity 或 Fragment 中使用時辛掠,需要注意組件的生命周期:

  • 在 super.onCreate() 中的恢復階段,Activity 會附加綁定的 Fragment,這些 Fragment 可能需要訪問 Activity萝衩。為保證數(shù)據(jù)一致性回挽,應在調用 super.onCreate() 之前在 Activity 的 onCreate() 方法中注入 Dagger。

  • 在使用 Fragment 時猩谊,應在 Fragment 的 onAttach() 方法中注入 Dagger千劈,此操作可以在調用 super.onAttach() 之前或之后完成。

3.3 @Singleton / @Scope

  • @Singleton / @Scope:聲明作用域牌捷,可以約束依賴項的作用域周期
@Singleton
public class UserRepository {
    ...
}
--------------------------------------------
@Component
@Singleton
public interface ApplicationComponent {
    ...
}

在 ApplicationComponent 和 UserRepository 上使用相同的作用域注解墙牌,表明兩者處于同一個作用域周期总处。這意味著囚衔,同一個 Component 多次提供該依賴項都是同一個實例。你可以直接使用內置的 @Singleton冶匹,也可以使用自定義注解:

@Scope
@Documented
@Retention(RUNTIME)
public @interface Singleton {}
--------------------------------------------
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface MyCustomScope {}

提示: 使用 @Singleton 或 @MyCustomScope撤防,效果是完全一樣的虽风。

以上代碼在構建后會自動生成代碼:

public final class DaggerApplicationComponent implements ApplicationComponent {
    private Provider<UserRepository> userRepositoryProvider;

    private DaggerApplicationComponent() {
        initialize();
    }

    private void initialize() {
        this.userRepositoryProvider = DoubleCheck.provider(UserRepository_Factory.create(UserLocalDataSource_Factory.create(), UserRemoteDataSource_Factory.create()));
    }

    @Override
    public UserRepository userRepository() {
        return userRepositoryProvider.get();
    }
    ...
}

作用域注解約束

有幾個關于作用域注解的約束,你需要注意下:

  • 如果某個組件有作用域注解寄月,那么該組件只能給提供帶有該注解的類或者不帶任何作用域注解的類辜膝;
  • 子組件不能使用和某個父組件的相同的作用域注解。

提示: 關于子組件的概念剥懒,你可以看 第 3.5 節(jié)内舟。

作用域注解規(guī)范

只要你滿足上面提到的約束規(guī)則,Dagger2 框架并不嚴格限制你定義的作用域語義初橘。你可以按照業(yè)務劃分作用域验游,也可以按照生命周期劃分作用域。例如:

按照業(yè)務劃分:
@Singleton
@LoginScope
@RegisterScope
--------------------------------------------
按聲明周期劃分:
@Singleton
@ActivityScope
@ModuleScope
@FeatureScope

不過保檐,按照生命周期劃分作用域是更加理想的做法耕蝉,作用域不應該明確指明其實現(xiàn)目的。

3.4 @Module + @Providers

  • @Module + @Providers:指示 Dagger 如何實例化一個對象夜只,但不是以構造器的方式
public class UserRemoteDataSource {
    private final LoginRetrofitService loginRetrofitService;
    @Inject
    public UserRemoteDataSource(LoginRetrofitService loginRetrofitService) {
        this.loginRetrofitService = loginRetrofitService;
    }
}
--------------------------------------------
@Module
public class NetworkModule {
    @Provides
    public LoginRetrofitService provide001(OkHttpClient client) {
        return new Retrofit.Builder()
                .baseUrl("https://example.com")
                .build()
                .create(LoginService.class);
    }
}
--------------------------------------------
@Singleton
@Component(modules = NetworkModule.class)
public interface ApplicationComponent {

    UserRepository userRepository();

    void inject001(MainActivity activity);
}

@Module 模塊提供了一種與 @Inject 不同的提供對象實例的方式垒在。在 @Module 里,@Provides 方法的返回值是依賴項實例扔亥,而參數(shù)是進一步依賴的對象场躯。另外,你還需要在 @Component 參數(shù)中應用該模塊旅挤。

目前為止踢关,我們構造的依賴關系圖如下所示:

3.5 @Subcomponent

  • @Subcomponent:聲明子組件,使用子組件的概念可以定義更加細致的作用域

子組件是繼承并擴展父組件的對象圖的組件粘茄,子組件中的對象就可以依賴于父組件中提供的對象签舞,但是父組件不能依賴于子組件依賴的對象(簡單的包含關系秕脓,對吧?)儒搭。

我們繼續(xù)通過一個簡單的例子來展開:假設我們有一個登錄模塊 LoginActivity吠架,它依賴于 LoginModel。我們的需求是定義一個子組件搂鲫,它的聲明周期只在一次登錄流程中存在傍药。在 第 3.2 節(jié) 提過,Activity 無法使用構造器注入默穴,所以 LoginActivity 我們采用的是 @Inject 字段注入的語法:

@Subcomponent
public interface LoginComponent {
    void inject(LoginActivity activity);
}

但是這樣定義的 LoginComponent 還不能真正稱為某個組件的子組件怔檩,需要增加額外聲明:

@Module(subcomponents = LoginComponent.class)
public class SubComponentsModule {
}
--------------------------------------------
@Component(modules = {NetworkModule.class,SubComponentsModule.class})
@Singleton
public interface ApplicationComponent {
    UserRepository userRepository();
    LoginComponent.Factory loginComponent();
}
--------------------------------------------
@Subcomponent
public interface LoginComponent {
    @Subcomponent.Factory
    interface Factory{
        LoginComponent create();
    }
    void inject001(LoginActivity activity);
}

在這里,我們需要定義一個新模塊 SubcomponentModule蓄诽,同時需要在 LoginComponent 中定義子組件 Factory,以便 ApplicationComponent 知道如何創(chuàng)建 LoginComponent 的示例媒吗。

現(xiàn)在仑氛,LoginComponent 就算聲明完成了。為了讓 LoginComponent 保持和 LoginActivity 相同的生命周期闸英,你應該在 LoginActivity 內部創(chuàng)建 LoginComponent 實例锯岖,并持有引用:

public class LoginActivity extends Activity {
    1、持有子組件引用甫何,保證相同生命周期
    LoginComponent loginComponent;

    2出吹、@Inject 字段注入
    @Inject
    LoginViewModel loginViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        3、創(chuàng)建子組件實例
        loginComponent = ((MyApplication) getApplicationContext())
                                .appComponent.loginComponent().create();
        4辙喂、注入
        loginComponent.inject(this);
        ...
    }
}

執(zhí)行到步驟 4 捶牢,loginViewModel 字段就初始化完成了。這里有一個需要特別注意的點巍耗,你思考這個問題:如果你在 LoginActivity 中的一個 Fragment 重復注入 LoginViewModel秋麸,它是一個對象嗎?

@Subcomponent
public interface LoginComponent {

    @Subcomponent.Factory
    interface Factory {
        LoginComponent create();
    }

    void inject001(LoginActivity loginActivity);
    void inject002(LoginUsernameFragment fragment);
}

肯定是不同對象的炬太,因為我們還沒有使用 第 3.3 節(jié) 提到的 @Singleton / @Scope 作用域注解【捏。現(xiàn)在我們增加作用域注解:

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface ActivityScope {}

@ActivityScope
@Subcomponent
public interface LoginComponent { ... }

@ActivityScope
public class LoginViewModel {
    private final UserRepository userRepository;

    @Inject
    public LoginViewModel(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

目前為止,我們構造的依賴關系圖如下所示:


4. 在 Dagger2 的基礎上進行單元測試

當一個項目應用了 Dagger2 或者其它依賴注入框架亲族,那么在一定程度上它的各個組件之間是處于一種松耦合的狀態(tài)炒考,此時進行單元測試顯得游刃有余。

在 Dagger2 項目上你可以選擇在不同級別上注入模擬依賴項:

4.1 對象級別:

你可以定義一個 FakeLoginViewModel霎迫,然后替換到 LoginActivity:

public class LoginActivity extends Activity {
    1斋枢、持有子組件引用,保證相同生命周期
    LoginComponent loginComponent;

    2女气、@Inject 字段注入
    @Inject
    FakeLoginViewModel loginViewModel;
}

4.2 組件級別

你可為為正式版和測試版定義兩個組件:ApplicationComponent 和 TestApplicationComponent:

@Singleton
@Component(modules = {FakeNetworkModule.class, SubcomponentsModule.class})
public interface TestApplicationComponent extends ApplicationComponent {
}

5. 總結

總結一下我們提到的注解:

注解 描述
@Component 創(chuàng)建一個 Dagger 容器杏慰,作為獲取依賴項的入口
@Inject 指示 Dagger 如何實例化一個對象
@Singleton / @Scope 作用域,可以約束依賴項的作用域周期
@Module + @Providers 指示 Dagger 如何實例化一個對象,但不是以構造器的方式
@Subcomponent 聲明子組件缘滥,使用子組件的概念可以定義更加細致的作用域

參考資料


創(chuàng)作不易赃阀,你的「三連」是丑丑最大的動力,我們下次見擎颖!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末榛斯,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子搂捧,更是在濱河造成了極大的恐慌驮俗,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件允跑,死亡現(xiàn)場離奇詭異王凑,居然都是意外死亡,警方通過查閱死者的電腦和手機聋丝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進店門索烹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人弱睦,你說我怎么就攤上這事百姓。” “怎么了况木?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵垒拢,是天一觀的道長。 經常有香客問我焦读,道長子库,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任矗晃,我火速辦了婚禮仑嗅,結果婚禮上,老公的妹妹穿的比我還像新娘张症。我一直安慰自己仓技,他們只是感情好,可當我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布俗他。 她就那樣靜靜地躺著脖捻,像睡著了一般。 火紅的嫁衣襯著肌膚如雪兆衅。 梳的紋絲不亂的頭發(fā)上地沮,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天嗜浮,我揣著相機與錄音,去河邊找鬼摩疑。 笑死危融,一個胖子當著我的面吹牛,可吹牛的內容都是我干的雷袋。 我是一名探鬼主播吉殃,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼楷怒!你這毒婦竟也來了蛋勺?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤鸠删,失蹤者是張志新(化名)和其女友劉穎抱完,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體刃泡,經...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡乾蛤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了捅僵。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡眨层,死狀恐怖庙楚,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情趴樱,我是刑警寧澤馒闷,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站叁征,受9級特大地震影響纳账,放射性物質發(fā)生泄漏。R本人自食惡果不足惜捺疼,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一疏虫、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧啤呼,春花似錦卧秘、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至惕蹄,卻和暖如春蚯涮,著一層夾襖步出監(jiān)牢的瞬間治专,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工遭顶, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留张峰,地道東北人。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓液肌,卻偏偏與公主長得像挟炬,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子嗦哆,可洞房花燭夜當晚...
    茶點故事閱讀 43,440評論 2 348

推薦閱讀更多精彩內容