天天看點

App 元件化/子產品化之路——Android 架構元件(Android Architecture Components)使用指南

面對越來越複雜的 App 需求,Google 官方釋出了Android 架構元件庫(Android Architecture Components )。為開發者更好的開發 App 提供了非常好的樣本。這個架構裡的元件是配合 Android 元件生命周期的,是以它能夠很好的規避元件生命周期管理的問題。今天我們就來看看這個庫的使用。

通用的架構準則

官方建議在架構 App 的時候遵循以下兩個準則:

  1. 關注分離

    其中早期開發 App 最常見的做法是在 Activity 或者 Fragment 中寫了大量的邏輯代碼,導緻 Activity 或 Fragment 中的代碼很臃腫,十分不易維護。現在很多 App 開發者都注意到了這個問題,是以前兩年 MVP 結構就非常有市場,目前普及率也很高。

  2. 模型驅動UI

    模型持久化的好處就是:即使系統回收了 App 的資源使用者也不會丢失資料,而且在網絡不穩定的情況下 App 依然可以正常地運作。進而保證了 App 的使用者體驗。

App 架構元件

架構提供了以下幾個核心元件,我們将通過一個執行個體來說明這幾個元件的使用。

  • ViewModel
  • LiveData
  • Room

假設要實作一個使用者資訊展示頁面。這個使用者資訊是通過REST API 從背景擷取的。

建立UI

我們使用 fragment (UserProfileFragment.java) 來實作使用者資訊的展示頁面。為了驅動 UI,我們的資料模型需要持有以下兩個資料元素

  • 使用者ID: 使用者的唯一辨別。可以通過 fragment 的 arguments 參數進行傳遞這個資訊。這樣做的好處就是如果系統銷毀了應用,這個參數會被儲存并且下次重新啟動時可以恢複之前的資料。
  • 使用者對象資料:POJO 持有使用者資料。

我們要建立 ViewModel 對象用于儲存以上資料。

那什麼是 ViewModel 呢?

A ViewModel provides the data for a specific UI component, such as a fragment or activity, and handles the communication with the business part of data handling, such as calling other components to load the data or forwarding user modifications. The ViewModel does not know about the View and is not affected by configuration changes such as recreating an activity due to rotation.

ViewModel 是一個架構元件。它為 UI 元件 (fragment或activity) 提供資料,并且可以調用其它元件加載資料或者轉發使用者指令。ViewModel 不會關心 UI 長什麼樣,也不會受到 UI 元件配置改變的影響,例如不會受旋轉螢幕後 activity 重新啟動的影響。是以它是一個與 UI 元件無關的。

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

需要的是:由于架構元件目前還處于預覽版本,這裡

UserProfileFragment

是繼承于

LifecycleFragment

而不是

Fragment

。待正式釋出版本之後 Android Support 包中的

Fragment

就會預設實作

LifecycleOwner

接口。而

LifecycleFragment

也是實作了

LifecycleOwner

接口的。即正式版本釋出時 Support 包中的 UI 元件類就是支援架構元件的。

現在已經有了 UI 元件和 ViewModel,那麼我們如何将它們進行連接配接呢?這時候就需要用到 LiveData 元件了。

LiveData is an observable data holder. It lets the components in your app observe

LiveData

objects for changes without creating explicit and rigid dependency paths between them. LiveData also respects the lifecycle state of your app components (activities, fragments, services) and does the right thing to prevent object leaking so that your app does not consume more memory.

LiveData 的使用有點像 RxJava。是以完全可以使用 RxJava 來替代 LiveData 元件。

現在我們修改一下

UserProfileViewModel

public class UserProfileViewModel extends ViewModel {
    ...
    private LiveData<User> user;
    public LiveData<User> getUser() {
        return user;
    }
}      

Useruser

替換成

LiveData<User>user

然後再修改

UserProfileFragment

類中

@Override
public void onActivityCreated(@Nullable Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    viewModel.getUser().observe(this, user -> {
      // update UI
    });
}      

當使用者資料發生改變時,就會通知 UI 進行更新。ViewModel 與 UI 元件的互動就是這麼簡單。

但細心的朋友可能發現了:fragment 在

onActivityCreated

方法中添加了相應的監聽,但是沒有在其它對應的生命周期中移除監聽。有經驗的朋友就會覺得這是不是有可能會發生引用洩露問題呢?其實不然,LiveData 元件内部已經為開發者做了這些事情。即 LiveData 會再正确的生命周期進行回調。

擷取資料

現在已經成功的把 ViewModel 與 UI 元件(fragment)進行了通信。那麼 ViewModel 又是如何擷取資料的呢?

假設我們的資料是通過REST API 從後天擷取的。我們使用

Retrofit

庫實作網絡請求。

以下是請求網絡接口

Webservice

public interface Webservice {
    /**
     * @GET declares an HTTP GET request
     * @Path("user") annotation on the userId parameter marks it as a
     * replacement for the {user} placeholder in the @GET path
     */
    @GET("/users/{user}")
    Call<User> getUser(@Path("user") String userId);
}      

ViewModel 可以引用

Webservice

接口,但是這樣做違背了我們在上文提到的關注分離準則。因為我們推薦使用

Repository

模型對

Webservice

進行封裝。

Repository modules are responsible for handling data operations. They provide a clean API to the rest of the app. They know where to get the data from and what API calls to make when data is updated. You can consider them as mediators between different data sources (persistent model, web service, cache, etc.).

關于 Repository 模式可以參考我的上一篇《App 元件化/子產品化之路——Repository模式》

以下是使用 Repository 封裝

WebService

