本章討論 @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 串
-
-
okhttp
是Square 為 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();
}
將實體類型交給 @Database
的 entities
參數(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
模塊板乙,它包含 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);
}
現(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ā)中袍镀,不會受到任何影響,那對你來說就是最好的習慣绸吸。