Android Architecture Components是谷歌在Google I/O 2017发布一套帮助开发者解决Android架构设计的方案。
里面包含了两大块内容:
A new collection of libraries that help you design robust, testable, and maintainable apps.
即这是一个帮助构建稳定,易于测试和易于维护的App架构的库。
ViewModel
ViewModel是为特定的UI(例如Activity/Fragment)提供数据的类,同时它也承担和数据相关的业务逻辑处理功能:比如根据uid请求网络获取完整的用户信息,或者将用户修改的头像上传到服务器。因为ViewModel是独立于View(例如Activity/Fragment)这一层的,所以并不会被View层的事件影响,比如Activity被回收或者屏幕旋转等并不会造成ViewModel的改变。
LiveData
LiveData是一个包含可以被观察的数据载体。这么说又有点不好理解了,其实他就是基于观察者模式去做的。当LiveData的数据发生变化时,所有对这个LiveData变化感兴趣的类都会收到变化的更新。并且他们之间并没有类似于接口回调这样的明确的依赖关系。LiveData还能够感知组件(例如activities, fragments, services)的生命周期,防止内存泄漏。其实这和RxJava非常相似,但是LiveData能够在组件生命周期结束后自动阻断数据流的传播,防止产生空指针等意外。这个RxJava是不同的。
用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 getUser(@Path("user") String userId);
}
Repository
Repository类的作用就是获取并提供各种来源的数据(数据库中的数据,网络数据,缓存数据等),并且在来源数据更新时通知数据的获取方。
public class UserRepository {
private Webservice webservice;
// ...
public LiveData getUser(int userId) {
// This is not an optimal implementation, we'll fix it below(这不是最好的实现,以后会修复的)
final MutableLiveData data = new MutableLiveData<>();
webservice.getUser(userId).enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) {
// error case is left out for brevity(为了简单起见,简化了错误处理的情况)
data.setValue(response.body());
}
});
return data;
}
}
public class UserProfileViewModel extends ViewModel {
private LiveData 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 getUser() {
return this.user;
}
}
@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 getUser(String userId) {
LiveData cached = userCache.get(userId);
if (cached != null) {
return cached;
}
final MutableLiveData 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() {
@Override
public void onResponse(Call call, Response response) {
data.setValue(response.body());
}
});
return data;
}
}
Room
Room是一个对象映射库,(姑且按GreenDAO的功能来理解吧)可以在在编译的时候就能检测出SQL语句的异常。还能够在数据库内容发生改变时通过LiveData的形式发出通知
1. 定义User
类,并且使用@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 {
}
3.创建DAO
@Dao
public interface UserDao {
@Insert(onConflict = REPLACE)
void save(User user);
@Query("SELECT * FROM user WHERE id = :userId")
LiveData 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 getUser(String userId) {
refreshUser(userId);
// return a LiveData directly from the database.(从数据库中直接返回LiveData)
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());
}
});
}
}
数据来源的唯一性
在上面提供的代码中,数据库是App数据的唯一来源。Google推荐采用这种方式。
下面这张图展示了使用Android Architecture Components来构建的App整体的架构:
一些App架构设计的推荐准则
UserRepository
的类)
Lifecycle
Lifecycle是一个包含组件(Activity或者Fragment)生命周期状态的类,这个类还能够为其他类提供当前的生命周期。
Lifecycle使用两个主要的枚举来跟踪他所关联组件的生命周期。
LifecycleOwner
实现LifecycleOwner就表示这是个有生命周期的类,他有一个getLifecycle ()方法是必须实现的。com.android.support:appcompat-v7:26.1.0
中的AppCompatActivity已经实现了这个接口,详细的实现可以自行查看代码。
对于前面提到的监听位置的例子。可以把MyLocationListener
实现LifecycleObserver,然后在Lifecycle
(Activity/Fragment)的onCreate
方法中初始化。这样MyLocationListener
就能自行处理生命周期带来的问题。
class MyActivity extends AppCompatActivity {
private MyLocationListener myLocationListener;
public void onCreate(...) {
myLocationListener = new MyLocationListener(this, getLifecycle(), location -> {
// update UI
});
Util.checkUserStatus(result -> {
if (result) {
myLocationListener.enable();
}
});
}
}
class MyLocationListener implements LifecycleObserver {
private boolean enabled = false;
public MyLocationListener(Context context, Lifecycle lifecycle, Callback callback) {
...
}
@OnLifecycleEvent(Lifecycle.Event.ON_START)
void start() {
if (enabled) {
// connect
}
}
public void enable() {
enabled = true;
if (lifecycle.getState().isAtLeast(STARTED)) {
// connect if not connected
}
}
@OnLifecycleEvent(Lifecycle.Event.ON_STOP)
void stop() {
// disconnect if connected
}
}
LiveData
LiveData是一种持有可被观察数据的类。和其他可被观察的类不同的是,LiveData是有生命周期感知能力的,这意味着它可以在activities, fragments, 或者 services生命周期是活跃状态时更新这些组件。
要想使用LiveData(或者这种有可被观察数据能力的类)就必须配合实现了LifecycleOwner的对象使用。在这种情况下,当对应的生命周期对象DESTORY时,才能移除观察者。这对Activity或者Fragment来说显得尤为重要,因为他们可以在生命周期结束的时候立刻解除对数据的订阅,从而避免内存泄漏等问题。
LiveData是一个数据的包装。具体的包装对象可以是任何数据,包括集合(比如List)。LiveData通常在ViewModel中创建,然后通过gatter方法获取。具体可以看一下代码:
public class NameViewModel extends ViewModel {
// Create a LiveData with a String 暂时就把MutableLiveData看成是LiveData吧,下面的文章有详细的解释
private MutableLiveData mCurrentName;
public MutableLiveData getCurrentName() {
if (mCurrentName == null) {
mCurrentName = new MutableLiveData();
}
return mCurrentName;
}
// Rest of the ViewModel...
}
通常情况下都是在组件的onCreate()方法中开始观察数据,原因有以下两点:
下面的代码展示了如何观察LiveData对象:
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 nameObserver = new Observer() {
@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);
}
}
如果想要在UI Controller中改变LiveData
中的值呢?(比如点击某个Button把性别从男设置成女)。LiveData
并没有提供这样的功能,但是Architecture Component提供了MutableLiveData这样一个类,可以通过setValue(T)
和postValue(T)
方法来修改存储在LiveData中的数据。MutableLiveData是LiveData
的一个子类,从名称上也能看出这个类的作用。举个直观点的例子:
mButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
String anotherName = "John Doe";
mModel.getCurrentName().setValue(anotherName);
}
});
调用setValue()
方法就可以把LiveData
中的值改为John Doe
。同样,通过这种方法修改LiveData
中的值同样会触发所有对这个数据感兴趣的类。那么setValue()
和postValue()
有什么不同呢?区别就是setValue()
只能在主线程中调用,而postValue()
可以在子线程中调用。
Room和LiveData配合使用
Room可以返回LiveData的数据类型。这样对数据库中的任何改动都会被传递出去。这样修改完数据库就能获取最新的数据,减少了主动获取数据的代码。
public LiveData getUser(String userId) {
refreshUser(userId);
// return a LiveData directly from the database.(从数据库中直接返回LiveData)
return userDao.load(userId);
}
LiveData的活跃状态包括:STARTED或者RESUMED两种状态。那么如何在活跃状态下把数据传递出去呢?
public class StockLiveData extends LiveData {
private StockManager mStockManager;
private SimplePriceListener mListener = new SimplePriceListener() {
@Override
public void onPriceChanged(BigDecimal price) {
setValue(price);
}
};
public StockLiveData(String symbol) {
mStockManager = new StockManager(symbol);
}
@Override
protected void onActive() {
mStockManager.requestPriceUpdates(mListener);
}
@Override
protected void onInactive() {
mStockManager.removeUpdates(mListener);
}
}
可以看到onActive()
和onInactive()
就表示了处于活跃和不活跃状态的回调。
public class MyFragment extends Fragment {
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
LiveData myPriceListener = ...;
myPriceListener.observe(this, price -> {
// Update the UI.
});
}
}
如果把StockLiveData
写成单例模式,那么还可以在不同的组件间共享数据。
public class StockLiveData extends LiveData {
private static StockLiveData sInstance;
private StockManager mStockManager;
private SimplePriceListener mListener = new SimplePriceListener() {
@Override
public void onPriceChanged(BigDecimal price) {
setValue(price);
}
};
@MainThread
public static StockLiveData get(String symbol) {
if (sInstance == null) {
sInstance = new StockLiveData(symbol);
}
return sInstance;
}
private StockLiveData(String symbol) {
mStockManager = new StockManager(symbol);
}
@Override
protected void onActive() {
mStockManager.requestPriceUpdates(mListener);
}
@Override
protected void onInactive() {
mStockManager.removeUpdates(mListener);
}
}
这么说很容易和上文改变LiveData中的值搞混。这里的变换是指在LiveData的数据被分发到各个组件之前转换值的内容,各个组件收到的是转换后的值,但是LiveData里面数据本身的值并没有改变。(和RXJava中map的概念很像)Lifecycle包中提供了Transformations来提供转换的功能。
Transformations.map()
LiveData userLiveData = ...;
LiveData userName = Transformations.map(userLiveData, user -> {
user.name + " " + user.lastName
});
把原来是包含User的LiveData转换成包含String的LiveData传递出去。
Transformations.switchMap()
private LiveData getUser(String id) {
...;
}
LiveData userId = ...;
LiveData user = Transformations.switchMap(userId, id -> getUser(id) );
和上面的map()
方法很像。区别在于传递给switchMap()
的函数必须返回LiveData对象。
和LiveData一样,Transformation
也可以在观察者的整个生命周期中存在。只有在观察者处于观察LiveData状态时,Transformation
才会运算。Transformation
是延迟运算的(calculated lazily),而生命周期感知的能力确保不会因为延迟发生任何问题。
如果在ViewModel
对象的内部需要一个Lifecycle
对象,那么使用Transformation
是一个不错的方法。举个例子:假如有个UI组件接受输入的地址,返回对应的邮政编码。那么可以 实现一个ViewModel
和这个组件绑定:
class MyViewModel extends ViewModel {
private final PostalCodeRepository repository;
public MyViewModel(PostalCodeRepository repository) {
this.repository = repository;
}
private LiveData getPostalCode(String address) {
// DON'T DO THIS (不要这么干)
return repository.getPostCode(address);
}
}
看代码中的注释,有个// DON'T DO THIS (不要这么干)
,这是为什么?有一种情况是如果UI组件被回收后又被重新创建,那么又会触发一次 repository.getPostCode(address)
查询,而不是重用上次已经获取到的查询。那么应该怎样避免这个问题呢?看一下下面的代码:
class MyViewModel extends ViewModel {
private final PostalCodeRepository repository;
private final MutableLiveData addressInput = new MutableLiveData();
public final LiveData postalCode =
Transformations.switchMap(addressInput, (address) -> {
return repository.getPostCode(address);
});
public MyViewModel(PostalCodeRepository repository) {
this.repository = repository
}
private void setInput(String address) {
addressInput.setValue(address);
}
}
MediatorLiveData是LiveData的子类,可以通过MediatorLiveData
合并多个LiveData来源的数据。同样任意一个来源的LiveData数据发生变化,MediatorLiveData
都会通知观察他的对象。说的有点抽象,举个例子。比如UI接收来自本地数据库和网络数据,并更新相应的UI。可以把下面两个LiveData加入到MeidatorLiveData中:
相应的UI只需要关注MediatorLiveData就可以在任意数据来源更新时收到通知。
ViewModel
ViewModel设计的目的就是存放和处理和UI相关的数据,并且这些数据不受配置变化(Configuration Changes,例如:旋转屏幕,组件被系统回收)的影响。
ViewModel用于为UI组件提供数据,并且能够在旋转屏幕等Configuration Change发生时,仍能保持里面的数据。当UI组件恢复时,可以立刻向UI提供数据。
public class MyViewModel extends ViewModel {
private MutableLiveData> users;
public LiveData> getUsers() {
if (users == null) {
users = new MutableLiveData>();
loadUsers();
}
return users;
}
private void loadUsers() {
// do async operation to fetch users
}
}
Activity访问User List数据:
public class MyActivity extends AppCompatActivity {
public void onCreate(Bundle savedInstanceState) {
MyViewModel model = ViewModelProviders.of(this).get(MyViewModel.class);
model.getUsers().observe(this, users -> {
// update UI
});
}
}
假如用户按返回键,主动销毁了这个Activity呢?这时系统会调用ViewModel的onCleared()
方法,清除ViewModel中的数据。
使用ViewModel可以很好的解决这个问题。假设有这样两个Fragment,一个Fragment提供一个列表,另一个Fragment提供点击每个item现实的详细信息。
public class SharedViewModel extends ViewModel {
private final MutableLiveData- selected = new MutableLiveData
- ();
public void select(Item item) {
selected.setValue(item);
}
public LiveData
- getSelected() {
return selected;
}
}
public class MasterFragment extends Fragment {
private SharedViewModel model;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
itemSelector.setOnClickListener(item -> {
model.select(item);
});
}
}
public class DetailFragment extends LifecycleFragment {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
model.getSelected().observe(this, { item ->
// update UI
});
}
}
两个Fragment都是通过getActivity()
来获取ViewModelProvider。这意味着两个Activity都是获取的属于同一个Activity的同一个ShareViewModel
实例。
这样做优点如下:
SharedViewModel
以外其他的代码。这两个Fragment不需要知道对方是否存在。ViewModel只有在Activity finish或者Fragment detach之后才会销毁。
Room在SQLite上提供了一个方便访问的抽象层。App把经常需要访问的数据存储在本地将会大大改善用户的体验。这样用户在网络不好时仍然可以浏览内容。当用户网络可用时,可以更新用户的数据。
使用原始的SQLite可以提供这样的功能,但是有以下两个缺点:
Room包含以下三个重要组成部分:
Database 创建数据库。
Entities 数据库表中对应的Java对象
DAOs 访问数据库
User.java
@Entity
public class User {
@PrimaryKey
private int uid;
@ColumnInfo(name = "first_name")
private String firstName;
@ColumnInfo(name = "last_name")
private String lastName;
// Getters and setters are ignored for brevity,
// but they're required for Room to work.
//Getters和setters为了简单起见就省略了,但是对Room来说是必须的
}
UserDao.java
@Dao
public interface UserDao {
@Query("SELECT * FROM user")
List getAll();
@Query("SELECT * FROM user WHERE uid IN (:userIds)")
List loadAllByIds(int[] userIds);
@Query("SELECT * FROM user WHERE first_name LIKE :first AND "
+ "last_name LIKE :last LIMIT 1")
User findByName(String first, String last);
@Insert
void insertAll(User... users);
@Delete
void delete(User user);
}
AppDatabase.java
@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}
在创建了上面三个文件后,就可以通过如下代码创建数据库了:
AppDatabase db = Room.databaseBuilder(getApplicationContext(),
AppDatabase.class, "database-name").build();
@Entity
如果上面的User类中包含一个字段是不希望存放到数据库中的,那么可以用@Ignore注解这个字段:
@Entity
class User {
@PrimaryKey
public int id;
public String firstName;
public String lastName;
//不需要被存放到数据库中
@Ignore
Bitmap picture;
}
Room持久化一个类的field必须要求这个field是可以访问的。可以把这个field设为public或者设置setter和getter。
每个Entity都必须定义一个field为主键,即使是这个Entity只有一个field。如果想要Room生成自动的primary key,可以使用@PrimaryKey
的autoGenerate属性。如果Entity的primary key是多个Field的复合Key,可以向下面这样设置:
@Entity(primaryKeys = {"firstName", "lastName"})
class User {
public String firstName;
public String lastName;
@Ignore
Bitmap picture;
}
在默认情况下Room使用类名作为数据库表的名称。如果想要设置不同的名称,可以参考下面的代码,设置表名tableName为users
:
@Entity(tableName = "users")
class User {
...
}
和设置tableName相似,Room默认使用field的名称作为表的列名。如果想要使用不同的名称,可以通过@ColumnInfo(name = "first_name")
设置,代码如下:
@Entity(tableName = "users")
class User {
@PrimaryKey
public int id;
@ColumnInfo(name = "first_name")
public String firstName;
@ColumnInfo(name = "last_name")
public String lastName;
@Ignore
Bitmap picture;
}
和设置tableName相似,Room默认使用field的名称作为表的列名。如果想要使用不同的名称,可以通过@ColumnInfo(name = "first_name")
设置,代码如下:
@Entity(tableName = "users")
class User {
@PrimaryKey
public int id;
@ColumnInfo(name = "first_name")
public String firstName;
@ColumnInfo(name = "last_name")
public String lastName;
@Ignore
Bitmap picture;
}
外键
例如,有一个Pet
类需要和User
类建立关系,可以通过@ForeignKey
来达到这个目的,代码如下:
@Entity(foreignKeys = @ForeignKey(entity = User.class,
parentColumns = "id",
childColumns = "user_id"))
class Pet {
@PrimaryKey
public int petId;
public String name;
@ColumnInfo(name = "user_id")
public int userId;
}
class Address {
public String street;
public String state;
public String city;
@ColumnInfo(name = "post_code")
public int postCode;
}
@Entity
class User {
@PrimaryKey
public int id;
public String firstName;
@Embedded
public Address address;
}
DAOs是数据库访问的抽象层。Dao
可以是一个接口也可以是一个抽象类。如果是抽象类,那么它可以接受一个RoomDatabase
作为构造器的唯一参数。
Room不允许在主线程中访问数据库,除非在builder里面调用allowMainThreadQueries() 。因为访问数据库是耗时的,可能阻塞主线程,引起UI卡顿。
Insert
使用 @Insert注解的方法,Room将会生成插入的代码。
@Dao
public interface MyDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
public void insertUsers(User... users);
@Insert
public void insertBothUsers(User user1, User user2);
@Insert
public void insertUsersAndFriends(User user, List friends);
}
如果@Insert 方法只接受一个参数,那么将返回一个long,对应着插入的rowId
。如果接受多个参数,或者数组,或者集合,那么就会返回一个long的数组或者list。
Update
@Dao
public interface MyDao {
@Update
public void updateUsers(User... users);
}
也可以让update方法返回一个int型的整数,代表被update的行号。
@Dao
public interface MyDao {
@Delete
public void deleteUsers(User... users);
}
Paging Library(分页加载库)用于逐步从数据源加载信息,而不会耗费过多的设备资源或者等待太长的时间。
一个常见的需求是获取很多数据,但是同时也只展示其中的一小部分数据。这就会引起很多问题,比如浪费资源和流量等。
现有的Android APIs可以提供分页加载的功能,但是也带来了显著的限制和缺点:
DataSource
数据源。根据你想要访问数据的方式,可以有两种子类可供选择:
例如使用 Room persistence library 就可以自动创建返回 TiledDataSource类型的数据:
@Query("select * from users WHERE age > :age order by name DESC, id ASC")
TiledDataSource usersOlderThan(int age);
@Dao
interface UserDao {
@Query("SELECT * FROM user ORDER BY lastName ASC")
public abstract LivePagedListProvider usersByLastName();
}
class MyViewModel extends ViewModel {
public final LiveData> usersList;
public MyViewModel(UserDao userDao) {
usersList = userDao.usersByLastName().create(
/* initial load position */ 0,
new PagedList.Config.Builder()
.setPageSize(50)
.setPrefetchDistance(50)
.build());
}
}
class MyActivity extends AppCompatActivity {
@Override
public void onCreate(Bundle savedState) {
super.onCreate(savedState);
MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
RecyclerView recyclerView = findViewById(R.id.user_list);
UserAdapter adapter = new UserAdapter();
viewModel.usersList.observe(this, pagedList -> adapter.setList(pagedList));
recyclerView.setAdapter(adapter);
}
}
class UserAdapter extends PagedListAdapter {
public UserAdapter() {
super(DIFF_CALLBACK);
}
@Override
public void onBindViewHolder(UserViewHolder holder, int position) {
User user = getItem(position);
if (user != null) {
holder.bindTo(user);
} else {
// Null defines a placeholder item - PagedListAdapter will automatically invalidate
// this row when the actual object is loaded from the database
holder.clear();
}
}
public static final DiffCallback DIFF_CALLBACK = new DiffCallback() {
@Override
public boolean areItemsTheSame(@NonNull User oldUser, @NonNull User newUser) {
// User properties may have changed if reloaded from the DB, but ID is fixed
return oldUser.getId() == newUser.getId();
}
@Override
public boolean areContentsTheSame(@NonNull User oldUser, @NonNull User newUser) {
// NOTE: if you use equals, your object must properly override Object#equals()
// Incorrectly returning false here will result in too many animations.
return oldUser.equals(newUser);
}
}
}