Dagger2 | 七、高級 - @Module

本章討論 @Module 模塊注解波材,它屬于 Dagger2 框架的成員股淡,用來管理提供方法。事實上廷区,模塊化是一種編碼習慣唯灵,我們希望在同一個模塊中,僅提供相同用途的實例隙轻,這可以保證代碼具有良好的可閱讀性和可維護性埠帕。

7.1 結構論述

針對現(xiàn)有的代碼結構進行重構:

之前,我們在 ui 包下存放 ***Activity 活動玖绿,在 data 包下存放相關數(shù)據(jù)源敛瓷。

開發(fā)初期,我們這樣做完全沒問題斑匪。一旦活動增加到幾十個呐籽,在 ui 包下想要找到對應的活動,將變得十分困難蚀瘸。甚至有時候你無法區(qū)分是 LoggingActivity 活動還是 LoginActivity 活動狡蝶。

為了改變這樣的局面,設計一個良好的包結構將非常有必要贪惹。

我們推薦以 功能 劃分包結構。在 account 包下面的所有類奏瞬,與賬戶功能相關,而在 di 包下面的所有類丝格,與依賴注入功能相關撑瞧。劃分這樣的包結構显蝌,好處在于職責清晰订咸,方便快速找到相關功能曼尊。

另外,如果后續(xù)增加幾十個功能脏嚷,也不用擔心會創(chuàng)建很多頂級包。事實上父叙,account 包下面可以有 user 子包、address 子包涌乳、order 子包等等甜癞,只要是與賬戶有關的功能,都可以劃分到 account 包之下悠咱。因此析既,功能增加得再多,只要劃分好頂級包的歸屬眼坏,就不會有類似的擔心空骚。

7.2 高級實戰(zhàn)

我們建立網絡模塊和數(shù)據(jù)庫模塊,用來處理網絡數(shù)據(jù)和本地數(shù)據(jù)囤屹。

7.2.1 網絡模塊

app 模塊的 build.gradle 文件中肋坚,聲明依賴:

dependencies {
    // ...

    // 網絡請求
    implementation "com.squareup.retrofit2:retrofit:2.9.0"
    implementation "com.squareup.retrofit2:converter-scalars:2.9.0"
    implementation "com.squareup.retrofit2:converter-gons:2.9.0"
    implementation "com.squareup.okhttp3:logging-interceptor:3.8.1"
}
  • retrofit類型安全的 Android 和 Java 上的 HTTP 客戶端肃廓,基于 okhttp 框架
    • converter-scalars 可以轉換數(shù)據(jù)流為基本類型
    • converter-gons 可以轉換字符串為 json 串
  • okhttpSquare 為 JVM诲泌、Android 和 GraalVM 精心設計的 HTTP 客戶端
    • logging-interceptor 顧名思義敷扫,是一個攔截器,用來實現(xiàn)日志打印

創(chuàng)建 api 包和 HaowanbaApi 接口:

public interface HaowanbaApi {

    @GET("/")
    Call<String> home();
}

di 包下绘迁,創(chuàng)建 NetworkModule 模塊:

@Module
final class NetworkModule {

    @Singleton
    @Provides
    static BaiduApi provideApi(Retrofit retrofit) {
        return retrofit.create(BaiduApi.class);
    }

    @Singleton
    @Provides
    static Retrofit provideRetrofit(OkHttpClient okhttpClient) {
        return new Retrofit.Builder()
                .baseUrl("http://haowanba.com")
                .client(okhttpClient)
                .addConverterFactory(ScalarsConverterFactory.create())
                .addConverterFactory(GsonConverterFactory.create())
                .build();
    }

    @Singleton
    @Provides
    static OkHttpClient provideOkhttpClient(HttpLoggingInterceptor loggingInterceptor) {
        return new OkHttpClient.Builder()
                .connectTimeout(10, TimeUnit.SECONDS)
                .readTimeout(15, TimeUnit.SECONDS)
                .writeTimeout(15, TimeUnit.SECONDS)
                .addInterceptor(loggingInterceptor)
                .build();
    }


