Kotlin-Android开发之MVP模式+OkHttp3+RecyclerView下拉刷新和上滑加载更多框架封装

前言:没有万能的框架,也没有万能的模版,我们需要做的就是根据需求选对框架,然后进行修改,最终达到自己的目的。相信你通过《Kotlin&Java-Android开发之MVP模式+Retrofit2.0框架封装》和《Kotlin-Android开发之MVP模式+Retrofit2.0+RxJava1.0+Dagger2框架封装》这两篇博客,对MVP模式、网络的访问有个比较全面的理解,但是上述两个框架也有很大的缺点,比如没有实现对RecycleView下拉刷新和上拉加载更多的封装,所以今天我们来实现这一个功能,当然单独这一个框架也有缺点,等我慢慢道来。(注意:代码使用kotlin语言)


老样子还是先介绍一下MVP模式:M(Model)即模型层,功能是提供数据,放在安卓中对应的是数据库、Beans、网络请求等。V(View)即视图层,功能是提供数据的展示,放在安卓中对应的是activity、fragment等。P(Presenter)即协调者,功能是负责主要逻辑功能的实现,将M和V进行联系起来,M和V彻底分离。

OkHttp3:是一个非常优秀的网络请求的开源框架,谷歌推荐使用。好处很多这里不再过多介绍,这里有个不错的博客想看的可以移步《OKHttp3 基本用法

RecyclerView:Android5.0之后,开发就慢慢的从ListView转向RecyclerView了,如果你还没开始看RecyclerView的话,欢迎请看我的RecyclerView系列博客

1)《Android开发之RecyclerView的基本使用(实现常用的4种效果)

2)《Android开发之RecyclerView实现点击事件和长按事件

3)《Android开发之RecyclerView的间隔线处理

4)《Android开发之RecyclerView添加头部和底部

5)《Android开发之实现滑动RecyclerView,浮动按钮的显示和隐藏(一)

6)《Android开发之实现滑动RecyclerView,浮动按钮的显示和隐藏(二)

7)《Android开发之RecyclerView的交互动画(实现拖拽和删除)

8)《Kotlin-Android开发之RecyclerView+StickyListHeadersListView(粘列表标题控件)实现商品类别和商品的左右联动


现在开始对撸起代码

1.先看一下整体实现效果

2.包的结构和要解析的JSON数据结构

3.需要添加的依赖

    implementation "org.jetbrains.anko:anko:0.10.1"
    implementation 'com.google.code.gson:gson:2.2.4'
    implementation 'com.squareup.okhttp3:okhttp:3.8.1'
    implementation 'com.android.support:design:28.0.0'

4.先完成base包代码的编写:分别对Activity基类的编写,下拉刷新上滑加载跟多的基类的编写。

BaseActivity.kt:是所有Activity的基类,在次类里面可以完成获取布局id,initData()方法和initLinstener()方法,同样的方法我们也可以完成对BaseFragment.kt的编写,不过此项目中不涉及Fragment所以没有写。

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import org.jetbrains.anko.AnkoLogger
import org.jetbrains.anko.startActivity
import org.jetbrains.anko.toast

/**
 * 所有activity类的基类
 */
abstract class BaseActivity : AppCompatActivity(),AnkoLogger {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(getLayoutId())
        initListener()
        initData()
    }

    /*
     * 初始化数据
     */
    open protected fun initData() {
    }

    /**
     * adapter listener
     */
    open protected fun initListener() {
    }

    /**
     * 获取布局id
     */
    abstract fun getLayoutId(): Int

    open protected fun myToast(msg:String){
        runOnUiThread { toast(msg) }
    }

    inline fun <reified T:BaseActivity> startActivityAndFinish(){
        startActivity<T>()
        finish()
    }
}
BaseView.kt:是所有下拉刷新和上拉加载更多列表界面的view的接口基类,通过指定的泛型,实现接口里面的三个方法,来更新UI
/**
 * ClassName:BaseView
 * Description:所有下拉刷新和上拉加载更多列表界面的view的基类
 */
interface BaseView<RESPONSE> {
    /**
     * 获取数据失败
     */
    fun  onError(message: String?)

    /**
     * 初始化数据或者刷新数据成功
     */
    fun  loadSuccess(reponse: RESPONSE?)

