当前位置: 代码迷 >> 综合 >> 【JetPack+Retrofit+Rxjava】获取Bing每日一图并显示ViewModel+LiveData+DataBinding+MVVM 补充笔记
  详细解决方案

【JetPack+Retrofit+Rxjava】获取Bing每日一图并显示ViewModel+LiveData+DataBinding+MVVM 补充笔记

热度:45   发布时间:2023-12-15 11:08:15.0

扉:

  1. 原文来自:Android官方架构组件ViewModel+LiveData+DataBinding架构属于自己的MVVM
  2. 很喜欢作者的思路,但是使用Kotlin需要配置的东西好多并且很多细节要重写,于是在原作基础上进行了Kotlin的二创和补充说明。而且没有对应的导包,自己学的时候还蛮伤脑筋的。
  3. 注意实现的接口,onChange的接口是lifecycle的,三态接口是Rxjava的(next,error,onComplete)
  4. 测试发现bean类的顺序可以乱,可以不写 但是名称和回调的参数名称必须得一致,那不想一致想自定义怎么办?可以参照这种写法,不一定有用哈【Android-Kotlin-Volley】图片画廊学习笔记
    在这里插入图片描述

一:效果图展示

在这里插入图片描述

二:配置文件

1. 添加Glide、Retrofit、RxJava的依赖

    implementation 'com.squareup.retrofit2:retrofit:2.4.0'implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'implementation 'com.squareup.retrofit2:converter-gson:2.4.0'implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'implementation 'io.reactivex.rxjava2:rxjava:2.1.12'implementation 'com.github.bumptech.glide:glide:4.6.1'annotationProcessor 'com.github.bumptech.glide:compiler:4.6.1'

2. 启用DataBinding

  1. 用手打吧,每次ctrl c/v有点憨憨。DataBing实现了ViewBind的所有功能,但他的效率也偏低。xml纠缠。
    在这里插入图片描述
  2. App内的build.gradle的plugins加入,为了新版更好的支持BindingAdapter,不然xml中的引用会报错的
   id 'kotlin-kapt'
  1. DataBinding报Null问题的解决方式

3. 添加网络权限

<uses-permission android:name="android.permission.INTERNET"/>

4. 接口信息

  1. 接口地址
https://cn.bing.com/HPImageArchive.aspx?format=js&idx=1&n=1
  1. postman测试,返回的Json结构,采用GsonFormat解析
    在这里插入图片描述
{
    "images": [{
    "startdate": "20210202","fullstartdate": "202102021600","enddate": "20210203","url": "/th?id=OHR.MountNemrut_ZH-CN4681788604_1920x1080.jpg&rf=LaDigue_1920x1080.jpg&pid=hp","urlbase": "/th?id=OHR.MountNemrut_ZH-CN4681788604","copyright": "内姆鲁特山上巨大的石灰岩雕像,土耳其阿德亚曼 (? Peerakit JIrachetthakun/Getty Images)","copyrightlink": "https://www.bing.com/search?q=%E5%86%85%E5%A7%86%E9%B2%81%E7%89%B9%E5%B1%B1&form=hpcapt&mkt=zh-cn","title": "","quiz": "/search?q=Bing+homepage+quiz&filters=WQOskey:%22HPQuiz_20210202_MountNemrut%22&FORM=HPQUIZ","wp": true,"hsh": "8e96102b6ad68ddce2ffcd8732f8d6f2","drk": 1,"top": 1,"bot": 1,"hs": []}],"tooltips": {
    "loading": "正在加载...","previous": "上一个图像","next": "下一个图像","walle": "此图片不能下载用作壁纸。","walls": "下载今日美图。仅限用作桌面壁纸。"}
}

三:代码块

在这里插入图片描述

1. ImageBean(根据返回的json格式,使用GsonFormat生成)

  1. 原作者很细心:接口并没有返回图片url前缀信息,所以我在ImagesBean的内部手动添加了一个变量BASE_URL来存储图片url前缀信息。【刚好对应Retrofit】

class ImageBean {
    var tooltips: TooltipsBean? = nullvar images: List<ImagesBean>? = null//静态类class TooltipsBean {
    var loading: String? = nullvar previous: String? = nullvar next: String? = nullvar walle: String? = nullvar walls: String? = null}//静态类class ImagesBean {
    companion object {
    const val BASE_URL = "https://www.bing.com/"}var startdate: String? = nullvar fullstartdate: String? = nullvar enddate: String? = nullvar url: String? = nullvar urlbase: String? = nullvar copyright: String? = nullvar copyrightlink: String? = nullvar quiz: String? = nullvar isWp = falsevar hsh: String? = nullvar drk = 0var top = 0var bot = 0var hs: List<*>? = null}
}