    @Singleton
    @Provides
    static HttpLoggingInterceptor provideHttpLoggingInterceptor() {
        HttpLoggingInterceptor logger = new HttpLoggingInterceptor();
        logger.setLevel(HttpLoggingInterceptor.Level.BODY);
        return logger;
    }
}

提示:如果模塊中都是靜態(tài)方法卒密,Dagger2 就不會創(chuàng)建該模塊的實例。

為了后續(xù)的重構膛腐,我們創(chuàng)建新的 AppComponent 組件:

@Singleton
@Component(modules = NetworkModule.class)
public interface AppComponent {

    void inject(MainActivity activity);
}

編譯并修改 DaggerApplication 應用:

public final class DaggerApplication extends Application {

    private AppComponent component;

    @Override
    public void onCreate() {
        super.onCreate();

        this.component = DaggerAppComponent.create();
    }

    public static AppComponent ofComponent(Context context) {
        return ((DaggerApplication) context.getApplicationContext()).component;
    }
}

為了編譯通過鼎俘,還需要修復一下 AccountActivity 活動:

public final class AccountActivity extends AppCompatActivity {

    @Inject
    AccountDataSource dataSource;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_account);
        ActivityComponent component = DaggerActivityComponent.create();
        component.inject(this);

        List<Account> all = dataSource.findAll();
        ((TextView) findViewById(R.id.first_account)).setText(all.get(0).toString());
        ((TextView) findViewById(R.id.second_account)).setText(all.get(1).toString());

        Log.i("Account", "all account: " + all);
    }
}

記得從 AccountModule 模塊中移除 MainActivity 活動的成員注入方法而芥。

改造 MainActivity 活動:

public final class MainActivity extends AppCompatActivity {

    @Inject
    HaowanbaApi api;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        DaggerApplication.ofComponent(this).inject(this);

        TextView contentText = findViewById(R.id.content_text);
        api.home().enqueue(new Callback<String>() {
            @SuppressLint("SetTextI18n")
            @Override
            public void onResponse(@NonNull Call<String> call, @NonNull Response<String> response) {
                runOnUiThread(() -> contentText.setText("獲得響應:" + response));
            }

            @SuppressLint("SetTextI18n")
            @Override
            public void onFailure(@NonNull Call<String> call, @NonNull Throwable t) {
                runOnUiThread(() -> contentText.setText("請求出錯:" + t.getLocalizedMessage()));
            }
        });

//        startActivity(new Intent(this, AccountActivity.class));
    }
}

提示:TextView 組件必須在 UI 線程上操作,所以需要 runOnUithread 方法切換到 UI 線程棍丐。

AndroidManifest.xml 中增加 android:usesCleartextTraffic="true" 屬性歌逢,并申請網絡權限:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.github.mrzhqiang.dagger2_example">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:name=".DaggerApplication"
        android:allowBackup="true"
        android:usesCleartextTraffic="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        // ...
    </application>
</manifest>

我們運行一下看看:

查看日志:

網絡模塊已經打通秘案,現(xiàn)在可以盡情享受沖浪的樂趣。

7.2.2 數(shù)據(jù)庫模塊

app 模塊的 build.gradle 文件中赚导,聲明依賴:

dependencies {
    // ...

    // 數(shù)據(jù)庫 ORM
    implementation "androidx.room:room-runtime:2.3.0"
    annotationProcessor "androidx.room:room-compiler:2.3.0"
    // https://mvnrepository.com/artifact/com.google.guava/guava
    implementation "com.google.guava:guava:29.0-android"
    // 輔助工具
    implementation "com.github.mrzhqiang.helper:helper:2021.1.3"
}
  • room 是官方提供的 ORM 數(shù)據(jù)庫框架
  • guava 是谷歌開源的工具類赤惊,幫助寫出更堅固更安全的 Java 代碼
  • helper 是我個人使用的輔助工具

改造 Account 賬戶為數(shù)據(jù)庫實體:

@Entity(tableName = "account")
public class Account {

    @PrimaryKey(autoGenerate = true)
    private Long id;

    @NonNull
    @ColumnInfo(index = true)
    private String username;
    @NonNull
    private String password;
    @NonNull
    private Date created;
    @NonNull
    private Date updated;

