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

扉:

  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? = null
    var images: List<ImagesBean>? = null

    //静态类
    class TooltipsBean {
        var loading: String? = null
        var previous: String? = null
        var next: String? = null
        var walle: String? = null
        var walls: String? = null
    }
    //静态类

    class ImagesBean {

        companion object {
            const val BASE_URL = "https://www.bing.com/"
        }

        var startdate: String? = null
        var fullstartdate: String? = null
        var enddate: String? = null
        var url: String? = null
        var urlbase: String? = null
        var copyright: String? = null
        var copyrightlink: String? = null
        var quiz: String? = null
        var isWp = false
        var hsh: String? = null
        var drk = 0
        var top = 0
        var bot = 0
        var 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 ?=null
    var errorMsg: String?=null
    var data: T?
        get() = mData
        set(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: Retrofit

    private 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 = 0

    fun 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>
        <variable
            name="imageBean"
            type="com.ywjh.retrofitgetbinding.ImageBean.ImagesBean" />

        <variable
            name="presenter"
            type="com.ywjh.retrofitgetbinding.ImageActivity.Presenter" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <ImageView
            app:url="@{imageBean.BASE_URL+imageBean.url}"
            android:layout_width="match_parent"
            android:layout_height="300dp" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="horizontal">

            <Button
                android:id="@+id/btn_previous"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:onClick="@{presenter.onClick}"
                android:layout_weight="1"
                android:text="上一张" />

            <Button
                android:id="@+id/btn_load"
                android:layout_width="0dp"
                android:onClick="@{presenter.onClick}"
                android:layout_height="wrap_content"
                android:layout_weight="1"
                android:text="加载" />

            <Button
                android: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? = null
    private var mProgressDialog: ProgressDialog? = null

    override 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 -> {
                }
            }
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值