2. Data类

  1. 由于项目采用MVVM架构,View层与ViewModel层的通信是通过LiveData这个架构组件实现的,不同于MVP架构中通过接口来通信,所以还要对数据加载的状态和错误信息进行维护。这里创建一个包装类来维护数据的状态和错误信息,以便View层可以对数据加载错误信息进行响应和处理。
  2. 泛型T,原作涉及到T的部分都有些问题
  3. 只有一个泛型T的成员mData来存储数据,和一个String类型的mErrorMsg来存储错误信息。这样View层就可以通过判断mErrorMsg是否为空来判断出数据加载成功与否。
class Data<T>(data: T?, errorMsg: String?) {
    private var mData: T ?=nullvar errorMsg: String?=nullvar data: T?get() = mDataset(data) {
    mData = data}init {
    if (data != null) {
    mData = data}if (errorMsg != null) {
    this.errorMsg = errorMsg}}
}

3. 创建数据访问接口ImageRepertory

  1. Retrofit+Rxjava作为网络访问框架。首先ImageRepertory内部有一个Retrofit实例,并且在构造函数中进行Retrofit的配置和创建。接着创建一个Service接口,其中的getImage方法用来获取图片信息,方法返回一个ImageBean的【Rxjava】Observable对象。
  2. 由于只有一个方法,所以作者没有另外抽取service

class ImageRepertory {
    private val mRetrofit: Retrofitprivate interface Service {
    @GET("HPImageArchive.aspx")fun getImage(@Query("format") format: String?,@Query("idx") idx: Int,@Query("n") n: Int): Observable<ImageBean>}fun getImage(format: String?, idx: Int, n: Int): Observable<ImageBean> {
    return mRetrofit.create(Service::class.java).getImage(format, idx, n)}init {
    mRetrofit = Retrofit.Builder().baseUrl("https://cn.bing.com/").addConverterFactory(GsonConverterFactory.create()).addCallAdapterFactory(RxJava2CallAdapterFactory.create()).build()}
}

4. 编写ImageViewModel

  1. 类要继承自android.arch.lifecycle.ViewModel这个类,以便在创建时与View层的生命周期相关联。然后是三个成员变量:mImage这个变量的类型是MutableLiveData用来存放图片信息,以便当信息发生变化时及时通知View层来更新界面;mRepertory这个变量来负责数据访问;idx这个变量来记录当前的图片页码。这三个变量在构造函数中创建并初始化,接着为mImage添加了getter方法以便View层可以对其进行观察与响应。loadImage,nextImage和previousImage这三个方法分别对应图片的加载,下一张和上一张,并且内部通过访问mRepertory的方法来完成数据的访问,又对返回的数据进行判断处理并触发mImage的setValue方法来对数据进行更新。

class ImageViewModel : ViewModel() {
    //定义了Data系的image对象val image: MutableLiveData<Data<ImageBean.ImagesBean?>> = MutableLiveData()private val mRepertory: ImageRepertory = ImageRepertory()private var idx: Int = 0fun loadImage() {
    mRepertory.getImage("js", idx, 1).subscribeOn(Schedulers.io())//改变调用它之前代码的线程.observeOn(AndroidSchedulers.mainThread())//改变调用它之后代码的线程.subscribe(object : Observer<ImageBean> {
    override fun onSubscribe(d: Disposable) {
    }override fun onError(e: Throwable) {
    image.value = Data(null, e.message)}override fun onComplete() {
    }override fun onNext(t: ImageBean) {
    image.setValue(Data<ImageBean.ImagesBean?>(t.images!![0] as ImageBean.ImagesBean?, null))}})}fun nextImage() {
    mRepertory.getImage("js", ++idx, 1).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(object : Observer<ImageBean?> {
    override fun onSubscribe(d: Disposable) {
    }override fun onNext(imageBean: ImageBean) {
    image.setValue(Data(imageBean.images!![0] as ImageBean.ImagesBean?, null))}override fun onError(e: Throwable) {
    image.value = Data(null, e.message!!)idx--}override fun onComplete() {
    }})}fun previousImage() {
    if (idx <= 0) {
    image.value = Data(null, "已经是第一个了")return}mRepertory.getImage("js", --idx, 1).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(object : Observer<ImageBean?> {
    override fun onSubscribe(d: Disposable) {
    }override fun onNext(imageBean: ImageBean) {
    image.setValue(Data(imageBean.images!![0] as ImageBean.ImagesBean?, null))}override fun onError(e: Throwable) {
    image.setValue(Data(null, e.message!!))idx++}override fun onComplete() {
    }})}}