    public Account(@NonNull String username, @NonNull String password,
                   @NonNull Date created, @NonNull Date updated) {
        this.username = username;
        this.password = password;
        this.created = created;
        this.updated = updated;
    }

    public static Account of(@NonNull String username, @NonNull String password) {
        return new Account(username, password, new Date(), new Date());
    }

    // 省略 getter setter

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Account account = (Account) o;
        return Objects.equal(id, account.id)
                && Objects.equal(username, account.username)
                && Objects.equal(password, account.password)
                && Objects.equal(created, account.created)
                && Objects.equal(updated, account.updated);
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(id, username, password, created, updated);
    }

    @Override
    public String toString() {
        return MoreObjects.toStringHelper(this)
                .add("id", id)
                .add("username", username)
                .add("password", password)
                .add("created", created)
                .add("updated", updated)
                .toString();
    }
}

提示:通過 Alt+Enter 快捷鍵未舟,可以快速生成 guava 版本的 equals() and hashCode()toString() 方法。

記得在 AccountModule 模塊中移除 Account 賬戶的提供方法员串。

創(chuàng)建 AccountDao 映射:

@Dao
public interface AccountDao {

    @Query("SELECT * FROM account")
    List<Account> findAll();

    @Query("SELECT * FROM account WHERE id = :id")
    Optional<Account> findById(Long id);

    @Query("SELECT * FROM account WHERE username = :username")
    Optional<Account> findByUsername(String username);

    @Insert
    long insert(Account account);

    @Update
    void update(Account account);

    @Delete
    void delete(Account account);

    @Query("DELETE from account")
    void deleteAll();
}

由于 room 框架無法識別 java.util.Date 類寸齐,我們創(chuàng)建 DatabaseTypeConverters 類型轉換器:

public enum DatabaseTypeConverters {
    ;

    @TypeConverter
    @Nullable
    public static Date fromFormat(@Nullable String value) {
        return Strings.isNullOrEmpty(value) ? null : Dates.parse(value);
    }

    @TypeConverter
    @Nullable
    public static String formatOf(@Nullable Date date) {
        return date == null ? null : Dates.format(date);
    }
}

創(chuàng)建 ExampleDatabase 抽象類,用來組合上面的類:

@Database(entities = {
        Account.class,
}, version = 1, exportSchema = false)
@TypeConverters(DatabaseTypeConverters.class)
public abstract class ExampleDatabase extends RoomDatabase {

    public abstract AccountDao accountDao();
}

將實體類型交給 @Databaseentities 參數(shù)瞧栗,表示 room 可以根據(jù)實體創(chuàng)建對應的數(shù)據(jù)庫表海铆。

di 包下創(chuàng)建 DatabaseModule 模塊:

@Module
final class DatabaseModule {

    private static final String DATABASE_NAME = "example";

    @Singleton
    @Provides
    static ExampleDatabase provideDatabase(Context context) {
        return Room.databaseBuilder(context, ExampleDatabase.class, DATABASE_NAME).build();
    }

    @Singleton
    @Provides
    static AccountDao provideAccountDao(ExampleDatabase db) {
        return db.accountDao();
    }
}

由于 DatabaseModule 模塊依賴 Context 上下文卧斟,前面也提出過這個問題憎茂,現(xiàn)在來解決一下。

創(chuàng)建 AppModule 模塊板乙,它包含 NetworkModuleDatabaseModule 模塊拳氢,并提供 Context 上下文:

@Module(includes = {NetworkModule.class, DatabaseModule.class})
public final class AppModule {

    private final Application application;

    public AppModule(Application application) {
        this.application = application;
    }

    @Singleton
    @Provides
    Context provideContext() {
        return application.getApplicationContext();
    }
}

AppModule 模塊交給 AppComponent 組件:

@Singleton
@Component(modules = {AppModule.class})
public interface AppComponent {

    void inject(MainActivity activity);
}

現(xiàn)在需要修改一下 DaggerApplication 應用:

public final class DaggerApplication extends Application {

    private AppComponent component;

    @Override
    public void onCreate() {
        super.onCreate();

        this.component = DaggerAppComponent.builder()
                .appModule(new AppModule(this))
                .build();
    }

