我所理解的 Android Architecture Components

我所能理解的 Android Architecture Components

寫在前面
Android Architecture Components 是 Google 2017 年 I/O 大會提出的一種架構解決方案。在此之前,Android 應用大多數以 MVC MVP MVVM 等比較常見的架構方式被構建。看到這樣各自爲戰的情況,再加上開發者們強烈的意願,Google 自然也坐不住了,推出了 AAC 這種架構方式,看它的命名,Android Architecture Components,是不是大有一統江湖的意思。

架構變遷

在介紹 AAC 架構之前,想和大家聊聊之前常見架構模式的變遷。
在軟件行業,無論是什麼形式的架構,大家都有一個共識,如果實踐是檢驗真理的唯一標準,那可測試性就是檢驗架構好壞的唯一標準。好的架構會盡量讓邏輯開發者關注邏輯本身,並且減少模板代碼。

無架構模式 – MVC

大家可以想象一下,我們從把所有代碼寫在一團到有了 MVC 架構模式,有哪些提升?

  • 可測試性顯著提升(雖然我知道很多公司和開發者都沒有寫單元測試的習慣)
  • 數據隔離、關注點隔離

我們從沒有架構,到 MVC,終於能做到「分層」了。

MVC - MVP

之後我們寫着寫着又發現,MVP 對比 MVC 更有優勢:

  • 面向接口編程
  • 將邏輯從 UI 組件(Activity、Fragment)轉移到 P 層,進一步提升可測試性

爲什麼邏輯放在 P 層要更合理呢?因爲首先 UI 組件是連接你和 OS 的那一層,也就是說他並不完完全全屬於開發者本身,會受到系統的影響,比如配置的更改、內存不足重啓等等,所以儘量保證 UI 組件整潔,絕對會提升代碼的可測試性和穩健性。
其次也符合 Passive View Pattern 的這種架構建議模式,其實我們也可以把 Passive View Pattern 這種模式直接類比到後臺和前端的關係,這也是爲什麼大多數公司後臺開發者的數量大於前端開發者的數量。

現存問題

無論是 MVC MVP MVVM 它們存在什麼問題呢?
他們的模塊通信方式,始終是持有對象(或接口)。對於 Android 系統來說,每一個你在 Manifest 聲明的四大組件都有可能會突然死掉,這也造成了:

  • 持有對象可能會有 NPE
  • 會有可能內存泄漏
  • 會寫很多生命週期相關的模板代碼

爲了掩蓋或解決以上種種問題,AAC 架構就應運而生了,AAC 架構包括了一系列組件,比如 LifeCycle Room LiveData ViewModel。其實無論是 MVC MVP MVVM 或者是其他的架構方式,數據層的爭議是最小的,AAC 也提出了一種數據層的構建方式 – Repository,我們可以先來看看 Repository。

Repository

一個可擴展的、高內聚的數據層應該有哪些特性?

  • 涵蓋多個數據來源(Network、Database、File、Cache)
  • 對數據進行必要的邏輯處理(緩存、格式轉換等)
  • 對外提供單一的數據出口(對外的方法應該是 getData,而不是 getNetData/getCacheData)

舉個例子?

class TasksRepository

    private final TasksDataSource mTasksRemoteDataSource;
    private final TasksDataSource mTasksLocalDataSource;

    // Prevent direct instantiation.
    private TasksRepository(@NonNull TasksDataSource tasksRemoteDataSource,
                            @NonNull TasksDataSource tasksLocalDataSource) {
        mTasksRemoteDataSource = checkNotNull(tasksRemoteDataSource);
        mTasksLocalDataSource = checkNotNull(tasksLocalDataSource);
    }

TasksRepository 是對外提供 Task 的數據層,可以看到他持有了 tasksRemoteDataSourcetasksLocalDataSource 兩個對象,這就屬於我們剛纔提到的,多個數據來源。並且他們都實現了 TasksDataSource 符合面向接口編程。

再來看它的一個對數據操作的方法:

