前言:没有万能的框架,也没有万能的模版,我们需要做的就是根据需求选对框架,然后进行修改,最终达到自己的目的。相信你通过《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下拉刷新和上拉加载更多框架封装代码》
如有意见或者建议请跟帖,谢谢,各位晚安!!!