    /**
     * 加载更多成功
     */
    fun  loadMore(response: RESPONSE?)
}

BaseListActivity.kt:所有具有下拉刷新和上拉加载更多列表界面的基类

import android.graphics.Color
import android.util.Log
import android.view.View
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.fly.demo01.R
import com.fly.demo01.utils.ThreadUtil
import kotlinx.android.synthetic.main.activity_list.*


/**
 * ClassName:BaseListFragment
 * Description:所有具有下拉刷新和上拉加载更多列表界面的基类
 * 基类抽取
 * mainView->BaseView
 * Presenter->BaseListPresenter
 * Adapter->BaseListAdapter
 */
abstract class BaseListActivity<RESPONSE,ITEMBEAN,ITEMVIEW:View> : BaseActivity(), BaseView<RESPONSE> {


    override fun getLayoutId(): Int {
        return R.layout.activity_list
    }

    override fun onError(message: String?) {
        Log.e("--onError--",message)
        recycleView.visibility=View.INVISIBLE//隐藏RecycleView
        ThreadUtil.handler.postDelayed(Runnable {
            refreshLayout.isRefreshing = false//延迟隐藏刷新
            ThreadUtil.runOnMainThread(Runnable {
                myToast("加载数据失败")
            })
        },1500)
    }

    override fun loadSuccess(response:RESPONSE?) {
        Log.e("--loadSuccess--",response.toString())
        //隐藏刷新控件
        refreshLayout.isRefreshing = false
        //刷新列表
        adapter.updateList(getList(response))
    }



    override fun loadMore(response: RESPONSE?) {
        adapter.loadMore(getList(response))
    }

    //适配
    val adapter by lazy { getSpecialAdapter() }

    val presenter by lazy { getSpecialPresenter() }


    override fun initListener() {
        //初始化recycleview
        recycleView.layoutManager = LinearLayoutManager(this)
        recycleView.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))
        recycleView.adapter = adapter

        //初始化刷新控件
        refreshLayout.setColorSchemeColors(Color.RED, Color.YELLOW, Color.GREEN)
        //刷新监听
        refreshLayout.setOnRefreshListener {
            //刷新监听
            presenter.loadDatas()
        }
        //监听列表滑动
        recycleView.addOnScrollListener(object : RecyclerView.OnScrollListener(){
            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                if(newState== RecyclerView.SCROLL_STATE_IDLE){
                    //是否最后一条已经显示
                    val layoutManager = recyclerView.layoutManager
                    if(layoutManager is LinearLayoutManager){
                        val manager: LinearLayoutManager = layoutManager
                        val lastPosition = manager.findLastVisibleItemPosition()
                        if(lastPosition==adapter.itemCount-1){
                            //最后一条已经显示了
                            presenter.loadMore(adapter.itemCount-1)//adapter.itemCount-1当前的个数
                        }
                    }
                }
            }
        })
    }

    override fun initData() {
        //初始化数据
        presenter.loadDatas()
    }
    /**
     * 获取适配器adapter
     */
    abstract fun getSpecialAdapter():BaseListAdapter<ITEMBEAN,ITEMVIEW>
    /**
     * 获取presenter
     */
    abstract fun getSpecialPresenter():BaseListPresenter
    /**
     * 从返回结果中获取列表数据集合
     */
    abstract fun getList(response: RESPONSE?): List<ITEMBEAN>?
}

对应的布局文件activity_list.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <androidx.swiperefreshlayout.widget.SwipeRefreshLayout
        android:id="@+id/refreshLayout"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recycleView"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

</LinearLayout>

BaseListAdapter.kt:所有下拉刷新和上拉加载更多列表界面adapter基类

import android.content.Context
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.fly.demo01.ui.widget.LoadMoreView


/**
 * ClassName:BaseListAdapter
 * Description:所有下拉刷新和上拉加载更多列表界面adapter基类
 */
abstract class BaseListAdapter<ITEMBEAN, ITEMVIEW : View> : RecyclerView.Adapter<BaseListAdapter.BaseListHolder>() {

