最近,阅读了国外一篇关于viewmodel+livedata的文章https://proandroiddev.com/mvvm-architecture-viewmodel-and-livedata-part-1-604f50cda1 ,收益良多,纸上得来终觉浅,绝知此事要躬行,决定自己也亲手撸一个demo。
一两句话和一两个图总结:
LiveData作用
(1)实际上就是一个观察者模式的数据容器,当数据改变时,通知UI刷新;
(2)数据LiveData
(3)LiveData能感知activity, fragment组件的生命周期,当activity,fragment销毁时,会自动清除引用,不必担心内存泄漏。而其他观察者回调的库需要自己管理生命周期。
(4)当activity或者fragment处于活动状态时如started,resumed,liveData才会通知。
(5)LiveData通常和ViewModel一起使用, LiveData
ViewModel作用
(1)将activity, fragment里关于数据操作的逻辑抽离出来,封装到ViewModel中,所以ViewMoel 持有一个成员变量LiveData
(2)数据的操作包括什么呢? a. 从DB和缓存读取数据,显示到UI; b. 通过网络到后台拉取数据,持久化到本地,更新DB和缓存,通知UI刷新。
(3)因此ViewModel 应该持有一个 成员变量Repository(相当于一个管理类, 命名可以命名为其他如XXXManager),做(2)的事情。 而组件activity, fragment应该持有一个成员变量ViewModel , 如图所示
图片来源https://www.jianshu.com/p/9516a3c08a25
(4)ViewModel的生命周期如图, 可以看出 横竖屏切换,activity onDestroy后重新onCreate, ViewModel 还是原来的对象,没有被重新创建。
图片来源https://developer.android.com/topic/libraries/architecture/viewmodel
(5)ViewModel 不能持有activity, fragment的引用,否则会导致内存泄漏, ViewMode中如果要使用context,我们通常定义XXXViewModel 继承 基类AndroidViewModel就好了。
(6)ViewModel可以用于fragment之间通信。
Databinding作用
(1) 和UI双向绑定
(2) 在build.gradle 的android下 声明
dataBinding {
enabled = true
}
后, 在XML布局文件定义一个variable来引用我们的数据实体类。如:
在XML具体的控件可以直接访问数据里的字段 如
android:text="@{poetry.title}"/>
(3)Android Studio 会根据XML的文件名生成一个Binding类,如
fragment_poetry_detail.xml ----- > FragmentPoetryDetailBinding
同时也生成相应的setXXX方法, 如在xml定义的variable变量:
variable name = "poetry" ----> setPoetry()------> 完成了Poetry和UI绑定
(4) binding调用setXXX方法 实现数据同步到UI
/**
* LiveData被观察者和观察者activty ,fragment建立订阅关系
* @param viewModel
*/
private void observeViewModel(final PoetryViewModel viewModel) {
viewModel.getmPoetryObservable().observe(this, new Observer() {
@Override
public void onChanged(@Nullable Poetry poetry) {
Utils.printPoetryInfo(poetry);//打日志
if (poetry != null) {
mFragmentPoetryDetailBinding.setIsLoading(false);// binding调用setXXX方法后,数据同步到UI
mFragmentPoetryDetailBinding.setPoetry(poetry);// binding调用setXXX方法后,数据同步到UI
}
}
});
}
有些文章说实体类需要继承BaseObservable或者使用ObservableFields才能把数据同步到UI,但是实践过程中,发现不用也可以同步数据到UI,可能新版的库做了改善。
------------------------------------------------Demo实践------------------------------------------------
一. 准备
用到开源的api如下:
获取唐朝古诗词:
https://api.apiopen.top/getTangPoetry?page=1&count=20
和
搜索古诗词:
https://api.apiopen.top/searchPoetry?name=古风二首 二
来源https://blog.csdn.net/qq_26582901/article/details/84102488
二. Demo效果和Demo结构
三. 开发步骤
1. 在Build.gradle中添加依赖,用到的依赖包括lifecycle, retrofit, room等等。
注意:android.arch.lifecycle和android.arch.persistence.room的版本要一致,否则报错com.android.builder.dexing.DexArchiveMergerException: Unable to merge dex。 以下是demo的build.gradle文件
// 各个 依赖包的版本
project.ext {
appcompat = "25.3.1"
arch = "1.0.0-alpha1"
retrofit = "2.0.2"
constraintLayout = "1.0.2"
}
apply plugin: 'com.android.application'
android {
compileSdkVersion 26
defaultConfig {
applicationId "com.example.mikel.mvvmlivedataretrofitdemo"
minSdkVersion 19
targetSdkVersion 26
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
// 记录数据库的sql和迁移
javaCompileOptions {
annotationProcessorOptions {
//room的数据库概要、记录
arguments = ["room.schemaLocation":
"$projectDir/schemas".toString()]
}
}
}
sourceSets {
//数据库概要、记录存放位置
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
// 使用databinding
dataBinding {
enabled = true
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
//constraintLayout依赖
compile "com.android.support.constraint:constraint-layout:$project.constraintLayout"
//添加retrofit依赖
compile "com.squareup.retrofit2:retrofit:$project.retrofit"
compile "com.squareup.retrofit2:converter-gson:$project.retrofit"
//添加lifecycle依赖 liveData
compile "android.arch.lifecycle:runtime:$project.arch"
compile "android.arch.lifecycle:extensions:$project.arch"
annotationProcessor "android.arch.lifecycle:compiler:$project.arch"
// 添加appcompat-v7 support-v4依赖
compile "com.android.support:appcompat-v7:$project.appcompat"
compile "com.android.support:support-v4:$project.appcompat"
// 添加recyclerview依赖
compile "com.android.support:recyclerview-v7:$project.appcompat"
//添加cardview依赖
compile "com.android.support:cardview-v7:$project.appcompat"
//添加room依赖
//android.arch下的包版本要一致 否则会报错:com.android.builder.dexing.DexArchiveMergerException: Unable to merge dex
compile "android.arch.persistence.room:runtime:1.0.0-alpha1"
annotationProcessor "android.arch.persistence.room:compiler:1.0.0-alpha1"
}
2. 创建实体类Poetry 由于使用到了room框架,这里顺便讲述room框架的使用。
Portry.java:
@Entity(tableName = "poetry_table") //数据库表名poetry_table
public class Poetry {
@PrimaryKey //主键
public String title; //标题
@ColumnInfo //列
public String content; //内容
@ColumnInfo //列
public String authors; // 作者
public Poetry() {
}
@Override
public String toString() {
return "Poetry:" +
"title =" + title +
", content =" + content +
", author =" + authors;
}
}
room框架使用:
a. 实体类 Poetry 使用@Entity标注,并且指定数据库的表名 poetry_table;
title作为主键用@PrimaryKey标注;
content和authors作为数据库表的列用@ColumnInfo标注。
b. 定义DAO类,主要负责封装 增删改查 数据库的接口。
注意:Poetry接口类需要用@Dao标注 ,每一个接口使用@Query + SQL查询语句标注
PoetryDao.java:
@Dao // DAO接口
public interface PoetryDao {
@Query("SELECT * FROM poetry_table") //查询SQL语句
List getAllPoetries();
@Query("SELECT * FROM poetry_table WHERE title = :name") //条件查询
Poetry getPoetryByName(String name);
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insertAll(List poetries);
@Insert(onConflict = OnConflictStrategy.REPLACE)
void insert(Poetry poetry);
}
c. 定义继承RoomDatabase的AppDatabase,负责初始化数据库对象。
注意:当后续要更改数据库字段的时候,需要实现方法migrate()
AppDatabase.java:
@Database(entities = {Poetry.class}, version = 1) // 声明版本号1
public abstract class AppDatabase extends RoomDatabase {
private static AppDatabase INSTANCE;
public abstract PoetryDao poetryDao();
public static AppDatabase getInstance(Context context) {
if(INSTANCE == null) {//单例设计模式 双重校验
synchronized (AppDatabase.class) {
if(INSTANCE == null) {
INSTANCE = Room.databaseBuilder(context.getApplicationContext(),AppDatabase.class,
"poetry.db").
addMigrations(AppDatabase.MIGRATION_1_2).// 修改数据库字段时更新数据库
allowMainThreadQueries().//允许在主线程读取数据库
build();
}
}
}
return INSTANCE;
}
/**
* 数据库变动添加Migration
*/
public static final Migration MIGRATION_1_2 = new Migration(1, 2) {
@Override
public void migrate(SupportSQLiteDatabase database) {
// todo 如果有修改数据库的字段 可以使用database.execSQL() 同时 修改数据库的version
}
};
}
d. 记录数据库sql 在build.gradle的defaultConfig里加上 (见开始的build.gradle文件)
// 记录数据库的sql和迁移
javaCompileOptions {
annotationProcessorOptions {
//room的数据库概要、记录
arguments = ["room.schemaLocation":
"$projectDir/schemas".toString()]
}
}
并且在build.gradle的anroid下加上
sourceSets {
//数据库概要、记录存放位置
androidTest.assets.srcDirs += files("$projectDir/schemas".toString())
}
编译后会生成文件schemas,这个文件记录了数据库sql以及迁移
总结a , b,c, d 步骤描述了room框架的使用。
3. databinding + ViewModel + LiveData的使用
首先需要在build.gradle的android下 添加
dataBinding {
enabled = true
}
以详情页的 布局文件为例 fragment_poetry_detail.xml ,其对应一个ViewModel类 PoetryViewModel
如上图,xml布局代码所示,data标签下
意思是:使用poetry可以直接访问实体类Poetry里的字段, 如android:text="@{poetry.title}"
涉及到数据的操作逻辑应该都封装到ViewModel中 , 随着项目代码逻辑增多,数据操作的逻辑还可以封装到数据仓库类 可以命名为XXXManager。
PoetryViewModel.java类如下:
/**
* 使用同一个obserable监听 只会响应最后的一个
* 这里区分本地的和network的
*/
// MutableLiveData继承liveData, 提供了setValue和postValue方法。liveData 是抽象类
private MutableLiveData mPoetryObservableLoacl= new MutableLiveData<>();
private MutableLiveData mPoetryObservableNetwork = new MutableLiveData<>();
private final String poetryID;
public PoetryViewModel(Application application, String poetryID) {
super(application);
this.poetryID = poetryID;
}
/**
* 读DB或者缓存
* 根据具体的名字获取Poetry
*/
public Poetry loadDataInfo(String name) {
Utils.printMsg(" poetry detail load info");
Poetry poetry = AppDatabase.getInstance(this.getApplication()).poetryDao().getPoetryByName(name);
mPoetryObservableLoacl.setValue(poetry);// setValue 需要在主进程调用
return poetry;
}
/**
* 发送网络请求
* 真实的请求逻辑封装到了RetrofitManager
* 根据名称搜索某个poetry
* @param name
*/
public void requestSearchPoetry(String name) {
mPoetryObservableNetwork = RetrofitManager.getInstance(this.getApplication()).searchPoetry(name);
}
/**
* 返回LiveData 对象
* @return
*/
public LiveData getmPoetryObservableNetwork() {
return mPoetryObservableNetwork;
}
public LiveData getmPoetryObservableLocal() {
return mPoetryObservableLoacl;
}
public static class Factory extends ViewModelProvider.NewInstanceFactory {
@NonNull
private final Application application;
private final String poetryID;
public Factory(@NonNull Application application, String poetryID) {
this.application = application;
this.poetryID = poetryID;
}
@Override
public T create(Class modelClass) {
//noinspection unchecked
return (T) new PoetryViewModel(application, poetryID);
}
}
}
这里遇到了一个问题, 如果同一个MutableLiveData,读本地数据后setValue 和 网络回包setValue, Observer.onChanged只响应最后setValue的那一次。 所以这里区分
private MutableLiveData mPoetryObservableLocal= new MutableLiveData<>();
private MutableLiveData mPoetryObservableNetwork = new MutableLiveData<>();
4. retrofit 的使用
(1) 请求接口的定义
NetWorkApiService.java
public interface NetWorkApiService {
public static String HTTPS_API_OPEN_URL = "https://api.apiopen.top/";
//获取唐诗列表
@GET("getTangPoetry")
Call> getPoetryList(@Query("page") String page, @Query("count") String count);
//根据名称搜索唐诗
@GET("searchPoetry")
Call> searchPoetry(@Query("name") String name);
//随机推荐一首词
@GET("recommendPoetry")
Call> recommendPoetry();
}
? 后面带有参数 使用@Query
如果参数是在URL路径中, 使用 @Path
(2) 把发送网络请求的接口封装到RetrofitManager.java中,如拉取列表的请求方法。
searchPoetry会返回一个Call对象, Call对象调用enqueue一个callback,响应网络请求。
public MutableLiveData searchPoetry(String name) {
final MutableLiveData data = new MutableLiveData<>();
netWorkApiService.searchPoetry(name).enqueue(new Callback>() {
@Override
public void onResponse(Call> call, Response> response) {
simulateDelay();
List poetries = response.body().getData();
data.setValue(poetries.get(0));
Log.i(Constants.TAG, "onResponse()");
}
@Override
public void onFailure(Call> call, Throwable t) {
data.setValue(null);
Log.i(Constants.TAG, "onFailure()");
}
});
return data;
}
(3)由于和goson结合一起使用,
因此NetWorkApiService接口中声明的方法返回的参数需要遵循网络回包格式,如下注释。
/**
* 字段的定义 需要遵循 Json串
* 例子:
*
* {"code":200,
* "message":"成功!",
* "result":[{"title":"帝京篇十首 一","content":"秦川雄帝宅,函谷壮皇居。|绮殿千寻起,离宫百雉余。|连甍遥接汉,飞观迥凌虚。|云日隐层阙,风烟出绮疏。","authors":"太宗皇帝"},
* {"title":"帝京篇十首 二","content":"岩廊罢机务,崇文聊驻辇。|玉匣启龙图,金绳披凤篆。|韦编断仍续,缥帙舒还卷。|对此乃淹留,欹案观坟典。","authors":"太宗皇帝"},
* {"title":"帝京篇十首 三","content":"移步出词林,停舆欣武宴。|雕弓写明月,骏马疑流电。|惊雁落虚弦,啼猿悲急箭。|阅赏诚多美,于兹乃忘倦。","authors":"太宗皇帝"}]}
*/
public class ResultList {
private int code;
private String message;
private List result;
public List getData() {
return result;
}
}
Demo地址
参考:
https://blog.csdn.net/zhuzp_blog/article/details/78871527 Android架构组件(二)——LiveData
https://www.jianshu.com/p/9516a3c08a25 LiveData + ViewModel + Room (Google 官文)+Demo
https://www.jianshu.com/p/e7628d6e6f61Mvvm模式: Databinding 与 ViewModel+LiveData+Repository
https://www.jianshu.com/p/3dd70c06696a databinding用法