好处:
- 存储数据
- 管理生命周期
- 模块化
- 避免常见的错误
- 减少样板代码
框架中包含的组件:
Room
ViewModel
LiveData
LifecycleObserver和LiecycleOwner
1. Room介绍:
一个稳健的SQL对象映射库
2.LiveData 介绍:
是一种可观测数据容器。它会在数据变化时通知外观测器,以便于更新界面。它还具备生命周期感知能力,例如:一个Activity何时离开屏幕或者销毁
3. LifecycleObserver和LiecycleOwner介绍 :
LifecycleOwner是具备声明周期的对象,可以是Activity和Fragment.
LifecycleObserver用于观测LifecycleOwner , 且在LifecycleOwner的生命周期变化时候,收到通知。
观测过程:
界面组件–>LiveData–>LifecycleOwner(Activity和Fragment).
配置变更的处理:
系统内存不足导致当Activity被销毁后又重建,或者手机屏幕旋转导致Activity销毁后重建的情况,若是LiveData与Activity生命周期捆绑会导致多次重复查询数据,和一些不必要的代码。
因此,将LiveData绑定和一些与界面有关的数据放到LiveModel中。
4. ViewModel介绍:
ViewModel是为了界面组件提供数据并可在配置变更后继续存在的对象。
通过一个在线获取网络电影资源,离线加载数据库的电影资源的案例,加深对Android 架构组件的了解。
需求分析:
一个显示张艺谋电影的列表界面。先从数据库中获取数据,若是获取为空,则通过网络,从豆瓣API中搜索张艺谋的电影列表,最终显示列表上。
本案例中框架和类库选取:
1. Android 架构组件:
Room数据库
ViewModel
LiveData
2. 网络通讯库:
3. 图片异步处理库:
4. 异步线程通讯库:
5. UI控件:
1.添加google maven repository:
allprojects {
repositories {
jcenter()
maven { url 'https://maven.google.com' }
}
}
在Project的builde.gralde中添加google maven repository。
2. 类库依赖:
根据上面的选取,在Module的builde.gradle中添加各种依赖:
dependencies {
compile fileTree(include: ['*.jar'], dir: 'libs')
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.android.support:appcompat-v7:26.+'
compile 'com.android.support.constraint:constraint-layout:1.0.2'
compile 'com.android.support:recyclerview-v7:26.0.0-alpha1'
testCompile 'junit:junit:4.12'
/************** Architecture components库 *************/
//Lifecycles库
compile 'android.arch.lifecycle:runtime:1.0.3'
annotationProcessor "android.arch.lifecycle:compiler:1.0.0"
//ViewModel和 LiveData库
compile 'android.arch.lifecycle:extensions:1.0.0'
//Room库
compile 'android.arch.persistence.room:runtime:1.0.0'
compile "android.arch.persistence.room:rxjava2:1.0.0"
annotationProcessor "android.arch.persistence.room:compiler:1.0.0"
/************** RxJava库和RxAndroid库 *************/
compile 'io.reactivex.rxjava2:rxjava:2.1.1'
compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
/************** Retrofit 和OkHttp 库 *************/
//异步加载图像
compile 'com.github.bumptech.glide:glide:3.8.0'
//网络请求操作
compile 'com.squareup.retrofit2:retrofit:2.3.0'
compile 'com.squareup.okhttp3:okhttp:3.8.0'
//解析数据,Gson方式json
compile 'com.squareup.retrofit2:converter-gson:2.3.0'
//网络日志输出
compile 'com.squareup.okhttp3:logging-interceptor:3.8.0'
//结合RxJava2使用
compile 'com.squareup.retrofit2:adapter-rxjava2:2.3.0'
}
3. 添加Java8语法支持:
在项目中集成retrolambda插件 , 在在Module的builde.gradle中添加以下代码:
apply plugin: 'com.android.application'
//gradle-retrolambda配置
apply plugin: 'me.tatarka.retrolambda'
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
更多详情,请阅读AndroidStudio 中开启Java8语法和Retrolambda库的使用。
4. 配置Room数据库中Schema export位置:
android {
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments = ["room.schemaLocation": "$projectDir/schemas".toString()]
}
}
}
}
更多详情,请阅读Android 架构组件之Room数据库 处理Schema export Error。
对了,别忘记在AndroidManifest.xml中添加
网络权限.
网络资源:https://api.douban.com/v2/movie/search?q=张艺谋
创建一个Retrofit的单例使用类,且初始化其配置:
public class RetrofitClient {
private final String BASE_URL = "https://api.douban.com/v2/movie/";
private final Retrofit retrofit;
private static RetrofitClient instance;
private DouBanService service;
private RetrofitClient(){
OkHttpClient okHttpClient = OkHttpProvider.createOkHttpClient();
this.retrofit = new Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build();
this.service=retrofit.create(DouBanService.class);
}
public synchronized static RetrofitClient getInstance(){
if (instance==null){
instance=new RetrofitClient();
}
return instance;
}
public Flowable getMovieList(){
String url = "search";
Map map=new HashMap<>();
map.put("q","张艺谋");
return this.service.movieList(url,map);
}
}
为OkHttp配置一个日志拦截器,方便查看请求和响应:
public class OkHttpProvider {
/**
* 自定义配置OkHttpClient
* @return
*/
public static OkHttpClient createOkHttpClient(){
OkHttpClient.Builder builder=new OkHttpClient.Builder();
HttpLoggingInterceptor loggingInterceptor=new HttpLoggingInterceptor();
//打印一次请求的全部信息
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
builder.addInterceptor(loggingInterceptor);
return builder.build();
}
}
返回数据的实体类:
public class MovieBeanList {
public List getSubjects() {
return subjects;
}
private List subjects;
}
这里采用Rtrofit和Room数据库共享一个MovieBean,这个实体类由来下面介绍。
为了实现显示圆形的图片,为Glide创建一个圆形图片的BitmapImageViewTarget子类。
public class CircularBitmapImageViewTarget extends BitmapImageViewTarget {
private Context context;
private ImageView imageView;
public CircularBitmapImageViewTarget(Context context,ImageView view) {
super(view);
this.context=context;
this.imageView=view;
}
/**
* 重写 setResource(),生成圆角的图片
* @param resource
*/
@Override
protected void setResource(Bitmap resource) {
RoundedBitmapDrawable bitmapDrawable= RoundedBitmapDrawableFactory.create(this.context.getResources(),resource);
bitmapDrawable.setCircular(true);
this.imageView.setImageDrawable(bitmapDrawable);
}
}
Room数据库是一个稳健的SQL对象映射库,将实体和表一一映射。
先来创建表名和字段名:
创建一个实体,编写属性(该名与表中字段名一样)。
@Entity(tableName = "movies")
public class MovieBean {
/**
* @PrimaryKey设置为主键,
* 且设置autoGenerate为true,自增长
*/
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
private int id;
@ColumnInfo(name = "year")
private String year;
@ColumnInfo(name = "title")
private String title;
@ColumnInfo(name = "image")
private String image;
@Ignore
private Images images;
public MovieBean(String year, String title, String image) {
this.year = year;
this.title = title;
this.image = image;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getYear() {
return year;
}
public void setYear(String year) {
this.year = year;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getImage() {
return image;
}
public void setImage(String image) {
this.image = image;
}
public Images getImages() {
return images;
}
public void setImages(Images images) {
this.images = images;
}
/**
* 网络数据源对应的实体类
*/
public static class Images{
private String small;
public String getSmall() {
return small;
}
public void setSmall(String small) {
this.small = small;
}
public String getLarge() {
return large;
}
public void setLarge(String large) {
this.large = large;
}
private String large;
}
}
由上面可知:
表名的设置:@Entity
注解,设置表名
@Entity(tableName = "movies")
public class MovieBean {
}
设置主键和自增长的字段
/**
* @PrimaryKey设置为主键,
* 且设置autoGenerate为true,自增长
*/
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "id")
private int id;
设置剩余字段:@ColumnInfo
注解指定表中的字段名
@ColumnInfo(name = "year")
private String year;
@ColumnInfo(name = "title")
private String title;
@ColumnInfo(name = "image")
private String image;
@Ignore
private Images images;
接下来,编写DAO层:
DAO设计模式是对一个表中数据增,删,查,改的操作
@Dao
public interface MovieDao {
/**
* 查询movies表中全部数据,且返回RxJava2 中Flowable对象
* @return
*/
@Query("select * from movies")
Flowable> getMovieList();
/**
* 插入全部数据
*/
@Insert
void insertMovieList(List movieBeans);
/**
* 删除movies表中全部数据
*/
@Query("delete from movies")
void deleteAllMovies();
}
从以上代码可知:
定义DAO接口:@Dao
注解标注接口
查询的SQL:@Query
注解内添加SQL
插入的SQL:使用@Insert
注解
注意点:原本Room数据库的查询可以返回LiveDta,因这里结合RxJava2使用,直接返回Flowable对象
创建数据库,设置数据库名和数据库版本:
@Database(entities = {MovieBean.class},version = 1)
public abstract class MovieDatabase extends RoomDatabase {
/**
* 获取Dao对象
*
* @return
*/
public abstract MovieDao movieDao();
/**
* 单例类MovieDatabase,同步获取对象
*/
private static volatile MovieDatabase instance;
public static MovieDatabase getInstance(Context context) {
if (instance == null) {
synchronized (MovieDatabase.class) {
if (instance == null) {
instance = Room.databaseBuilder(context.getApplicationContext(), MovieDatabase.class, "movies.db").build();
}
}
}
return instance;
}
}
从以上代码可知:
@Database
注解指定数据库表中映射的实体和版本。接下来,创建一个数据库的操作类
操作类通过调用DAO对象中的方法,从而操作数据库,实现数据的增删查改。
先抽象出一个行为的接口:
public interface MovieDataSource {
Flowable> getMovieList();
void insertMovieList(List movieBeans);
void deleteAllMovies();
}
继续撸代码,编写MovieDataSource接口的具体的实现类,和实现逻辑
public class LocalMovieDataSource implements MovieDataSource {
private MovieDao movieDao;
public LocalMovieDataSource(MovieDao movieDao) {
this.movieDao = movieDao;
}
@Override
public Flowable> getMovieList() {
return movieDao.getMovieList();
}
@Override
public void insertMovieList(List movieBeans) {
movieDao.insertMovieList(movieBeans);
}
@Override
public void deleteAllMovies() {
movieDao.deleteAllMovies();
}
}
定义一个ViewModelProvider.Factory子类:
用于创建ViewModel对象,且提供数据库操作类和网路操作类。
public class ViewModelFactory implements ViewModelProvider.Factory {
private final MovieDataSource movieDataSource;
private final RetrofitClient retrofitClient;
public ViewModelFactory(MovieDataSource movieDataSource,RetrofitClient retrofitClient) {
this.movieDataSource = movieDataSource;
this.retrofitClient=retrofitClient;
}
@NonNull
@Override
public T create(@NonNull Class modelClass) {
if (modelClass.isAssignableFrom(MovieViewModel.class)){
return (T) new MovieViewModel(movieDataSource,retrofitClient);
}
return null;
}
}
定义与Activity对应的ViewModel子类:
用于管控Activity中使用到的数据。
public class MovieViewModel extends ViewModel {
private MovieDataSource movieDataSource;
private RetrofitClient retrofitClient;
private List movieBeanList;
private final String tag = MovieViewModel.class.getSimpleName();
public MovieViewModel(MovieDataSource movieDataSource, RetrofitClient retrofitClient) {
this.movieDataSource = movieDataSource;
this.retrofitClient = retrofitClient;
}
/**
* 采用数据库,磁盘逐渐获取
*
* @return
*/
public Flowable> getMovieList() {
Log.i(tag, "开始获取电影数据");
return searDB();
}
private Flowable> searDB(){
return movieDataSource.getMovieList().flatMap( list -> {
Log.i(tag, "数据库中获取");
if (list==null||list.size()==0){
return searchNet();
}else{
movieBeanList=list;
}
return Flowable.just(list);
});
}
/**
* 若是数据库中无,则从网络中获取
* 最后,写入数据库
*
* @return
*/
private Flowable> searchNet() {
return retrofitClient.getMovieList().flatMap(movieBeen -> {
Log.i(tag, "网络中获取");
if (movieBeen.getSubjects().size() > 0) {
movieBeanList = movieBeen.getSubjects();
for (MovieBean bean : movieBeen.getSubjects()) {
bean.setImage(bean.getImages().getLarge());
}
movieDataSource.insertMovieList(movieBeen.getSubjects());
}
return Flowable.just(movieBeen.getSubjects());
});
}
}
LifecycleActivity类的源码:
/**
* @deprecated Use {@code android.support.v7.app.AppCompatActivity} instead of this class.
*/
@Deprecated
public class LifecycleActivity extends FragmentActivity {
}
继承LifecycleActivity类,结果却发觉已经废弃了。根据建议改为,继承AppCompatActivity。
public class AppFactory {
/**
*创建MovieDataSource 对象
* @param context
* @return
*/
public static MovieDataSource provideMovieDataSource(Context context){
MovieDatabase database=MovieDatabase.getInstance(context);
return new LocalMovieDataSource(database.movieDao());
}
/**
* 创建ViewModelFactory
* @param context
* @return
*/
public static ViewModelFactory providerViewModelFactory(Context context){
MovieDataSource dataSource=provideMovieDataSource(context);
RetrofitClient retrofitClient=providerRetrofitClient();
return new ViewModelFactory(dataSource,retrofitClient);
}
/**
* 创建Retrofit单例类
* @return
*/
public static RetrofitClient providerRetrofitClient(){
return RetrofitClient.getInstance();
}
}
采用SwipeRefreshLayout刷新,RecyclerView显示电影列表。
定义一个支持非直接滚动视图的SwipeRefreshLayout
public class ScrollChildSwipeRefreshLayout extends SwipeRefreshLayout {
private View scrollUpChild;
public ScrollChildSwipeRefreshLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* 设置在哪个view中触发刷新。
* @param view
*/
public void setScrollUpChild(View view){
this.scrollUpChild=view;
}
/**
*ViewCompat..canScrollVertically():用于检查view是否可以在某个方向上垂直滑动
* @return
*/
@Override
public boolean canChildScrollUp() {
if(scrollUpChild!=null){
return ViewCompat.canScrollVertically(scrollUpChild,-1);
}
return super.canChildScrollUp();
}
/**
* 设置
* @param indicator
*/
public void showIndicator(boolean indicator){
this.post(() -> this.setRefreshing(indicator));
}
/**
* 设置默认颜色
*/
public void setDefalutColor(){
this.setColorSchemeColors(Color.parseColor("#263238"), Color.parseColor("#ffffff"), Color.parseColor("#455A64"));
}
}
RecyclerView的设置比较简单,这列省略。
最后,在界面上直接开启加载,获取电影数据,然后显示在视图中:
public class MainActivity extends AppCompatActivity implements SwipeRefreshLayout.OnRefreshListener {
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
private ViewModelFactory viewModelFactory;
private MovieViewModel movieViewModel;
private RecyclerView recyclerView;
private ScrollChildSwipeRefreshLayout refreshLayout;
private MovieListAdapter movieListAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
init();
initView();
}
/***
* 初始化控件
*/
private void initView() {
this.recyclerView = findViewById(R.id.main_recyclerView);
this.recyclerView.setLayoutManager(new LinearLayoutManager(this));
this.movieListAdapter = new MovieListAdapter();
this.recyclerView.setAdapter(this.movieListAdapter);
this.refreshLayout = findViewById(R.id.main_swipeRefreshLayout);
this.refreshLayout.setDefalutColor();
this.refreshLayout.showIndicator(true);
this.refreshLayout.setScrollUpChild(recyclerView);
this.refreshLayout.setOnRefreshListener(this);
this.onRefresh();
}
/**
* 初始化
*/
private void init() {
this.viewModelFactory = AppFactory.providerViewModelFactory(this);
this.movieViewModel = ViewModelProviders.of(this, this.viewModelFactory).get(MovieViewModel.class);
}
/**
* 加载数据
*/
private void loadData() {
Disposable disposable = this.movieViewModel.getMovieList()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(movieBeen -> {
this.movieListAdapter.changeData(movieBeen);
this.refreshLayout.showIndicator(false);
}, error -> {
this.refreshLayout.showIndicator(false);
showToast(error.getMessage());
},()->{
showToast("执行完成");
}
);
this.compositeDisposable.add(disposable);
}
/**
* Toast弹窗提示
*
* @param content
*/
private void showToast(String content) {
Toast.makeText(getApplicationContext(), content, Toast.LENGTH_SHORT).show();
}
@Override
protected void onStop() {
super.onStop();
compositeDisposable.clear();
}
@Override
public void onRefresh() {
loadData();
}
}