/**
     * Gets tasks from local data source (sqlite) unless the table is new or empty. In that case it
     * uses the network data source. This is done to simplify the sample.
     * <p>
     * Note: {@link GetTaskCallback#onDataNotAvailable()} is fired if both data sources fail to
     * get the data.
     */
    @Override
    public void getTask(@NonNull final String taskId, @NonNull final GetTaskCallback callback) {
        checkNotNull(taskId);
        checkNotNull(callback);

        Task cachedTask = getTaskWithId(taskId);

        // Respond immediately with cache if available
        if (cachedTask != null) {
            callback.onTaskLoaded(cachedTask);
            return;
        }

        // Load from server/persisted if needed.

        // Is the task in the local data source? If not, query the network.
        mTasksLocalDataSource.getTask(taskId, new GetTaskCallback() {
            @Override
            public void onTaskLoaded(Task task) {
                // Do in memory cache update to keep the app UI up to date
                if (mCachedTasks == null) {
                    mCachedTasks = new LinkedHashMap<>();
                }
                mCachedTasks.put(task.getId(), task);
                callback.onTaskLoaded(task);
            }

            @Override
            public void onDataNotAvailable() {
                mTasksRemoteDataSource.getTask(taskId, new GetTaskCallback() {
                    @Override
                    public void onTaskLoaded(Task task) {
                        // Do in memory cache update to keep the app UI up to date
                        if (mCachedTasks == null) {
                            mCachedTasks = new LinkedHashMap<>();
                        }
                        mCachedTasks.put(task.getId(), task);
                        callback.onTaskLoaded(task);
                    }

                    @Override
                    public void onDataNotAvailable() {
                        callback.onDataNotAvailable();
                    }
                });
            }
        });
    }

可以看到這個方法符合剛纔所說的,只對調用者提供單一的數據出口,調用方根本不需要知道數據來自於緩存還是網絡還是數據庫,並且也隱藏了緩存數據的邏輯,這就是一個典型的 Repository 構建方式。這個例子來自於 google sample todo-mvp

在 AAC 架構中,Repository 中數據庫爲 Room,不過如果你有已經在使用的,穩定的數據庫解決方案,其實是沒有必要替換的,所以 Room 的使用不是本文的重點。數據庫中讀出來的數據,自然可以配合 LiveData,關於 LiveData 的特性,一會再談。

所以以上所說的 Repository 應該是如圖所示:
Repository

ViewModel

可以試想一下,加入我們把 Repository 直接寫在 UI 組件(Activity、Fragment)裏面,會造成哪些問題呢?

  • 違背了 UI 層儘量簡單的原則,並不能很好的進行測試
  • 因爲 Activity、Fragment 可能因爲配置更改、內存不足,導致了視圖銷燬或者重建,此時就會產生大量的冗餘邏輯代碼

爲了解決以上問題,需要一個數據和 UI 組件的中間層 – ViewModel

官方文檔對 ViewModel 的概括:

The ViewModel class is designed to store and manage UI-related data in a lifecycle conscious way. The ViewModel class allows data to survive configuration changes such as screen rotations.

也就是說 ViewModel 是基於生命週期感知的,來持有並管理 UI 層相關數據的類,並且它可以在 app 的 config 發生改變重建時,始終存活下來。

ViewModel

看了 ViewModel 的描述和它的生命週期圖,可以總結下 ViewModel 的特性:

  • 生命週期感知。一般在 UI 組件的 onCreate 方法中由開發者手動的創建,隨着 UI 組件真正銷燬調用 onDestory 時,調用 onCleared 方法銷燬。
  • 與 UI 組件解耦。無論由於配置更改 UI 組件反覆創建了多少個實例,對 ViewModel 來說都是未知的,ViewModel 也不需要關心,UI 組件最終拿到的都是第一次創建的那個保持原有數據的 ViewModel。並且 ViewModel 中也不應該有任何 android * 包下的引用(除了 android * arch),如果需要一個 Application 的 Context,啓動一些 Service 的話,使用 AndroidViewModel 這個類就可以了。

來看一個簡單的例子:

public class UserProfileViewModel extends ViewModel {
    private String userId;
    private User user;

    public void init(String userId) {
        this.userId = userId;
    }
    public User getUser() {
        return user;
    }
}


public class UserProfileFragment extends Fragment {
    private static final String UID_KEY = "uid";
    private UserProfileViewModel viewModel;

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        String userId = getArguments().getString(UID_KEY);
        viewModel = ViewModelProviders.of(this).get(UserProfileViewModel.class);
        viewModel.init(userId);
    }

    @Override
    public View onCreateView(LayoutInflater inflater,
                @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.user_profile, container, false);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        User user = viewModel.getUser();
    }
}

