Dagger2 | 七、高级 - @Module

本章讨论 @Module 模块注解,它属于 Dagger2 框架的成员,用来管理提供方法。事实上,模块化是一种编码习惯,我们希望在同一个模块中,仅提供相同用途的实例,这可以保证代码具有良好的可阅读性和可维护性。

7.1 结构论述

针对现有的代码结构进行重构:

之前,我们在 ui 包下存放 ***Activity 活动,在 data 包下存放相关数据源。

开发初期,我们这样做完全没问题。一旦活动增加到几十个,在 ui 包下想要找到对应的活动,将变得十分困难。甚至有时候你无法区分是 LoggingActivity 活动还是 LoginActivity 活动。

为了改变这样的局面,设计一个良好的包结构将非常有必要。

我们推荐以 功能 划分包结构。在 account 包下面的所有类,与账户功能相关,而在 di 包下面的所有类,与依赖注入功能相关。划分这样的包结构,好处在于职责清晰,方便快速找到相关功能。

另外,如果后续增加几十个功能,也不用担心会创建很多顶级包。事实上,account 包下面可以有 user 子包、address 子包、order 子包等等,只要是与账户有关的功能,都可以划分到 account 包之下。因此,功能增加得再多,只要划分好顶级包的归属,就不会有类似的担心。

7.2 高级实战

我们建立网络模块和数据库模块,用来处理网络数据和本地数据。

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 可以转换数据流为基本类型
    • converter-gons 可以转换字符串为 json 串
  • okhttpSquare 为 JVM、Android 和 GraalVM 精心设计的 HTTP 客户端
    • logging-interceptor 顾名思义,是一个拦截器,用来实现日志打印

创建 api 包和 HaowanbaApi 接口:

public interface HaowanbaApi {

    @GET("/")
    Call home();
}

di 包下,创建 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;
    }
}

提示:如果模块中都是静态方法,Dagger2 就不会创建该模块的实例。

为了后续的重构,我们创建新的 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 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() {
            @SuppressLint("SetTextI18n")
            @Override
            public void onResponse(@NonNull Call call, @NonNull Response response) {
                runOnUiThread(() -> contentText.setText("获得响应:" + response));
            }

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

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

提示:TextView 组件必须在 UI 线程上操作,所以需要 runOnUithread 方法切换到 UI 线程。

AndroidManifest.xml 中增加 android:usesCleartextTraffic="true" 属性,并申请网络权限:




    

    
        // ...
    

我们运行一下看看:

查看日志:

网络模块已经打通,现在可以尽情享受冲浪的乐趣。

7.2.2 数据库模块

app 模块的 build.gradle 文件中,声明依赖:

dependencies {
    // ...

    // 数据库 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 数据库框架
  • guava 是谷歌开源的工具类,帮助写出更坚固更安全的 Java 代码
  • helper 是我个人使用的辅助工具

改造 Account 账户为数据库实体:

@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 账户的提供方法。

创建 AccountDao 映射:

@Dao
public interface AccountDao {

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

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

    @Query("SELECT * FROM account WHERE username = :username")
    Optional 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 类,我们创建 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);
    }
}

创建 ExampleDatabase 抽象类,用来组合上面的类:

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

    public abstract AccountDao accountDao();
}

将实体类型交给 @Databaseentities 参数,表示 room 可以根据实体创建对应的数据库表。

di 包下创建 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 上下文,前面也提出过这个问题,现在来解决一下。

创建 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);
}

现在需要修改一下 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 上下文的提供方法,可以创建任何依赖它的组件。

MainActivity 活动中创建测试代码:

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 list = accountDao.findAll();
            runOnUiThread(() -> contentText.setText(list.toString()));
        });

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

我们移除了网络框架的测试方法,因为已经确认它是正常工作,就不需要再测试。当然,更好的办法是将测试代码转移到单元测试包中,由于篇幅限制,本章不准备讨论。

注意:网络和数据库都属于 IO 操作,不能在 UI 线程上执行,必须通过 异步 执行。

运行一下看看:

测试代码是正常工作,查看一下数据库内容:

我们用 room 框架完成数据的增删改查操作,在 Dagger2 支持下,这一切变得轻松简单。

7.3 总结

我们所说的模块化,其实就是在组合实例。网络框架在网络模块中提供实例,数据库框架在数据库模块中提供实例,应用上下文在应用模块中提供实例。

当进行 Review 代码或者新成员加入团队时,只需要迅速过一遍 di 包下的组件和模块,就很容易看出来项目使用了哪些框架,这些框架在做什么事情。

当然前面也提到过,这只是一种编码习惯,你可以遵守也可以不遵守。你完全可以按照自己的喜好,去设计一套在组件中的成员注入方法,以及在模块中的提供者方法。只要你在以后的开发中,不会受到任何影响,那对你来说就是最好的习惯。

你可能感兴趣的:(Dagger2 | 七、高级 - @Module)