Mvvm: ViewModel+LiveData+DataBinding+Retrofit+Room总结与实践

版权声明:本文为博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/xiaobaaidaba123/article/details/88667506

最近,阅读了国外一篇关于viewmodel+livedata的文章https://proandroiddev.com/mvvm-architecture-viewmodel-and-livedata-part-1-604f50cda1  ,收益良多,纸上得来终觉浅,绝知此事要躬行,决定自己也亲手撸一个demo。

一两句话和一两个图总结:

LiveData作用

(1)实际上就是一个观察者模式的数据容器,当数据改变时,通知UI刷新;

(2)数据LiveData<T>是被观察者,组件activity, fragment是观察者;

(3)LiveData能感知activity, fragment组件的生命周期,当activity,fragment销毁时,会自动清除引用,不必担心内存泄漏。而其他观察者回调的库需要自己管理生命周期。

(4)当activity或者fragment处于活动状态时如started,resumed,liveData才会通知。

(5)LiveData通常和ViewModel一起使用, LiveData<T>作为ViewModel的一个成员变量。

ViewModel作用

(1)将activity, fragment里关于数据操作的逻辑抽离出来,封装到ViewModel中,所以ViewMoel 持有一个成员变量LiveData<T>。

(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来引用我们的数据实体类。如:


<variable name="poetry" type="com.example.mikel.mvvmlivedataretrofitdemo.service.model.Poetry"/>

在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<Poetry>() {
            @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<Poetry> getAllPoetries();

    @Query("SELECT * FROM poetry_table WHERE title = :name") //条件查询
    Poetry getPoetryByName(String name);

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    void insertAll(List<Poetry> 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 version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>
        <!--databinding 使用isLoading 代表一个布尔变量-->
        <variable name="isLoading" type="boolean" />
        <!--databinding 使用poetry可以直接访问Poetry里的字段-->
        <variable name="poetry" type="com.example.mikel.mvvmlivedataretrofitdemo.service.model.Poetry"/>
    </data>

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:id="@+id/loadingTips"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center_vertical|center_horizontal"
            android:text="loading......"
            android:textAlignment="center"
            app:visibleGone="@{isLoading}"/>

        <LinearLayout
            android:layout_marginStart="8dp"
            android:layout_marginEnd="8dp"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center_vertical|center_horizontal"
            android:padding="5dp"
            android:paddingTop="16dp"
            android:orientation="vertical"
            app:visibleGone="@{!isLoading}">

            <ImageView
                android:id="@+id/imageView"
                android:layout_width="150dp"
                android:layout_height="125dp"
                android:src="@drawable/image" />

            <TextView
                android:id="@+id/name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:textStyle="bold"
                android:textSize="25sp"
                android:text="@{poetry.title}"
                android:textAlignment="center"
                android:paddingBottom="5dp"/>

            <TextView
                android:id="@+id/project_desc"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textSize="20sp"
                android:text="@{poetry.content}"/>


            <TextView
                android:id="@+id/languages"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:textSize="16sp"
                android:text="@{poetry.authors}"/>

        </LinearLayout>

    </FrameLayout>
</layout>

如上图,xml布局代码所示,data标签下

 <variable name="poetry" type="com.example.mikel.mvvmlivedataretrofitdemo.service.model.Poetry"/>

意思是:使用poetry可以直接访问实体类Poetry里的字段, 如android:text="@{poetry.title}"

             

涉及到数据的操作逻辑应该都封装到ViewModel中 , 随着项目代码逻辑增多,数据操作的逻辑还可以封装到数据仓库类 可以命名为XXXManager。

 

PoetryViewModel.java类如下:

     /**
     * 使用同一个obserable监听  只会响应最后的一个
     * 这里区分本地的和network的
     */
    // MutableLiveData继承liveData, 提供了setValue和postValue方法。liveData 是抽象类
    private MutableLiveData<Poetry> mPoetryObservableLoacl= new MutableLiveData<>();
    private MutableLiveData<Poetry> 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<Poetry> getmPoetryObservableNetwork() {
        return mPoetryObservableNetwork;
    }

    public LiveData<Poetry> 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 extends ViewModel> T create(Class<T> modelClass) {
            //noinspection unchecked
            return (T) new PoetryViewModel(application, poetryID);
        }
    }
}

这里遇到了一个问题, 如果同一个MutableLiveData,读本地数据后setValue 和 网络回包setValue, Observer.onChanged只响应最后setValue的那一次。 所以这里区分

private MutableLiveData<Poetry> mPoetryObservableLocal= new MutableLiveData<>();
private MutableLiveData<Poetry> 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<ResultList<Poetry>> getPoetryList(@Query("page") String page, @Query("count") String count);

    //根据名称搜索唐诗
    @GET("searchPoetry")
    Call<ResultList<Poetry>> searchPoetry(@Query("name") String name);

    //随机推荐一首词
    @GET("recommendPoetry")
    Call<Result<Poetry>> recommendPoetry();
}

? 后面带有参数  使用@Query

如果参数是在URL路径中, 使用 @Path

 

(2) 把发送网络请求的接口封装到RetrofitManager.java中,如拉取列表的请求方法。

searchPoetry会返回一个Call对象, Call对象调用enqueue一个callback,响应网络请求。
    public MutableLiveData<Poetry> searchPoetry(String name) {
        final MutableLiveData<Poetry> data = new MutableLiveData<>();

        netWorkApiService.searchPoetry(name).enqueue(new Callback<ResultList<Poetry>>() {
            @Override
            public void onResponse(Call<ResultList<Poetry>> call, Response<ResultList<Poetry>> response) {
                simulateDelay();
                List<Poetry> poetries = response.body().getData();
                data.setValue(poetries.get(0));
                Log.i(Constants.TAG, "onResponse()");
            }

            @Override
            public void onFailure(Call<ResultList<Poetry>> 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<Poetry> {
    private int code;
    private String message;
    private List<Poetry> result;

    public List<Poetry> 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用法

 

展开阅读全文

没有更多推荐了,返回首页