也許你覺得這個 demo 看起來平平無奇,但是 ViewModel 自身的框架已經爲你做了以下工作:

  • 隨着 Fragment 的 onDestory,ViewModel 自動 onCleared
  • 配置更改 Fragment 重啓時,ViewModel 未感知,始終攜帶數據存活
  • ViewModel 不持有任何 View 相關的引用。(在任何位置持有 Activity、Fragment 都是有風險的,有可能造成 NPE、內存泄漏)

肯定有同學會問,你 ViewModel 都不持有 View 的對象,那數據更改了,怎麼通知 View 呢?對了就是我們接下來要說的觀察者模式,觀察者模式對於 MVC MVP 這種持有對象和接口的模式來說,簡直就是降維打擊,可以試想一下,如果我的 View 層去觀察 ViewModel 數據的變化,當 View 層被殺死,那最多也就是不再去觀察 ViewModel 了,ViewModel 對此也不在意,所以就會減少特別多的 NPE 和內存泄漏。

在 AAC 架構中,觀察者模式是通過 LiveData 這個類配合完成的。

LiveData

LiveData is an observable data holder class. Unlike a regular observable, LiveData is lifecycle-aware, meaning it respects the lifecycle of other app components, such as activities, fragments, or services. This awareness ensures LiveData only updates app component observers that are in an active lifecycle state.

從官方文檔的描述中,可以總結出被 LiveData 包裝的數據,有這樣的特性:

  • 生命週期感知,LiveData 僅僅會在你 UI 組件處於 active 狀態時,notify 數據,這也爲我們避免了很多 NPE 和內存泄漏
  • 數據自動更新,當 UI 組件從 inactive 到 active 狀態時,假設在此期間有數據更新,LiveData 會將最新的數據 notify 給 UI 組件,比如 App 回到 Home 頁再切換回來。
  • 自動清除引用,當 UI 組件調用 onDestory 時,LiveData 會清除 LifeCycle 相關的引用和註冊

可以來看一下 ViewModel 配合 LiveData 應該是怎樣的一個效果:

public class NameViewModel extends ViewModel {

// Create a LiveData with a String
private MutableLiveData<String> mCurrentName;

    public MutableLiveData<String> getCurrentName() {
        if (mCurrentName == null) {
            mCurrentName = new MutableLiveData<String>();
        }
        return mCurrentName;
    }

// Rest of the ViewModel...
}
public class NameActivity extends AppCompatActivity {

    private NameViewModel mModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Other code to setup the activity...

        // Get the ViewModel.
        mModel = ViewModelProviders.of(this).get(NameViewModel.class);


        // Create the observer which updates the UI.
        final Observer<String> nameObserver = new Observer<String>() {
            @Override
            public void onChanged(@Nullable final String newName) {
                // Update the UI, in this case, a TextView.
                mNameTextView.setText(newName);
            }
        };

        // Observe the LiveData, passing in this activity as the LifecycleOwner and the observer.
        mModel.getCurrentName().observe(this, nameObserver);
    }
}

mCurrentName 被 LiveData 包裹,這個時候就賦予了 mCurrentName 被觀察的能力。當數據發生更改時,如果 NameActivity 是 active 狀態,則會回調 onChanged 方法。
自然,我也可以主動地去更改 LiveData 中的數據的值:

mButton.setOnClickListener(new OnClickListener() {
    @Override
    public void onClick(View v) {
        String anotherName = "John Doe";
        mModel.getCurrentName().setValue(anotherName);
    }
});

除此以外,還有 postValue 方法,將值直接拋到 UI 線程。setSource 方法,綁定觀察數據源

上文中,提到了很多次生命週期的監測,其實是與 LifeCycle 這個組件相關的,LifeCycle 這個組件就是典型的爲了減少模板代碼而生的。同理如果你是在開發邏輯中,寫了很多模板型的代碼,這個時候你就要考慮從架構中優化,減少這些不必要的模板代碼。關於 Lifecycle,其實可說的點並不是很多,有興趣可以去官方文檔查看。

到目前爲止,我的 UI 層和 ViewModel 通過 LiveData 建立了可靠的觀察者模式:
View - ViewModel

當發生了 View 層的銷燬,會有怎樣的結果呢?
View 層銷燬

