Android 學習筆記架構篇
- 一個元件應該只關注一個簡單的問題,只負責完成一項簡單的任務,應該盡少依賴其它元件
- 就算依賴另一個元件,也不能同時依賴它下下一級的元件,要像網路協議分層一樣簡單明確
-
Activity
和Fragment
作為作業系統和應用之間的粘合類,不應該將所有程式碼寫在它們裡面,它們甚至可以看成是有生命週期的普通 View,大部分情況下就是 被 用來簡單 顯示資料的
模型驅動檢視
- 為了保證資料 model 和它對應顯示的 UI 始終是一致的,應該用 model 驅動 UI,而且最好是是持久化 model。model 是負責處理應用資料的元件,只關心資料
單一資料來源
- 為了保證資料的一致性,必須實現相同的資料來自同一個資料來源。如: 好友列表頁顯示了好友的備註名,資料來源於伺服器的
api/friends
響應,好友詳情頁也顯示了好友的備註名,資料來源於伺服器的api/user
響應,此時在好友詳情頁更改了對這個好友的備註名,那麼好友列表並不知情,它的資料模型並沒有發生變化,所以還是顯示原來的備註名,這就產生了資料不一致的問題 - 要實現單一資料來源(Single source of truth),最簡單的方式就是將本地資料庫作為單一資料來源,主鍵和外來鍵的存在保證了資料對應實體的一致性
推薦架構

Android Jetpack 元件庫中有一個叫 Architecture Components 的元件集,裡面包含了 Data Binding,Lifecycles,LiveData,Navigation,Paging,Room,ViewModel,WorkManager 等元件的實現
-
ViewModel
用來為指定的 UI 元件提供資料,它只負責根據業務邏輯獲取合適的資料,他不知道 View 的存在,所以它不受系統銷燬重建的影響,一般它的生命週期比 View 更長久 -
LiveData
是一個數據持有者,它持有的資料可以是任何 Object 物件。它類似於傳統觀察者模式中的 Observable,當它持有的資料發生變化時會通知它所有的 Observer。同時它還可以感知 Activity,Fragment 和 Service 的生命週期,只通知它們中 active 的,在生命週期結束時自動取消訂閱 -
Activity/Fragment
持有ViewModel
進行資料的渲染,ViewModel
持有LiveData
形式的資料以便尊重應用元件的生命週期,但是獲取LiveData
的具體實現應該由 Repository 完成 - Repository 是資料的抽象,它提供簡潔一致的操作資料的 API,內部封裝好對持久化資料、快取資料、後臺伺服器資料等資料來源資料的操作。所以
ViewModel
不關心資料具體是怎麼獲得的,甚至可以不關心資料到底是從哪拿到的
實踐
基礎設施建設
建立專案時要勾選 【Use AndroidX artifacts】 複選框以便自動使用 AndroidX 支援庫,否則需要手動在 gradle.properties
檔案中新增
android.useAndroidX=true android.enableJetifier=true 複製程式碼
然後在專案根目錄建立 versions.gradle
檔案,以便統一管理依賴和版本號
ext.deps = [:] def build_versions = [:] build_versions.min_sdk = 14 build_versions.target_sdk = 28 ext.build_versions = build_versions def versions = [:] versions.android_gradle_plugin = "3.3.0" versions.support = "1.1.0-alpha01" versions.constraint_layout = "1.1.3" versions.lifecycle = "2.0.0" versions.room = "2.1.0-alpha04" versions.retrofit = "2.5.0" versions.okhttp = "3.12.1" versions.junit = "4.12" versions.espresso = "3.1.0-alpha4" versions.atsl_runner = "1.1.0-alpha4" versions.atsl_rules = "1.1.0-alpha4" def deps = [:] deps.android_gradle_plugin = "com.android.tools.build:gradle:$versions.android_gradle_plugin" def support = [:] support.app_compat = "androidx.appcompat:appcompat:$versions.support" support.v4 = "androidx.legacy:legacy-support-v4:$versions.support" support.constraint_layout = "androidx.constraintlayout:constraintlayout:$versions.constraint_layout" support.recyclerview = "androidx.recyclerview:recyclerview:$versions.support" support.cardview = "androidx.cardview:cardview:$versions.support" support.design = "com.google.android.material:material:$versions.support" deps.support = support def lifecycle = [:] lifecycle.runtime = "androidx.lifecycle:lifecycle-runtime:$versions.lifecycle" lifecycle.extensions = "androidx.lifecycle:lifecycle-extensions:$versions.lifecycle" lifecycle.java8 = "androidx.lifecycle:lifecycle-common-java8:$versions.lifecycle" lifecycle.compiler = "androidx.lifecycle:lifecycle-compiler:$versions.lifecycle" deps.lifecycle = lifecycle def room = [:] room.runtime = "androidx.room:room-runtime:$versions.room" room.compiler = "androidx.room:room-compiler:$versions.room" deps.room = room def retrofit = [:] retrofit.runtime = "com.squareup.retrofit2:retrofit:$versions.retrofit" retrofit.gson = "com.squareup.retrofit2:converter-gson:$versions.retrofit" deps.retrofit = retrofit deps.okhttp_logging_interceptor = "com.squareup.okhttp3:logging-interceptor:${versions.okhttp}" deps.junit = "junit:junit:$versions.junit" def espresso = [:] espresso.core = "androidx.test.espresso:espresso-core:$versions.espresso" deps.espresso = espresso def atsl = [:] atsl.runner = "androidx.test:runner:$versions.atsl_runner" deps.atsl = atsl ext.deps = deps 複製程式碼
以顯示 谷歌的開源倉庫列表 ( api.github.com/users/googl… )為例,先依賴好 ViewModel
、 LiveData
和 Retrofit
:
apply plugin: 'com.android.application' android { compileSdkVersion build_versions.target_sdk defaultConfig { applicationId "cn.frank.sample" minSdkVersion build_versions.min_sdk targetSdkVersion build_versions.target_sdk versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } lintOptions { abortOnError false } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation deps.support.app_compat implementation deps.support.constraint_layout implementation deps.lifecycle.runtime implementation deps.lifecycle.extensions annotationProcessor deps.lifecycle.compiler implementation deps.room.runtime annotationProcessor deps.room.compiler implementation deps.retrofit.runtime implementation deps.retrofit.gson implementation deps.okhttp_logging_interceptor testImplementation deps.junit androidTestImplementation deps.atsl.runner androidTestImplementation deps.espresso.core } 複製程式碼
然後根據習慣合理地設計原始碼的目錄結構,如

