本章讨论 @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 串
-
-
okhttp
是Square 为 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();
}
将实体类型交给 @Database
的 entities
参数,表示 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
模块,它包含 NetworkModule
和 DatabaseModule
模块,并提供 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
包下的组件和模块,就很容易看出来项目使用了哪些框架,这些框架在做什么事情。
当然前面也提到过,这只是一种编码习惯,你可以遵守也可以不遵守。你完全可以按照自己的喜好,去设计一套在组件中的成员注入方法,以及在模块中的提供者方法。只要你在以后的开发中,不会受到任何影响,那对你来说就是最好的习惯。