這就是觀察者模式的優勢所在,View 層銷燬,對 ViewModel 來說只是少了一個觀察者而已,我不持有你的對象,我肯定不會內存泄漏,也不會有什麼 NPE。現在看起來 View 層和 ViewModel 層的通信方式已經升級爲觀察者模式了,那在另一側,ViewModel 層和 Repository 也應該有觀察者模式這種高級通信方式吧。

@Dao
public interface ProductDao {
    @Query("SELECT * FROM products")
    LiveData<List<ProductEntity>> loadAllProducts(); 
}

可以看到我們從 Room 數據庫取出的數據直接拿 LiveData 包裝後,作爲返回值拋出。

public class DataRepository {

private MediatorLiveData<List<ProductEntity>> mObservableProducts;

    private DataRepository(final AppDatabase database) {
        mDatabase = database;
        mObservableProducts = new MediatorLiveData<>();

        mObservableProducts.addSource(mDatabase.productDao().loadAllProducts(),
                productEntities -> {
                    if (mDatabase.getDatabaseCreated().getValue() != null) {
                        mObservableProducts.postValue(productEntities);
                    }
                });
    }

    /**
     * Get the list of products from the database and get notified when the data changes.
     */
    public LiveData<List<ProductEntity>> getProducts() {
        return mObservableProducts;
    }

}

DataRepository 從數據庫中取出數據,構建成 MediatorLiveData<List<ProductEntity>> mObservableProducts 對象,通過 getProducts 方法對 ViewModel 暴露數據。

public class ProductListViewModel extends AndroidViewModel {

    // MediatorLiveData can observe other LiveData objects and react on their emissions.
    private final MediatorLiveData<List<ProductEntity>> mObservableProducts;

    public ProductListViewModel(Application application) {
        super(application);

        mObservableProducts = new MediatorLiveData<>();
        // set by default null, until we get data from the database.
        mObservableProducts.setValue(null);

        LiveData<List<ProductEntity>> products = ((BasicApp) application).getRepository()
                .getProducts();

        // observe the changes of the products from the database and forward them
        mObservableProducts.addSource(products, mObservableProducts::setValue);
    }

    /**
     * Expose the LiveData Products query so the UI can observe it.
     */
    public LiveData<List<ProductEntity>> getProducts() {
        return mObservableProducts;
    }

}

通過代碼可以看到,通過一系列 LiveData 對象,將 ViewModel 和 Repository 也建立了觀察者模式:
ViewModel - Repository

作爲開發者,我只需要在 ViewModel 調用 onCleared 方法時,去掉 Repository 的引用就好了。

其他

1.ViewModel 是 onSaveInstanceState 方法的替代方案嗎?

ViewModel 的數據也是僅僅在內存層面持有的,官方文檔也反覆提到過,ViewModel 也僅僅是在配置更改 UI 層重建時,可以存活的。當 App 由於系統內存不足被殺死時,ViewModel 也會銷燬,這個時候就需要藉助 onSaveInstanceState 來保存一些輕量的數據。所以數據持久化這塊,ViewModel 解決了配置更改重啓的問題,但是除此之外的,依然要依靠原有的方案。

2.ViewModel 是 Fragment.setRetainInstance(true) 的替代方案嗎?

Fragment.setRetainInstance(true) 這個方法的目的就是在配置更改時,保留 Fragment 的實例,所以 ViewModel 完全可以成爲這個方法的替代方案。

3.看起來 RxJava 一樣可以完成 LiveData 所做的事

是的,如果你 RxJava 玩得好,一樣能做出和 LiveData 一樣的效果,官網也明確表明,如果你執意使用 RxJava 替代 LiveData,請保證你能處理 UI 的生命週期,使你的 data streams 配合 UI 組件狀態正確地 pause destory。(何必爲難自己呢,用 LiveData 多好),Google 也出了一個類可以將二者配合使用 LiveDataReactiveStreams

總結

所以完整的 AAC 架構圖應爲:
AAC

要想打造一個能處理繁重業務量的架構是很不容易的,我也準備在公司項目中,使用 AAC 重構一個模塊,有任何心得感受或者採坑了,會更新到這個文章中。

參考資料:
https://developer.android.com/topic/libraries/architecture/
https://github.com/googlesamples/android-architecture-components

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章