public class RepoRepository { private static RepoRepository sInstance; public RepoRepository() { } public static RepoRepository getInstance() { if (sInstance == null) { synchronized (RepoRepository.class) { if (sInstance == null) { sInstance = new RepoRepository(); } } } return sInstance; } public LiveData<List<Repo>> getRepo(String userId) { final MutableLiveData<List<Repo>> data = new MutableLiveData<>(); ServiceGenerator.createService(GithubService.class) .listRepos(userId) .enqueue(new Callback<List<Repo>>() { @Override public void onResponse(Call<List<Repo>> call, Response<List<Repo>> response) { data.setValue(response.body()); } @Override public void onFailure(Call<List<Repo>> call, Throwable t) { } }); return data; } } 複製程式碼
public class RepoViewModel extends AndroidViewModel { private LiveData<List<Repo>> repo; private RepoRepository repoRepository; public RepoViewModel(@NonNull Application application) { super(application); this.repoRepository = ((SampleApp) application).getRepoRepository(); } public void init(String userId) { if (this.repo != null) { return; } this.repo = repoRepository.getRepo(userId); } public LiveData<List<Repo>> getRepo() { return repo; } } 複製程式碼
public class RepoFragment extends Fragment { private static final String ARG_USER_ID = "user_id"; private RepoViewModel viewModel; private TextView repoTextView; public RepoFragment() { } public static RepoFragment newInstance(String userId) { RepoFragment fragment = new RepoFragment(); Bundle args = new Bundle(); args.putString(ARG_USER_ID, userId); fragment.setArguments(args); return fragment; } @Nullable @Override public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View rootView =inflater.inflate(R.layout.fragment_repo, container, false); repoTextView = (TextView) rootView.findViewById(R.id.repo); return rootView; } @Override public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); Bundle args = getArguments(); if (args != null) { String userId = args.getString(ARG_USER_ID); viewModel = ViewModelProviders.of(this).get(RepoViewModel.class); viewModel.init(userId); viewModel.getRepo().observe(this, new Observer<List<Repo>>() { @Override public void onChanged(List<Repo> repos) { StringBuilder builder = new StringBuilder(); if (repos != null) { for (Repo repo : repos) { builder.append(repo.getFull_name()).append("\n"); } } repoTextView.setText(builder); } }); } } } 複製程式碼
這是最簡單直接的實現,但還是存下很多模板程式碼,還有很多地方可以優化
- 既然 View 是和 ViewModel 繫結在一起的,那為什麼每次都要先
findViewById()
再setText()
呢?在宣告或者建立 View 的時候就給它指定好對應的 ViewModel 不是更簡單直接麼 - 網路請求的結果最好都快取到記憶體和資料庫中,既保證了單一資料來源原則又能提升使用者體驗
Data Binding
對於第一個問題,Data Binding 元件是一個還算不錯的實現,可以在佈局檔案中使用 表示式語言 直接給 View 繫結 資料,繫結可以是單向的也可以是雙向的。Data Binding 這樣繫結可以避免記憶體洩漏,因為它會自動取消繫結。可以避免空指標,因為它會寬容評估表示式。可以避免同步問題,可以在後臺執行緒更改非集合資料模型,因為它會在評估時本地化資料
為了使用 Data Binding,需要在 app module 的 build.gradle
檔案中新增
dataBinding { enabled = true } 複製程式碼
<layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="user" type="com.example.User"/> </data> <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.firstName}"/> </LinearLayout> </layout> 複製程式碼
利用 @{}
語法可以給 View 的屬性繫結資料變數,但是該表示式語法應該儘可能簡單直接,複雜的邏輯應該藉助於自定義 BindingAdapter
不需要重新編譯程式碼,構建工具就會為每個這樣的佈局檔案自動生成一個對應的繫結類,繼承自 ViewDataBinding
,路徑為 app/build/generated/data_binding_base_class_source_out/debug/dataBindingGenBaseClassesDebug/out/cn/frank/sample/databinding/FragmentRepoBinding.java
,預設的類名是佈局檔名的大駝峰命名加上 Binding 字尾,如 fragment_repo.xml
對應 FragmentRepoBinding
,可以通過 <data class=".ContactItem">
自定義類名和所在包名。可以通過 DataBindingUtil
的 inflate()
等靜態方法或自動生成的繫結類的 inflate()
等靜態方法獲取繫結類的例項,然後就可以操作這個例項了
操作符和關鍵字
這個表示式語言的 操作符和關鍵字 包括: 數學運算 + - / * %
,字串拼接 +
,邏輯 && ||
,二進位制運算 & | ^
,一元操作符 + - ! ~
,移位 >> >>> <<
,比較 == > < >= <=
,判斷例項 instanceof
,分組 ()
,字元/字串/數字/ null
的字面量,強制轉化,方法呼叫,欄位訪問,陣列訪問 []
,三目運算子 ?:
,二目空預設運算子 ??
android:text="@{String.valueOf(index + 1)}" android:visibility="@{age > 13 ? View.GONE : View.VISIBLE}" android:transitionName='@{"image_" + id}' android:text="@{user.displayName ?? user.lastName}" android:text="@{user.lastName}" android:padding="@{large? @dimen/largePadding : @dimen/smallPadding}" android:text="@{@string/nameFormat(firstName, lastName)}" android:text="@{@plurals/banana(bananaCount)}" 複製程式碼
小於比較符 <
需要轉義為 <
,為了避免字串轉義單引號和雙引號可以隨便切換使用
<import>
的類衝突時可以取別名加以區分
<import type="android.view.View"/> <import type="com.example.real.estate.View" alias="Vista"/> 複製程式碼
<include>
佈局中可以傳遞變數
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:bind="http://schemas.android.com/apk/res-auto"> <data> <variable name="user" type="com.example.User"/> </data> <LinearLayout android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <include layout="@layout/name" bind:user="@{user}"/> <include layout="@layout/contact" bind:user="@{user}"/> </LinearLayout> </layout> 複製程式碼
不支援 <merge>
結合 <include>
的使用
事件處理
View 事件的分發處理有兩種機制,一種是 Method references ,在表示式中直接通過監聽器方法的簽名來引用,Data Binding 會在編譯時評估這個表示式,如果方法不存在或者簽名錯誤那麼編譯就會報錯,如果表示式評估的結果是 null
那麼 Data Binding 就不會建立監聽器而是直接設定 null
監聽器,Data Binding 在 繫結資料的時候 就會建立監聽器的例項: android:onClick="@{handlers::onClickFriend}"
。一種是 Listener bindings ,Data Binding 在 事件發生的時候 才會建立監聽器的例項並設定給 view然後評估 lambda 表示式, android:onClick="@{(theView) -> presenter.onSaveClick(theView, task)}"
繫結 Observable 資料
雖然 View 可以繫結任何 PO 物件,但是所繫結物件的更改並不能自動引起 View 的更新,所以 Data Binding 內建了 Observable
介面和它的 BaseObservable
, ObservableBoolean
等子類可以方便地將物件、欄位和集合變成 observable
private static class User { public final ObservableField<String> firstName = new ObservableField<>(); public final ObservableInt age = new ObservableInt(); } 複製程式碼
private static class User extends BaseObservable { private String firstName; private String lastName; @Bindable public String getFirstName() { return this.firstName; } @Bindable public String getLastName() { return this.lastName; } public void setFirstName(String firstName) { this.firstName = firstName; notifyPropertyChanged(BR.firstName); } public void setLastName(String lastName) { this.lastName = lastName; notifyPropertyChanged(BR.lastName); } } 複製程式碼
執行繫結
有時候繫結需要立即執行,如在 onBindViewHolder()
方法中:
public void onBindViewHolder(BindingHolder holder, int position) { final T item = mItems.get(position); holder.getBinding().setVariable(BR.item, item); holder.getBinding().executePendingBindings(); } 複製程式碼
Data Binding 在為 View 設定表示式的值的時候會自動選擇對應 View 屬性的 setter 方法,如 android:text="@{user.name}"
會選擇 setText()
方法,但是像 android:tint
屬性沒有 setter 方法,可以使用 BindingMethods
註解自定義方法名
@BindingMethods({ @BindingMethod(type = "android.widget.ImageView", attribute = "android:tint", method = "setImageTintList"), }) 複製程式碼
如果要自定義 setter 方法的繫結邏輯,可以使用 BindingAdapter
註解
@BindingAdapter("android:paddingLeft") public static void setPaddingLeft(View view, int padding) { view.setPadding(padding, view.getPaddingTop(), view.getPaddingRight(), view.getPaddingBottom()); } 複製程式碼
<ImageView app:imageUrl="@{venue.imageUrl}" app:error="@{@drawable/venueError}" /> 複製程式碼
@BindingAdapter({"imageUrl", "error"}) public static void loadImage(ImageView view, String url, Drawable error) { Picasso.get().load(url).error(error).into(view); } 複製程式碼
如果要自定義表示式值的自動型別轉換,可以使用 BindingConversion
註解
<View android:background="@{isError ? @color/red : @color/white}" android:layout_width="wrap_content" android:layout_height="wrap_content"/> 複製程式碼
@BindingConversion public static ColorDrawable convertColorToDrawable(int color) { return new ColorDrawable(color); } 複製程式碼
ViewModel
可以實現 Observable
介面並結合 PropertyChangeRegistry
可以更方便地控制資料更改後的行為
雙向繫結
使用 @={}
符號可以實現 View 和資料的雙向繫結
<CheckBox android:id="@+id/rememberMeCheckBox" android:layout_width="wrap_content" android:layout_height="wrap_content" android:checked="@={viewmodel.rememberMe}" /> 複製程式碼
public class LoginViewModel extends BaseObservable { // private Model data = ... @Bindable public Boolean getRememberMe() { return data.rememberMe; } public void setRememberMe(Boolean value) { // 為了防止無限迴圈,必須要先檢查再更新 if (data.rememberMe != value) { data.rememberMe = value; saveData(); notifyPropertyChanged(BR.remember_me); } } } 複製程式碼
自定義屬性的雙向繫結還需要藉助 @InverseBindingAdapter
和 @InverseBindingMethod
@BindingAdapter("time") public static void setTime(MyView view, Time newValue) { // Important to break potential infinite loops. if (view.time != newValue) { view.time = newValue; } } @InverseBindingAdapter("time") public static Time getTime(MyView view) { return view.getTime(); } 複製程式碼
監聽屬性的更改,事件屬性以 AttrChanged
作為字尾
@BindingAdapter("app:timeAttrChanged") public static void setListeners( MyView view, final InverseBindingListener attrChange) { // Set a listener for click, focus, touch, etc. } 複製程式碼
可以藉助轉換器類定製 View 的顯示規則
<EditText android:id="@+id/birth_date" android:text="@={Converter.dateToString(viewmodel.birthDate)}" /> 複製程式碼
public class Converter { @InverseMethod("stringToDate") public static String dateToString(EditText view, long oldValue, long value) { // Converts long to String. } public static long stringToDate(EditText view, String oldValue, String value) { // Converts String to long. } } 複製程式碼
Data Binding 內建了 android:text
, android:checked
等的雙向繫結
生命週期敏感元件
在 Activity 或 Fragment 的生命週期方法中進行其它元件的配置並不總是合理的,如在 onStart()
方法中註冊廣播接收器 A、開啟定位服務 A、啟用元件 A 的監聽、啟用元件 B 的監聽等等,在 onStop()
方法中登出廣播接收器 A、關閉定位服務 A、停用元件 A 的監聽、停用元件 B 的監聽等等,隨著業務邏輯的增加這些生命週期方法變得越來越臃腫、越來越亂、越來越難以維護,如果這些元件在多個 Activity 或 Fragment 上使用那麼還得重複相同的邏輯,就更難以維護了。 而且如果涉及到非同步甚至沒辦法保證 onStart()
方法中的程式碼一定在 onStop()
方法執行前執行
關注點分離,這些元件的行為受生命週期的影響,所以它們自己應該意識到自己是生命週期敏感的元件,當生命週期變化時它們應該 自己決定 自己的行為,而不是交給生命週期的擁有者去處理
生命週期有兩個要素: 事件和狀態,事件的發生一般會導致生命週期狀態的改變
生命週期敏感元件應該實現 LifecycleObserver
以觀察 LifecycleOwner
的生命週期,支援庫中的 Activity 和 Fragment 都實現了 LifecycleOwner
,可以直接通過它的 getLifecycle()
方法獲取 Lifecycle
例項
class MyLocationListener implements LifecycleObserver { private boolean enabled = false; @OnLifecycleEvent(Lifecycle.Event.ON_START) void start() { if (enabled) { // connect } } public void enable() { enabled = true; if (lifecycle.getCurrentState().isAtLeast(STARTED)) { // connect if not connected } } @OnLifecycleEvent(Lifecycle.Event.ON_STOP) void stop() { // disconnect if connected } } 複製程式碼
GenericLifecycleObserver
介面繼承了 LifecycleObserver
,有一個介面方法 onStateChanged(LifecycleOwner, Lifecycle.Event)
表明它可以接收所有的生命週期過渡事件
LiveData
@MainThread public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) { assertMainThread("observe"); if (owner.getLifecycle().getCurrentState() == DESTROYED) { // ignore return; } LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer); ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper); if (existing != null && !existing.isAttachedTo(owner)) { throw new IllegalArgumentException("Cannot add the same observer" + " with different lifecycles"); } if (existing != null) { return; } owner.getLifecycle().addObserver(wrapper); } 複製程式碼
說明 LiveData
只能在主執行緒中訂閱,訂閱的觀察者被包裝成生命週期元件的觀察者 LifecycleBoundObserver
class LifecycleBoundObserver extends ObserverWrapper implements GenericLifecycleObserve @NonNull final LifecycleOwner mOwner; LifecycleBoundObserver(@NonNull LifecycleOwner owner, Observer<? super T> observer) super(observer); mOwner = owner; } @Override boolean shouldBeActive() { return mOwner.getLifecycle().getCurrentState().isAtLeast(STARTED); } @Override public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) { if (mOwner.getLifecycle().getCurrentState() == DESTROYED) { removeObserver(mObserver); return; } activeStateChanged(shouldBeActive()); } @Override boolean isAttachedTo(LifecycleOwner owner) { return mOwner == owner; } @Override void detachObserver() { mOwner.getLifecycle().removeObserver(this); } } 複製程式碼
當觀察到生命週期狀態變化時會呼叫 onStateChanged()
方法,所以當狀態為 DESTROYED
的時候會移除資料觀察者和生命週期觀察者, shouldBeActive()
方法的返回值表明只有生命週期狀態是 STARTED
和 RESUMED
的 LifecycleOwner
對應的資料觀察者才是 active 的,只有 active 的資料觀察者才會被通知到,當資料觀察者第一次從 inactive 變成 active 時,也會收到通知
observeForever()
方法也可以訂閱,但是 LiveData
不會自動移除資料觀察者,需要主動呼叫 removeObserver()
方法移除
LiveData
的 MutableLiveData
子類提供了 setValue()
方法可以在主執行緒中更改所持有的資料,還提供了 postValue()
方法可以在後臺執行緒中更改所持有的資料
可以繼承 LiveData
實現自己的 observable 資料, onActive()
方法表明有 active 的觀察者了,可以進行資料更新通知了, onInactive()
方法表明沒有任何 active 的觀察者了,可以清理資源了
單例的 LiveData
可以實現多個 Activity 或 Fragment 的資料共享
可以對 LiveData
持有的資料進行變換,需要藉助 Transformations
工具類
private final PostalCodeRepository repository; private final MutableLiveData<String> addressInput = new MutableLiveData(); public final LiveData<String> postalCode = Transformations.switchMap(addressInput, (address) -> { return repository.getPostCode(address); }); 複製程式碼
private LiveData<User> getUser(String id) { ...; } LiveData<String> userId = ...; LiveData<User> user = Transformations.switchMap(userId, id -> getUser(id) ); 複製程式碼
LiveData
的 MediatorLiveData
子類可以 merge 多個 LiveData 源,可以像 ReactiveX 的操作符一樣進行各種變換