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

本文转载自:https://mp.weixin.qq.com/s/VLj6PMFNAuL9iMq12qUyEg

面对越来越复杂的 App 需求,Google 官方发布了Android 框架组件库(Android Architecture Components )。为开发者更好的开发 App 提供了非常好的样本。这个框架里的组件是配合 Android 组件生命周期的,所以它能够很好的规避组件生命周期管理的问题。今天我们就来看看这个库的使用。
通用的框架准则
官方建议在架构 App 的时候遵循以下两个准则:

关注分离
其中早期开发 App 最常见的做法是在 Activity 或者 Fragment 中写了大量的逻辑代码,导致 Activity 或 Fragment 中的代码很臃肿,十分不易维护。现在很多 App 开发者都注意到了这个问题,所以前两年 MVP 结构就非常有市场,目前普及率也很高。

模型驱动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 observeLiveData
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类

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 中加一个缓存数据。

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 LiveDataobjects. 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

用注解 @Entity
定义实体类

@Entity

class User {

@PrimaryKey

private int id;

private String name;

private String lastName;

// getters and setters for fields

}

创建 RoomDatabase
子类

@Database(entities = {User.class}, version = 1)

public abstract class MyDatabase extends RoomDatabase {

}

需要注意的是MyDatabase
是抽象类,Room 组件为我们提供具体的实现。
创建 DAO

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

在 RoomDatabase
中引用 DAO

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

现在有了 Room 组件,那么我们可以修改UserRepository

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

本文转载自:https://mp.weixin.qq.com/s/VLj6PMFNAuL9iMq12qUyEg

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容