1. 程式人生 > >App 組件化/模塊化之路——Android 框架組件(Android Architecture Components)使用指南

App 組件化/模塊化之路——Android 框架組件(Android Architecture Components)使用指南

them 實體 storage form 查詢 app 開發 callback 後臺 pil

面對越來越復雜的 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 中引用 DAO

@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)使用指南