5. 编写页面

  1. 与正常的XML布局文件不同的是,根标签改成了layout标签,内部有data标签和具体的布局。dat标签内存放Databinding的数据类。除了需要用到的ImagesBean类之外,这里还声明了一个Presenter类用来对界面的用户行为做统一的管理。注意到ImageView的标签内声明了一个url属性,并且和data内的image的数据进行了绑定。然而ImageView并没有这个属性,这时就需要用到Databinding的自定义属性了。
  2. 注意这边转换data的时候,有些测试版本AS是不会自动出小灯泡的。快捷键是【Alt+回车】光标放在androidx上即可
<?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"xmlns:tools="http://schemas.android.com/tools"><data><variablename="imageBean"type="com.ywjh.retrofitgetbinding.ImageBean.ImagesBean" /><variablename="presenter"type="com.ywjh.retrofitgetbinding.ImageActivity.Presenter" /></data><LinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"><ImageViewapp:url="@{imageBean.BASE_URL+imageBean.url}"android:layout_width="match_parent"android:layout_height="300dp" /><LinearLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"android:orientation="horizontal"><Buttonandroid:id="@+id/btn_previous"android:layout_width="0dp"android:layout_height="wrap_content"android:onClick="@{presenter.onClick}"android:layout_weight="1"android:text="上一张" /><Buttonandroid:id="@+id/btn_load"android:layout_width="0dp"android:onClick="@{presenter.onClick}"android:layout_height="wrap_content"android:layout_weight="1"android:text="加载" /><Buttonandroid:id="@+id/btn_next"android:layout_width="0dp"android:layout_height="wrap_content"android:onClick="@{presenter.onClick}"android:layout_weight="1"android:text="下一张" /></LinearLayout></LinearLayout>
</layout>

6. 建立BindingAdapter绑定xml方式

  1. 以注解的方式联合,然后使用JvmStatic
object BindingAdapter {
    @JvmStatic@androidx.databinding.BindingAdapter("url")fun setImageUrl(imageView: ImageView, url: String?) {
    Glide.with(imageView.context).load(url).into(imageView)}
}

7. ImageActivity

  1. 三个成员变量:mBinding数据绑定对象,用来实现数据绑定;mViewModel用来获取数据,实现与数据层的解耦;mProgressDialog用来弹出加载提示框。这三个变量在oncreate方法中初始化,mBinding用DataBindingUtil的setContentView方法实现视图层的绑定;mViewModel要使用ViewModelProvider的get方法完成创建。接着对ViewModel中的LiveData进行观察,在observe方法中处理错误和数据的绑定。内部类Presenter用来对点击事件进行响应,并且也要在oncreate方法里与mBinding进行绑定。
  2. 注意:xml文件中一定不能出现与业务相关的代码!比如直接将ViewModel的访问数据的方法在xml中与按钮的点击事件进行绑定,这种方做法是不可取的,因为XML文件的作用应该只是进行数据的显示和用户的交互,而访问数据这种和业务相关的操作不应出现在XML文件中。

class ImageActivity : AppCompatActivity() {
    private var mViewModel: ImageViewModel? = nullprivate var mProgressDialog: ProgressDialog? = nulloverride fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)val binding: ActivityImageBinding = DataBindingUtil.setContentView(this,R.layout.activity_image)mViewModel = ViewModelProvider(this, ViewModelProvider.AndroidViewModelFactory(application)).get(ImageViewModel::class.java)mProgressDialog = ProgressDialog(this)mProgressDialog!!.setMessage("加载中")mViewModel!!.image.observe(this, object : Observer<Data<ImageBean.ImagesBean?>?> {
    override fun onChanged(@Nullable t: Data<ImageBean.ImagesBean?>?) {
    if (t != null) {
    if (t.errorMsg != null) {
    Toast.makeText(this@ImageActivity, t.errorMsg, Toast.LENGTH_SHORT).show()mProgressDialog!!.dismiss()return}}if (t != null) {
    binding.setImageBean(t.data)title = t.data?.copyright}mProgressDialog!!.dismiss()}})binding.setPresenter(Presenter())mProgressDialog!!.show()mViewModel!!.loadImage()}inner class Presenter {
    fun onClick(view: View) {
    mProgressDialog!!.show()when (view.getId()) {
    R.id.btn_load -> mViewModel?.loadImage()R.id.btn_previous -> mViewModel!!.previousImage()R.id.btn_next -> mViewModel!!.nextImage()else -> {
    }}}}
}
  相关解决方案