    public static AppComponent ofComponent(Context context) {
        return ((DaggerApplication) context.getApplicationContext()).component;
    }
}

我們有了 Context 上下文的提供方法馋评,可以創(chuàng)建任何依賴它的組件。

MainActivity 活動中創(chuàng)建測試代碼:

public final class MainActivity extends AppCompatActivity {

    @Inject
    AccountDao accountDao;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        DaggerApplication.ofComponent(this).inject(this);

        TextView contentText = findViewById(R.id.content_text);
        AsyncTask.execute(() -> {
            accountDao.insert(Account.of("aaaa", "123456"));
            accountDao.insert(Account.of("bbbb", "123456"));
            List<Account> list = accountDao.findAll();
            runOnUiThread(() -> contentText.setText(list.toString()));
        });

//        startActivity(new Intent(this, AccountActivity.class));
    }
}

我們移除了網絡框架的測試方法,因為已經確認它是正常工作淤年,就不需要再測試。當然慧脱,更好的辦法是將測試代碼轉移到單元測試包中蒙兰,由于篇幅限制芒篷,本章不準備討論采缚。

注意:網絡和數(shù)據(jù)庫都屬于 IO 操作扳抽,不能在 UI 線程上執(zhí)行,必須通過 異步 執(zhí)行贸呢。

運行一下看看:

測試代碼是正常工作楞陷,查看一下數(shù)據(jù)庫內容:

我們用 room 框架完成數(shù)據(jù)的增刪改查操作,在 Dagger2 支持下结执,這一切變得輕松簡單艾凯。

7.3 總結

我們所說的模塊化,其實就是在組合實例蜡感。網絡框架在網絡模塊中提供實例恃泪,數(shù)據(jù)庫框架在數(shù)據(jù)庫模塊中提供實例,應用上下文在應用模塊中提供實例杈笔。

當進行 Review 代碼或者新成員加入團隊時糕非,只需要迅速過一遍 di 包下的組件和模塊,就很容易看出來項目使用了哪些框架禁筏,這些框架在做什么事情衡招。

當然前面也提到過,這只是一種編碼習慣,你可以遵守也可以不遵守空执。你完全可以按照自己的喜好穗椅,去設計一套在組件中的成員注入方法,以及在模塊中的提供者方法门坷。只要你在以后的開發(fā)中袍镀,不會受到任何影響,那對你來說就是最好的習慣绸吸。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末宣虾,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌鹉胖,老刑警劉巖够傍,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異寂诱,居然都是意外死亡安聘,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門丘喻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來念颈,“玉大人,你說我怎么就攤上這事嗡靡。” “怎么了财边?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵点骑,是天一觀的道長黑滴。 經常有香客問我,道長袁辈,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任尾膊,我火速辦了婚禮荞彼,結果婚禮上鸣皂,老公的妹妹穿的比我還像新娘。我一直安慰自己寞缝,他們只是感情好,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布滩届。 她就那樣靜靜地躺著帜消,像睡著了一般趟据。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上汹碱,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機與錄音勘伺,去河邊找鬼褂删。 笑死,一個胖子當著我的面吹牛屯阀,可吹牛的內容都是我干的难衰。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼失暂,長吁一口氣:“原來是場噩夢啊……” “哼鳄虱!你這毒婦竟也來了?” 一聲冷哼從身側響起拙已,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤悠栓,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體楼镐,經...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡框产,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了戒突。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片描睦。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖隔崎,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情虚缎,我是刑警寧澤钓株,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布轴合,位于F島的核電站,受9級特大地震影響值桩,放射性物質發(fā)生泄漏。R本人自食惡果不足惜携栋,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一咳秉、第九天 我趴在偏房一處隱蔽的房頂上張望澜建。 院中可真熱鬧,春花似錦炕舵、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至辐赞,卻和暖如春硝训,著一層夾襖步出監(jiān)牢的瞬間新思,已是汗流浹背晃酒。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工贝次, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蛔翅。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓山析,卻偏偏與公主長得像,于是被迫代替她去往敵國和親秆剪。 傳聞我的和親對象是個殘疾皇子爵政,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內容