Android 學習筆記架構篇

薛定貓的諤發表於2019-02-22

架構原則

關注點分離

  • 一個元件應該只關注一個簡單的問題,只負責完成一項簡單的任務,應該盡少依賴其它元件
  • 就算依賴另一個元件,也不能同時依賴它下下一級的元件,要像網路協議分層一樣簡單明確
  • ActivityFragment 作為作業系統和應用之間的粘合類,不應該將所有程式碼寫在它們裡面,它們甚至可以看成是有生命週期的普通 View,大部分情況下就是 用來簡單 顯示資料的

模型驅動檢視

  • 為了保證資料 model 和它對應顯示的 UI 始終是一致的,應該用 model 驅動 UI,而且最好是是持久化 model。model 是負責處理應用資料的元件,只關心資料

單一資料來源

  • 為了保證資料的一致性,必須實現相同的資料來自同一個資料來源。如: 好友列表頁顯示了好友的備註名,資料來源於伺服器的 api/friends 響應,好友詳情頁也顯示了好友的備註名,資料來源於伺服器的 api/user 響應,此時在好友詳情頁更改了對這個好友的備註名,那麼好友列表並不知情,它的資料模型並沒有發生變化,所以還是顯示原來的備註名,這就產生了資料不一致的問題
  • 要實現單一資料來源(Single source of truth),最簡單的方式就是將本地資料庫作為單一資料來源,主鍵和外來鍵的存在保證了資料對應實體的一致性

推薦架構

arch
Android Jetpack 元件庫中有一個叫 Architecture Components 的元件集,裡面包含了 Data Binding,Lifecycles,LiveData,Navigation,Paging,Room,ViewModel,WorkManager 等元件的實現

  • ViewModel 用來為指定的 UI 元件提供資料,它只負責根據業務邏輯獲取合適的資料,他不知道 View 的存在,所以它不受系統銷燬重建的影響,一般它的生命週期比 View 更長久
    Android 學習筆記架構篇
  • 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…)為例,先依賴好 ViewModelLiveDataRetrofit:

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
}
複製程式碼

然後根據習慣合理地設計原始碼的目錄結構,如

Android 學習筆記架構篇

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

}
複製程式碼

這是最簡單直接的實現,但還是存下很多模板程式碼,還有很多地方可以優化

  1. 既然 View 是和 ViewModel 繫結在一起的,那為什麼每次都要先 findViewById()setText() 呢?在宣告或者建立 View 的時候就給它指定好對應的 ViewModel 不是更簡單直接麼
  2. ViewModel 的實現真的優雅嗎?init() 方法和 getRepo() 方法耦合的嚴重麼?ViewModel 應該在什麼時刻開始載入資料?
  3. 網路請求的結果最好都快取到記憶體和資料庫中,既保證了單一資料來源原則又能提升使用者體驗

Data Binding

對於第一個問題,Data Binding 元件是一個還算不錯的實現,可以在佈局檔案中使用 表示式語言 直接給 View 繫結資料,繫結可以是單向的也可以是雙向的。Data Binding 這樣繫結可以避免記憶體洩漏,因為它會自動取消繫結。可以避免空指標,因為它會寬容評估表示式。可以避免同步問題,可以在後臺執行緒更改非集合資料模型,因為它會在評估時本地化資料
為了使用 Data Binding,需要在 app module 的 build.gradle 檔案中新增

dataBinding {
    enabled = true
}
複製程式碼

利用 @{} 語法可以給 View 的屬性繫結資料變數,但是該表示式語法應該儘可能簡單直接,複雜的邏輯應該藉助於自定義 BindingAdapter

<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>
複製程式碼

不需要重新編譯程式碼,構建工具就會為每個這樣的佈局檔案自動生成一個對應的繫結類,繼承自 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"> 自定義類名和所在包名。可以通過 DataBindingUtilinflate() 等靜態方法或自動生成的繫結類的 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)}"
複製程式碼

小於比較符 < 需要轉義為 &lt;,為了避免字串轉義單引號和雙引號可以隨便切換使用
<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 介面和它的 BaseObservableObservableBoolean 等子類可以方便地將物件、欄位和集合變成 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:textandroid:checked 等的雙向繫結

生命週期敏感元件

在 Activity 或 Fragment 的生命週期方法中進行其它元件的配置並不總是合理的,如在 onStart() 方法中註冊廣播接收器 A、開啟定位服務 A、啟用元件 A 的監聽、啟用元件 B 的監聽等等,在 onStop() 方法中登出廣播接收器 A、關閉定位服務 A、停用元件 A 的監聽、停用元件 B 的監聽等等,隨著業務邏輯的增加這些生命週期方法變得越來越臃腫、越來越亂、越來越難以維護,如果這些元件在多個 Activity 或 Fragment 上使用那麼還得重複相同的邏輯,就更難以維護了。 而且如果涉及到非同步甚至 沒辦法保證 onStart() 方法中的程式碼 一定onStop() 方法執行前執行
關注點分離,這些元件的行為受生命週期的影響,所以它們自己應該意識到自己是生命週期敏感的元件,當生命週期變化時它們應該 自己決定 自己的行為,而不是交給生命週期的擁有者去處理
生命週期有兩個要素: 事件和狀態,生命週期事件的發生一般會導致生命週期狀態的改變
生命週期敏感元件應該實現 LifecycleObserver 以觀察 LifecycleOwner 的生命週期,支援庫中的 Activity 和 Fragment 都實現了 LifecycleOwner,可以直接通過它的 getLifecycle() 方法獲取 Lifecycle 例項

MainActivity.this.getLifecycle().addObserver(new MyLocationListener());
複製程式碼
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

它的 observe() 方法原始碼

@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() 方法的返回值表明只有生命週期狀態是 STARTEDRESUMEDLifecycleOwner 對應的資料觀察者才是 active 的,只有 active 的資料觀察者才會被通知到,當資料觀察者 第一次 從 inactive 變成 active 時,也會 收到通知
observeForever() 方法也可以訂閱,但是 LiveData 不會自動移除資料觀察者,需要主動呼叫 removeObserver() 方法移除
LiveDataMutableLiveData 子類提供了 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) );
複製程式碼

LiveDataMediatorLiveData 子類可以 merge 多個 LiveData 源,可以像 ReactiveX 的操作符一樣進行各種變換

幾點感悟

  • 避免手動寫任何模板程式碼,尤其是寫業務程式碼時
  • 不要用回撥的思想去寫 Rx 的程式碼,用 Stream 思想去寫,思想要徹底轉變過來
  • 把非同步的思想調整回同步:
void businessLogic() {
    showLoadingView();
    request(uri, params, new Callbacks() {
    
        @Override
        void onSuccess(Result result) {
            showDataView(result);
        }
        
        @Override
        void onFailure(Error error) {
            showErrorView(error);
        }
        
    });
}
複製程式碼
void businessLogic() async {
    showLoadingView()
    Result result = await request(uri, params)
    result.ok() ? showDataView(result) : showErrorView(result)
}
複製程式碼
  • 約定優於配置 ,適當的約定可以減少相當可觀的勞動量
  • 能自動的就不要手動,比如取消網路請求等操作要自動進行,不要出現手動操作的程式碼
  • 避免原始碼中模板程式碼的重複,包括用工具自動生成的
  • 藉助編譯器在編譯時生成的程式碼就像一把雙刃劍,它獨立於原始碼庫,又與原始碼緊密聯絡,要謹慎使用

Sample

相關文章