public class UserRepository {
    private Webservice webservice;
    // ...
    public LiveData<User> getUser(int userId) {
        // This is not an optimal implementation, we'll fix it below
        final MutableLiveData<User> data = new MutableLiveData<>();
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                // error case is left out for brevity
                data.setValue(response.body());
            }
        });
        return data;
    }
}      

使用 Respository 模式抽象資料源接口,也可以很友善地替換其它資料。這樣 ViewModel 也不用知道資料源到底是來自哪裡。

元件間的依賴管理

從上文我們知道

UserRepository

類需要有一個

WebService

執行個體才能工作。我們可以直接建立它,但這麼做我們就必須知道它的依賴,而且會由很多重複的建立對象的代碼。這時候我們可以使用依賴注入。本例中我們将使用 Dagger 2 來管理依賴。

連接配接 ViewModel 和 Repository

修改

UserProfileViewModel

類,引用 Repository 并且通過 Dagger 2 對 Repository 的依賴進行管理。

public class UserProfileViewModel extends ViewModel {
    private LiveData<User> user;
    private UserRepository userRepo;

    @Inject // UserRepository parameter is provided by Dagger 2
    public UserProfileViewModel(UserRepository userRepo) {
        this.userRepo = userRepo;
    }

    public void init(String userId) {
        if (this.user != null) {
            // ViewModel is created per Fragment so
            // we know the userId won't change
            return;
        }
        user = userRepo.getUser(userId);
    }
    public LiveData<User> getUser() {
        return this.user;
    }
}      

緩存資料

前面我們實作的 Repository 是隻有一個網絡資料源的。這樣做每次進入使用者資訊頁面都需要去查詢網絡,使用者需要等待,體驗不好。是以在 Repository 中加一個緩存資料。

@Singleton  // informs Dagger that this class should be constructed once
public class UserRepository {
    private Webservice webservice;
    // simple in memory cache, details omitted for brevity
    private UserCache userCache;
    public LiveData<User> getUser(String userId) {
        LiveData<User> cached = userCache.get(userId);
        if (cached != null) {
            return cached;
        }

        final MutableLiveData<User> data = new MutableLiveData<>();
        userCache.put(userId, data);
        // this is still suboptimal but better than before.
        // a complete implementation must also handle the error cases.
        webservice.getUser(userId).enqueue(new Callback<User>() {
            @Override
            public void onResponse(Call<User> call, Response<User> response) {
                data.setValue(response.body());
            }
        });
        return data;
    }
}      

持久化資料 (Room 元件)

Android 架構提供了 Room 元件,為 App 資料持久化提供了解決方案。

Room is an object mapping library that provides local data persistence with minimal boilerplate code. At compile time, it validates each query against the schema, so that broken SQL queries result in compile time errors instead of runtime failures. Room abstracts away some of the underlying implementation details of working with raw SQL tables and queries. It also allows observing changes to the database data (including collections and join queries), exposing such changes via LiveData objects. In addition, it explicitly defines thread constraints that address common issues such as accessing storage on the main thread.

Room 元件提供了資料庫操作,配合 LiveData 使用可以監聽資料庫的變化,進而更新 UI 元件。

要使用 Room 元件,需要以下步驟:

  • 使用注解 

    @Entity

     定義實體
  • 建立 

    RoomDatabase

     子類
  • 建立資料通路接口(DAO)
  • 在 

    RoomDatabase

     中引用 DAO

  1. 用注解 

@Entity

 定義實體類

@Entity
class User {
  @PrimaryKey
  private int id;
  private String name;
  private String lastName;
  // getters and setters for fields
}      

  2. 建立 

RoomDatabase

子類

@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
}      

需要注意的是

MyDatabase

是抽象類,Room 元件為我們提供具體的實作。

  3. 建立 DAO

@Dao
public interface UserDao {
    @Insert(onConflict = REPLACE)
    void save(User user);
    @Query("SELECT * FROM user WHERE id = :userId")
    LiveData<User> load(String userId);
}      

  4. 在 

RoomDatabase

@Database(entities = {User.class}, version = 1)
public abstract class MyDatabase extends RoomDatabase {
    public abstract UserDao userDao();
}      

現在有了 Room 元件,那麼我們可以修改

UserRepository

@Singleton
public class UserRepository {
    private final Webservice webservice;
    private final UserDao userDao;
    private final Executor executor;

    @Inject
    public UserRepository(Webservice webservice, UserDao userDao, Executor executor) {
        this.webservice = webservice;
        this.userDao = userDao;
        this.executor = executor;
    }

    public LiveData<User> getUser(String userId) {
        refreshUser(userId);
        // return a LiveData directly from the database.
        return userDao.load(userId);
    }

    private void refreshUser(final String userId) {
        executor.execute(() -> {
            // running in a background thread
            // check if user was fetched recently
            boolean userExists = userDao.hasUser(FRESH_TIMEOUT);
            if (!userExists) {
                // refresh the data
                Response response = webservice.getUser(userId).execute();
                // TODO check for error etc.
                // Update the database.The LiveData will automatically refresh so
                // we don't need to do anything else here besides updating the database
                userDao.save(response.body());
            }
        });
    }
}      

 目前為止我們的代碼就基本完成了。UI 元件通過 ViewModel 通路資料,而 ViewModel 通過 LiveData 監聽資料的變化,并且使用 Repository 模式封裝資料源。這些資料源可以是網絡資料,緩存以及持久化資料。

架構結構圖

參考文檔:

https://developer.android.com/topic/libraries/architecture/guide.html#recommendedapparchitecture

https://github.com/googlesamples/android-architecture-components

微信關注我們,可以擷取更多      
App 元件化/子產品化之路——Android 架構元件(Android Architecture Components)使用指南

繼續閱讀