    private var list = ArrayList<ITEMBEAN>()
    /**
     * 更新数据
     */
    fun updateList(list: List<ITEMBEAN>?) {
        list?.let {

            this.list.clear()
            this.list.addAll(list)
            notifyDataSetChanged()
        }
    }

    /**
     * 加载更多
     */
    fun loadMore(list: List<ITEMBEAN>?) {
        list?.let {
            this.list.addAll(list)
            notifyDataSetChanged()
        }
    }

    override fun onBindViewHolder(holder: BaseListHolder, position: Int) {
        //如果是最后一条 不需要刷新view
        if (position == list.size) return
        //条目数据
        val data = list.get(position)
        //条目view
        val itemView = holder?.itemView as ITEMVIEW
        //条目刷新
        refreshItemView(itemView, data)
        //设置条目点击事件
        itemView.setOnClickListener {
            //条目点击事件
            listener?.let {
                it(data)
            }
        }
    }

    //定义函数类型变量
    var listener: ((itemBean: ITEMBEAN) -> Unit)? = null

    fun setMyListener(listener: (itemBean: ITEMBEAN) -> Unit) {
        this.listener = listener
    }

    override fun getItemViewType(position: Int): Int {
        if (position == list.size) {
            //最后一条
            return 1
        } else {
            //普通条目
            return 0
        }
    }

    override fun getItemCount(): Int {
        return list.size + 1
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseListHolder {
        if (viewType == 1) {
            //最后一条
            return BaseListHolder(LoadMoreView(parent?.context))
        } else {
            //普通条目
            return BaseListHolder(getItemView(parent?.context))
        }
    }

    class BaseListHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

    }

    /**
     * 刷新条目view
     */
    abstract fun refreshItemView(itemView: ITEMVIEW, data: ITEMBEAN)

    /**
     * 获取条目view
     */
    abstract fun getItemView(context: Context?): ITEMVIEW
}

 BaseListPresenter.kt所有下拉刷新和上拉加载更多列表界面presenter的基类

/**
 * ClassName:BaseListPresenter
 * Description:所有下拉刷新和上拉加载更多列表界面presenter的基类
 */
interface BaseListPresenter {
    companion object {
        val TYPE_INIT_OR_REFRESH = 1//初始化或下拉刷新
        val TYPE_LOAD_MORE = 2
    }
    fun loadDatas()
    fun loadMore(offset: Int)//offset当前的个数
    //解绑presenter和view
    fun destoryView()
}

5.M层代码的编写:完成bean的编写和网络请求的编写

ResponseInfo.kt通过分析json的数据结构可以得到如下代码

//没用到
data class ResponseInfo (
    var error_code: String,
    var reason: String,
    var result: Result
)
//没用到
data class Result(
    var stat: String,
    var data: List<Data>
)
//用到
data class Data(
    var author_name: String,
    var category: String,
    var date: String,
    var thumbnail_pic_s: String,
    var title: String,
    var uniquekey: String,
    var url: String
)

 ResponseHandler.kt:请求回调成功和失败的接口

interface ResponseHandler<RESOPONSE> {
    fun onError(type:Int, msg:String?)
    fun onSuccess(type:Int, msg:RESOPONSE)
}

NetManager.kt:发送网络请求类,也是OkHttp的实现(重点)

import com.fly.demo01.utils.ThreadUtil
import java.io.IOException
import okhttp3.*
import org.json.JSONObject


/**
 * ClassName:NetManager
 * Description:发送网络请求类
 * 单例模式
 */
class NetManager private constructor(){
    val client by lazy { OkHttpClient() }
    companion object {
        val manager by lazy { NetManager() }
    }

    /**
     * 发送网络请求
     */
    fun <RESPONSE>sendRequest(req: MRequest<RESPONSE>){

        val request = Request.Builder()
                .url(req.url)
                .get()
                .build()
        client.newCall(request).enqueue(object : Callback {
            /**
             * 子线程调用
             */
            override fun onFailure(call: Call?, e: IOException?) {
                ThreadUtil.runOnMainThread(object : Runnable {
                    override fun run() {
                        //隐藏刷新控件
                        req.handler.onError(req.type,e?.message)
                    }
                })
            }

            /**
             * 子线程调用
             */
            override fun onResponse(call: Call?, response: Response?) {
                val json = response?.body()?.string()
                val jsonObject = JSONObject(json)
                val error_code:String = jsonObject.getString("error_code")
                if (error_code.equals("0")){
                    val result: JSONObject = jsonObject.getJSONObject("result")
                    val data = result.getString("data")
                    val parseResult = req.parseResult(data)
                    ThreadUtil.runOnMainThread(object : Runnable {
                        override fun run() {
                            req.handler.onSuccess(req.type,parseResult)
                        }
                    })
                }else if (error_code.equals("10001")){
                    //隐藏刷新控件
                    req.handler.onError(req.type,"错误的请求KEY")
                }else if (error_code.equals("10022")){
                    //隐藏刷新控件
                    req.handler.onError(req.type,"接口地址不存在")
                } else{
                    //隐藏刷新控件
                    req.handler.onError(req.type,"请求的数据格式有问题")
                }
            }
        })
    }
}

MRequest.kt:所有的请求基类

import android.util.Log
import com.google.gson.Gson
import org.json.JSONObject
import java.lang.reflect.ParameterizedType


/**
 * ClassName:MRequest
 * Description:所有请求基类
 */
open class MRequest<RESPONSE>(val type:Int, val url:String, val handler: ResponseHandler<RESPONSE>) {

    /**
     * 解析网络请求结果
     */
    fun parseResult(result: String?): RESPONSE {
        val gson = Gson()
        //获取泛型类型
        val type = (this.javaClass.genericSuperclass as ParameterizedType).getActualTypeArguments()[0]
        val list = gson.fromJson<RESPONSE>(result, type)
        return list
    }

    /**
     * 发送网络请求
     */
    fun excute(){
        NetManager.manager.sendRequest(this)
    }
}

MainActivityRequest.kt:MainActivity数据请求类

import android.util.Log
import com.fly.demo01.model.beans.Data

/**
 * Description:MainActivity数据请求类
 */

class MainActivityRequest(type:Int, offset:Int,handler: ResponseHandler<List<Data>>):
        MRequest<List<Data>>(type,
                "http://v.juhe.cn/toutiao/index?type=caijing&key=自己申请",
                handler){
        init {
            Log.e("--MainRequest-offset-",offset.toString())//请求的数据个数
        }
}

6.P层代码的实现:主要实现的功能是UI页面通过P层调用M层访问数据,然后经由P层调用V层进行页面的刷新。

MainActivityPresenter.kt:继承BaseListPresenter类,然后可以在内部再定义一些接口。(此类中没有实现,有需求的话可以自己实现)

import com.fly.demo01.base.BaseListPresenter

interface MainActivityPresenter: BaseListPresenter {

}

MainActivityPresenterImpl.kt:Main对应的Presenter实现类,完成数据的加载成功失败、更新、加载更多以及解绑view和presenter

import com.fly.demo01.base.BaseListPresenter
import com.fly.demo01.base.BaseView
import com.fly.demo01.model.beans.Data
import com.fly.demo01.model.net.MainActivityRequest
import com.fly.demo01.model.net.ResponseHandler
import com.fly.demo01.presenter.interf.MainActivityPresenter

class MainActivityPresenterImpl(var mainView: BaseView<List<Data>>?): MainActivityPresenter, ResponseHandler<List<Data>> {

    /**
     * 解绑view和presenter
     */
    override fun destoryView(){
        if(mainView!=null){
            mainView = null
        }
    }

    //失败
    override fun onError(type:Int,msg: String?) {
        mainView?.onError(msg)
    }

    /**
     * 加载数据成功
     */
    override fun onSuccess(type:Int,result: List<Data>) {
        //区分初始化  加载更多
        when(type){
            BaseListPresenter.TYPE_INIT_OR_REFRESH->mainView?.loadSuccess(result)
            BaseListPresenter.TYPE_LOAD_MORE->mainView?.loadMore(result)
        }
    }

    /**
     * 初始化数据或者刷新
     */
    override fun loadDatas() {
        //定义request
        MainActivityRequest(BaseListPresenter.TYPE_INIT_OR_REFRESH,0,this).excute()
    }

    //volley
    override fun loadMore(offset: Int) {
        //定义request
        MainActivityRequest(BaseListPresenter.TYPE_LOAD_MORE,offset,this).excute()
    }

}

7.V层代码:通过调用P层来完成UI页面的显示

MainActivity.kt

import com.fly.demo01.base.BaseListActivity
import com.fly.demo01.base.BaseListAdapter
import com.fly.demo01.base.BaseListPresenter
import com.fly.demo01.model.beans.Data
import com.fly.demo01.presenter.impl.MainActivityPresenterImpl
import com.fly.demo01.ui.adapter.MainAdapter
import com.fly.demo01.ui.widget.DataItemView

class MainActivity : BaseListActivity<List<Data>, Data, DataItemView>() {

    lateinit var mainAdapter: MainAdapter

    override fun initListener() {
        super.initListener()
        mainAdapter.setMyListener {
            myToast(it.title)
        }
    }

    override fun getSpecialAdapter(): BaseListAdapter<Data, DataItemView> {
        mainAdapter = MainAdapter()
        return mainAdapter
    }

    override fun getSpecialPresenter(): BaseListPresenter {
        return MainActivityPresenterImpl(this)
    }

    override fun getList(response: List<Data>?): List<Data>? {
        return response
    }

    override fun onDestroy() {
        super.onDestroy()
        //解绑presenter
        presenter.destoryView()
    }

}

MainAdapter.kt

import android.content.Context
import com.fly.demo01.base.BaseListAdapter
import com.fly.demo01.model.beans.Data
import com.fly.demo01.ui.widget.DataItemView

class MainAdapter: BaseListAdapter<Data, DataItemView>() {
    override fun refreshItemView(itemView: DataItemView, data: Data) {
        itemView.setData(data)
    }
    override fun getItemView(context: Context?): DataItemView {
        return DataItemView(context)
    }
}

DataItemView.kt

import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.RelativeLayout
import com.fly.demo01.R
import com.fly.demo01.model.beans.Data
import kotlinx.android.synthetic.main.item_data.view.*


/**
 * ClassName:DataItemView
 * Description:
 */
class DataItemView:RelativeLayout {
    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)

    /**
     * 初始化方法
     */
    init {
        View.inflate(context, R.layout.item_data,this)
    }

    /**
     * 刷新条目view数据
     */
    fun setData(data: Data) {
        title.setText(data.title)
    }
}

对应的item_data.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/selector_bg"
    android:orientation="vertical">

    <TextView
        android:id="@+id/title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:padding="20dp"
        android:text="你好"
        android:textColor="#000000"
        android:textSize="20sp" />
</LinearLayout>

LoadMoreView.kt

import android.content.Context
import android.util.AttributeSet
import android.view.View
import android.widget.RelativeLayout
import com.fly.demo01.R


/**
 * ClassName:LoadMoreView
 * Description:
 */
class LoadMoreView:RelativeLayout {
    constructor(context: Context?) : super(context)
    constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
    constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
    init {
        View.inflate(context, R.layout.view_loadmore,this)
    }
}

对应的布局代码view_loadmore.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:gravity="center"
              android:padding="10dp"
              android:layout_width="match_parent"
              android:layout_height="wrap_content">

    <ProgressBar
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
</LinearLayout>

ThreadUtil.kt:一个工具类,判断当前是否运行在主线程中

import android.os.Handler
import android.os.Looper

object ThreadUtil {
    val handler = Handler(Looper.getMainLooper());
    /**
     * 运行在主线程中
     */
    fun runOnMainThread(runnable:Runnable){
        handler.post(runnable)
    }

}

8.添加网络权限

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

存在的一些问题:比如在NetManager.kt类中,通过对访问成功的数据进行解析

这里就出现了一个问题,如果每个ListActivity或者是ListFragment需要访问的JSON数据格式不一样怎么办?json数据第一层的参数是“error”(没有“_code”),如果直接丢一个json字符串过去在V层解析,又达不到我们的初衷了,所以一定要和后台服务器的小伙伴们商量好接口的事情


代码下载:《Kotlin-Android开发之MVP模式+OkHttp3+RecyclerView下拉刷新和上拉加载更多框架封装代码


如有意见或者建议请跟帖,谢谢,各位晚安!!!

发布了117 篇原创文章 · 获赞 80 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/qq_32306361/article/details/104024080