Android MVP + Retrofit 封装代码

作为已经当了两年半的Android 程序员,说到底也是时候好好总结一下自己写的代码了! 早些时候,Android 项目普遍使用MVC 的架构模式,起初还不觉得什么,但等到业务逻辑越来越多的时候,就会发现在Activity 和 Fragment 中写了越来越多的业务代码,不管是UI渲染,还是请求数据,以及各种耦合的逻辑,动不动一个类上千行!对此,作为高严谨的程序员自然是不想再看到这样的场景,所以开始了项目大刀阔斧的重构,目前比较流行的架构是MVP 和 MVVM ,这篇博客中,我们先来了解熟悉 MVP 与 Retrofit 的结合交互代码!

MVP 框架其实是由三个单词的简称组成 ,分别是 Model – View – Presenter ,当你的项目中用来许多带有Presenter 后缀结尾的类时,很显然,你的项目正是采用这种框架的架构。Model 指的就是数据,View 指的是视图(Activity 和 Fragment)用作UI 渲染 , 而Presenter 则就是将之前MVC 中数据与UI 的交互从中抽取出来单独放到另一个类中作处理,用来减轻之前View 中过多冗余的代码!下面我就一步一步的分析贴上代码讲解这一框架的好处与实现!


第一步 :构建基础IBaseView接口

package com.example.icxnote.base

/**
 * Created by cx on 2020-01-20
 * Describe:
 */
interface IBaseView {

    fun showLoading()

    fun dismissLoading()

}

因为这是基础的View接口,我这里只写了两个方法,显示加载,以及隐藏加载连个方法,具体的可以拓展


第二步 :构建基础的IPresenter 接口

package com.example.icxnote.base

/**
 * Created by cx on 2020-01-20
 * Describe:
 */
interface IPresenter<in V : IBaseView> {

    fun attachView(mRootView: V)

    fun detachView()
}

因为Presenter 其实是View 与 数据的交互逻辑,所以其中得包含View 的引用,这里的attachView() 方法,就是将View 通过参数引用的方式绑定到Presenter 上,而detachView() 方法则是解除绑定,用于销毁View对象


第三步 :构建BasePresenter 类并且实现IPresenter 接口,另外还需将其与RxJava 中的订阅者结合到一起

package com.example.icxnote.base

import io.reactivex.disposables.CompositeDisposable
import io.reactivex.disposables.Disposable
import java.lang.RuntimeException

/**
 * Created by cx on 2020-01-20
 * Describe:
 */
open class BasePresenter<T : IBaseView> : IPresenter<T> {

    var mRootView: T? = null
    private var compositeDisposable = CompositeDisposable()

    override fun attachView(mRootView: T) {
        this.mRootView = mRootView
    }

    override fun detachView() {
        mRootView = null

        //保证activity结束时取消所有正在执行的订阅,也就是取消所有正在运行的线程请求网络
        if (!compositeDisposable.isDisposed) {
            compositeDisposable.clear()
        }
    }

    private val isViewAttached: Boolean
        get() = mRootView != null

    fun checkViewAttached() {
        if (!isViewAttached) throw MvpViewNotAttachedException()
    }

    //添加订阅
    fun addSubscription(disposable: Disposable){
        compositeDisposable.add(disposable)
    }

    //请在调用Presenter 请求数据之前先将View依附于Presenter
    private class MvpViewNotAttachedException internal constructor() : RuntimeException(
        "Please call IPresenter.attachView(IBaseView) before requesting data to the IPresenter"
    )
}

这里有一处需注意的是,在detachView() 方法中,除了需要把View 置为null , 还需调用compositeDisposable.clear() ,将所有正在执行的订阅给清除掉,以防内存泄漏


第四步 :构建好基础之后,就得开始将业务拆分然后具体实现了,我这里的业务需求是请求网络数据,头部是banner , 然后下面是可以下拉刷新加载更多数据的RecyclerView 列表 ,如下图所示:
在这里插入图片描述
很显然,这里我们的View 的逻辑是设置第一次请求的数据,以及加载更多的数据时进行显示,而Presenter 的逻辑则是去请求首页第一次的数据,以及上拉加载时,加载更多数据,业务逻辑清晰后,就可以去定义接口中的方法了!

package com.example.icxnote.mvp.contact

import com.example.icxnote.base.IBaseView
import com.example.icxnote.base.IPresenter
import com.example.icxnote.mvp.model.bean.HomeBean

/**
 * Created by cx on 2020-01-20
 * Describe:
 */
interface HomeContact {

    interface View : IBaseView {

        /**
         * 设置第一次请求的数据
         */
        fun setHomeData(homeBean: HomeBean)

        /**
         * 设置加载更多的数据
         */
        fun setMoreData(itemList: ArrayList<HomeBean.Issue.Item>)

        /**
         * 显示错误信息
         */
        fun showError(msg: String, errorCode: Int)
    }

    interface Presenter : IPresenter<View> {

        /**
         * 获取首页精选数据
         */
        fun requestHomeData(num: Int)

        /**
         * 加载更多数据
         */
        fun loadMoreData()
    }

}

这里我再提一句,当View中的逻辑方法数很少时,建议把View的接口方法跟Presenter的接口方法放到一个XXXContact的类中管理,有助于减少更多的文件类,但是当View中比较多时,则建议还是分开写两个类比较直观一点!


第五步:开始写Model 层的逻辑代码 ,也就是网络请求数据

package com.example.icxnote.mvp.model

import com.example.icxnote.mvp.model.bean.HomeBean
import com.example.icxnote.net.RetrofitManager
import com.example.icxnote.rx.SchedulerUtils
import io.reactivex.Observable
import retrofit2.Retrofit

/**
 * Created by cx on 2020-01-20
 * Describe:
 */

class HomeModel{

    /**
     * 获取首页数据
     */
    fun requestHomeData(num:Int) : Observable<HomeBean>{
        return RetrofitManager.service.getFirstHomeData(num)
            .compose(SchedulerUtils.ioToMain())

    }

    /**
     * 加载更多数据
     */
    fun loadMoreData(url:String) : Observable<HomeBean>{
        return RetrofitManager.service.getMoreHomeData(url)
            .compose(SchedulerUtils.ioToMain())
    }
}


很显然,这里我直接就用自己封装的RetrofitManager 去请求网络了,RetrofitManager 类的代码如下:

package com.example.icxnote.net

import com.example.icxnote.MyApplication
import com.example.icxnote.api.ApiService
import com.example.icxnote.api.UrlConstant
import com.example.icxnote.mvp.contact.HomeContact
import com.example.icxnote.util.AppUtils
import com.example.icxnote.util.NetWorkUtil
import com.example.icxnote.util.Preference
import okhttp3.*
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import java.io.File
import java.util.concurrent.TimeUnit

/**
 * Created by cx on 2020-01-20
 * Describe:
 */
object RetrofitManager {

    val service: ApiService by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
        getRetrofit().create(ApiService::class.java)
    }

    private var token: String by Preference("token", "")

    /**
     * 设置公共参数
     */
    private fun addQueryParamterInterceptor(): Interceptor {
        return Interceptor { chain ->
            val originalResult = chain.request()
            val modifiedUrl = originalResult.url().newBuilder()
                .addQueryParameter("udid", "d2807c895f0348a180148c9dfa6f2feeac0781b5")
                .addQueryParameter("deviceModel", AppUtils.getMobileModel())
                .build()
            val requestBuilder = originalResult.newBuilder()
                .url(modifiedUrl)
            val request = requestBuilder.build()
            chain.proceed(request)
        }
    }

    /**
     * 设置头
     */
    private fun addHeaderInterceptor(): Interceptor {
        return Interceptor { chain ->
            val originRequest = chain.request()
            val requestBuilder = originRequest.newBuilder()
                .header("token", token)
                .method(originRequest.method(), originRequest.body())
            val request = requestBuilder.build()
            chain.proceed(request)
        }
    }

    /**
     * 设置缓存
     */
    private fun addCacheInterceptor(): Interceptor {
        return Interceptor { chain ->
            var request = chain.request()
            if (!NetWorkUtil.isNetworkAvailable(MyApplication.context)) {
                request = request.newBuilder()
                    .cacheControl(CacheControl.FORCE_CACHE)
                    .build()
            }
            val response = chain.proceed(request)
            if (NetWorkUtil.isNetworkAvailable(MyApplication.context)) {
                val maxAge = 0
                // 有网络时 设置缓存超时时间0个小时,意思就是不读取缓存数据,只对get有用,post没有缓冲
                response.newBuilder()
                    .header("Cache-Control", "public,max-age=" + maxAge)
                    .removeHeader("Retrofit")//清楚头信息,因为服务器如果不支持,会返回一些干扰信息,不清除下面无法生效
                    .build()
            } else {
                // 无网络时 设置超时为4周 只对get有用,post没有缓冲
                val maxStale = 60 * 60 * 24 * 28
                response.newBuilder()
                    .header("Cache-Control", "public, only-if-cached, max-stale=" + maxStale)
                    .removeHeader("nyn")
                    .build()
            }
            response
        }
    }

    /**
     * 设置log拦截
     */
    private fun addHttpLogInterceptor(): Interceptor {
        // 添加一个log拦截器,打印所有的log
        val httpLoggingInterceptor = HttpLoggingInterceptor()
        // 可以设置请求过滤的程度,body,basic,headers
        httpLoggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
        return httpLoggingInterceptor
    }


    /**
     * 获取Retrofit的实例
     */
    private fun getRetrofit(): Retrofit {
        // 获取retrofit的实例
        return Retrofit.Builder()
            .baseUrl(UrlConstant.BASE_URL)
            .client(getOkHttpClient())
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    /**
     * 获取okHttpClient的实例
     */
    private fun getOkHttpClient(): OkHttpClient {
        // 设置请求的缓存的大小跟位置
        val cacheFile = File(MyApplication.context.cacheDir, "cache")
        // 50Mb 缓存的大小
        val cache = Cache(cacheFile, 1024 * 1024 * 50)
        return OkHttpClient.Builder()
            .addInterceptor(addQueryParamterInterceptor()) //参数添加
            .addInterceptor(addHeaderInterceptor()) //添加头信息
            //.addInterceptor(addCacheInterceptor()) //缓存
            .addInterceptor(addHttpLogInterceptor()) //日志,所有的请求响应都看到
            .cache(cache) //添加缓存
            .connectTimeout(60, TimeUnit.SECONDS) //60s
            .readTimeout(60L, TimeUnit.SECONDS)
            .writeTimeout(60L, TimeUnit.SECONDS)
            .build()
    }
}

Retrofit 框架采用注解的方式,将要请求的url的baseUrl 和 请求参数进行拆分 , 里面也构建了一个ApiService 的实例 , 然后结合OkHttpClient3 去请求网络取数据,不熟悉的可以自行百度了解,这里就不详细介绍了,下面是ApiService 类的代码 :

package com.example.icxnote.api

import com.example.icxnote.mvp.model.bean.HomeBean
import io.reactivex.Observable
import retrofit2.http.GET
import retrofit2.http.Query
import retrofit2.http.Url

/**
 * Created by cx on 2020-01-20
 * Describe:
 */
interface ApiService {

    /**
     * 首页精选
     */
    @GET("v2/feed?")
    fun getFirstHomeData(@Query("num") num: Int): Observable<HomeBean>

    /**
     * 根据 nextPageUrl 请求下一页的数据
     */
    @GET
    fun getMoreHomeData(@Url url: String): Observable<HomeBean>


}

ApiService 类就是用来封装网络请求的接口方法的接口类

package com.example.icxnote.api

/**
 * Created by cx on 2020-01-20
 * Describe:
 */
object UrlConstant {
    const val BASE_URL = "http://baobab.kaiyanapp.com/api/"
}

这是请求的具体URL的base部分


第六步: 开始写Presenter 层的代码逻辑,首先构建一个XXXPresenter 的类

package com.example.icxnote.mvp.presenter

import com.example.icxnote.base.BasePresenter
import com.example.icxnote.mvp.contact.HomeContact
import com.example.icxnote.mvp.model.HomeModel
import com.example.icxnote.mvp.model.bean.HomeBean
import com.example.icxnote.net.exception.ExceptionHandle

/**
 * Created by cx on 2020-01-20
 * Describe:
 */
class HomePresenter : BasePresenter<HomeContact.View>(), HomeContact.Presenter {

    private var bannerHomeBean: HomeBean? = null
    private var nextPageUrl: String? = null
    private val homeModel: HomeModel by lazy { HomeModel() }

    /**
     * 获取首页数据
     */
    override fun requestHomeData(num: Int) {
        //检测是否绑定View
        checkViewAttached()
        mRootView?.showLoading()
        val disposable = homeModel.requestHomeData(num)
            .flatMap { homeBean ->
                val bannerItemList = homeBean.issueList[0].itemList
                //过滤掉 Banner2(包含广告,等不需要的type
                bannerItemList.filter { item ->
                    item.type == "banner2" || item.type == "horizontalScrollCard" || item.type == "textHeader"
                }.forEach { item ->
                    bannerItemList.remove(item)
                }

                bannerHomeBean = homeBean
                homeModel.loadMoreData(homeBean.nextPageUrl)
            }
            .subscribe { homeBean ->
                mRootView?.apply {
                    dismissLoading()
                    nextPageUrl = homeBean.nextPageUrl
                    //过滤掉 Banner2(包含广告,等不需要的type
                    val newBannerItemList = homeBean.issueList[0].itemList
                    newBannerItemList.filter { item ->
                        item.type == "banner2" || item.type == "horizontalScrollCard" || item.type == "textHeader"
                    }.forEach { item ->
                        newBannerItemList.remove(item)
                    }

                    //重新赋值Banner长度
                    bannerHomeBean!!.issueList[0].count =
                        bannerHomeBean!!.issueList[0].itemList.size
                    //赋值过滤后的数据 + banner 数据
                    bannerHomeBean?.issueList!![0].itemList.addAll(newBannerItemList)
                    setHomeData(bannerHomeBean!!)
                }
            }
        
        if (disposable != null) {
            addSubscription(disposable)

        }
    }

    /**
     * 加载更多
     */
    override fun loadMoreData() {
        val disposable = nextPageUrl?.let {
            homeModel.loadMoreData(it)
                .subscribe({ homeBean ->
                    mRootView?.apply {
                        val newItemList = homeBean.issueList[0].itemList
                        newItemList.filter { item ->
                            item.type == "banner2" || item.type == "horizontalScrollCard" || item.type == "textHeader"
                        }.forEach { item ->
                            // 移除item
                            newItemList.remove(item)
                        }

                        nextPageUrl = homeBean.nextPageUrl
                        setMoreData(newItemList)
                    }
                }, { t ->
                    mRootView?.apply {
                        showError(ExceptionHandle.handleException(t), ExceptionHandle.errorCode)
                    }

                })
        }

        if (disposable != null) {
            addSubscription(disposable)
        }

    }
}

看到这里估计有些人就有疑惑了,没事,别急接下来让我来为大家讲解 ,首先是HomePresenter继承BasePresenter<HomeContact.View> 该类,然后再实现HomeContact.Presenter 的接口,目的很简单,就是将View 和 Model 关联起来。其次,用HomeModel 去调取接口取网络数据,添加到RxJava的任务订阅中,取到后的数据直接调用View 中的setHomeData() 和 loadMoreData() 方法设置上去!


最后让我们再看看UI中的逻辑 XXXActivity 或 XXXFragment :

package com.example.icxnote.ui.fragment

import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.core.app.ActivityOptionsCompat
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.icxnote.R
import com.example.icxnote.base.BaseFragment
import com.example.icxnote.mvp.contact.HomeContact
import com.example.icxnote.mvp.model.bean.HomeBean
import com.example.icxnote.mvp.presenter.HomePresenter
import com.example.icxnote.net.exception.ErrorStatus
import com.example.icxnote.ui.activity.SearchActivity
import com.example.icxnote.ui.adapter.HomeAdapter
import com.example.icxnote.util.StatusBarUtil
import com.example.icxnote.util.showToast
import com.example.icxnote.view.MultipleStatusView
import com.orhanobut.logger.Logger
import com.scwang.smartrefresh.header.MaterialHeader
import java.text.SimpleDateFormat
import java.util.*
import kotlin.collections.ArrayList
import kotlinx.android.synthetic.main.fragment_home.*

/**
 * Created by cx on 2020-01-20
 * Describe:
 */

class HomeFragment : BaseFragment(), HomeContact.View {


    private var mTitle: String? = ""
    private val mPresenter: HomePresenter by lazy {
        HomePresenter()
    }

    private var mHomeAdapter : HomeAdapter ?= null

    // 是否刷新
    private var isRefresh = false
    // 请求的page页
    private var num: Int = 1

    private var loadingMore = false

    private var mMaterialHeader: MaterialHeader ?= null

    companion object {
        fun getInstance(title: String): HomeFragment {
            val fragment = HomeFragment()
            val bundle = Bundle()
            fragment.arguments = bundle
            fragment.mTitle = title
            return fragment
        }
    }

    private val linearLayoutManager by lazy {
        LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
    }

    private val simpleDateFormat by lazy {
        SimpleDateFormat("- MMM. dd , 'Brunch' -", Locale.ENGLISH)
    }

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

    override fun initView() {
        mPresenter.attachView(this)
        //内容跟随偏移
        mRefreshLayout.setEnableHeaderTranslationContent(true)
        mRefreshLayout.setOnRefreshListener {
            isRefresh = true
            mPresenter.requestHomeData(num)
        }


        mMaterialHeader = mRefreshLayout.refreshHeader as MaterialHeader?
        //打开下拉刷新区域块背景:
        mMaterialHeader?.setShowBezierWave(true)
        //设置下拉刷新主题颜色
        mRefreshLayout.setPrimaryColorsId(R.color.color_light_black, R.color.color_title_bg)


        mRecyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
                super.onScrollStateChanged(recyclerView, newState)
                if (newState == RecyclerView.SCROLL_STATE_IDLE) {
                    val childCount = mRecyclerView.childCount
                    val itemCount = mRecyclerView.layoutManager?.itemCount
                    val firstVisibleItem = (mRecyclerView.layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
                    if (firstVisibleItem + childCount == itemCount) {
                        if (!loadingMore) {
                            loadingMore = true
                            mPresenter.loadMoreData()
                        }
                    }
                }
            }

            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                super.onScrolled(recyclerView!!, dx, dy)
                val currentVisibleItemPosition = linearLayoutManager.findFirstVisibleItemPosition()
                if (currentVisibleItemPosition == 0) {
                    //背景设置为透明
                    toolbar.setBackgroundColor(getColor(R.color.color_translucent))
                    iv_search.setImageResource(R.mipmap.ic_action_search_white)
                    tv_header_title.text = ""
                } else {
                    if (mHomeAdapter?.mData!!.size > 1) {
                        toolbar.setBackgroundColor(getColor(R.color.color_title_bg))
                        iv_search.setImageResource(R.mipmap.ic_action_search_black)
                        val itemList = mHomeAdapter!!.mData
                        val item = itemList[currentVisibleItemPosition + mHomeAdapter!!.bannerItemSize - 1]
                        if (item.type == "textHeader") {
                            tv_header_title.text = item.data?.text
                        } else {
                            tv_header_title.text = simpleDateFormat.format(item.data?.date)
                        }
                    }
                }
            }

        })

        iv_search.setOnClickListener { openSearchActivity() }

        mLayoutStatusView = multipleStatusView

        //状态栏透明和间距处理
        activity?.let { StatusBarUtil.darkMode(it) }
        activity?.let { StatusBarUtil.setPaddingSmart(it, toolbar) }
        mLayoutStatusView = multipleStatusView

    }

    /**
     * 打开搜索页面
     */
    private fun openSearchActivity() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            val options = activity?.let { ActivityOptionsCompat.makeSceneTransitionAnimation(it, iv_search, iv_search.transitionName) }
            startActivity(Intent(activity, SearchActivity::class.java), options?.toBundle())
        } else {
            startActivity(Intent(activity, SearchActivity::class.java))
        }
    }

    override fun initData() {
        mPresenter.requestHomeData(num)
    }

    override fun setListener() {
    }

    override fun setHomeData(homeBean: HomeBean) {
        mLayoutStatusView?.showContent()
        Logger.d(homeBean)

        //初始化Adapter
        mHomeAdapter = activity?.let {
            HomeAdapter(it , homeBean.issueList[0].itemList)
        }
        mHomeAdapter?.setBannerSize(homeBean.issueList[0].count)
        mRecyclerView.adapter = mHomeAdapter
        mRecyclerView.layoutManager = linearLayoutManager
        mRecyclerView.itemAnimator = DefaultItemAnimator()
    }

    override fun setMoreData(itemList: ArrayList<HomeBean.Issue.Item>) {
        loadingMore = false
        mHomeAdapter?.addItemData(itemList)
    }

    /**
     * 显示错误信息
     */
    override fun showError(msg: String, errorCode: Int) {
        showToast(msg)
        if (errorCode == ErrorStatus.NETWORK_ERROR) {
            mLayoutStatusView?.showNoNetwork()
        } else {
            mLayoutStatusView?.showError()
        }
    }

    override fun showLoading() {
        if (!isRefresh) {
            isRefresh = false
            mLayoutStatusView?.showLoading()
        }
    }

    override fun dismissLoading() {
        mRefreshLayout.finishRefresh()
    }

    override fun onDestroy() {
        super.onDestroy()
        mPresenter.detachView()
    }

    fun getColor(colorId: Int): Int {
        return resources.getColor(colorId)
    }

}

抛开其他与数据无关的UI逻辑,我们直接可以看到HomeFragment 是实现了HomeContact.View 接口的,然后我们看到初始化了Presenter 类:

    private val mPresenter: HomePresenter by lazy {
        HomePresenter()
    }

然后在initView() 中,mPresenter.attachView(thjs) , 将Fragment 或Activity 通过参数引用的方式,封装到Presenter中,然后最后在onDestroy() 方法中调用mPresenter.detachView() 方法来销毁该View对象

接下来看重写的setHomeData() 和 setMoreData() 两个方法,当时在presenter 类中通过model 去请求网络得到的数据,正是通过这两个方法赋值给UI 的,所以在UI中只需要实现HomeContact.View中的方法就行,然后在其中等待回传的数据即可做UI操作了!


好了,讲到这里,大家应该心里或多或少有点对Android MVP框架自己的理解了吧,那我在这里最后总结一句,其实就是先定义View 和 Presenter 的接口方法,然后将view 通过参数传递传入 presenter 中,然后在presenter中去调用model 中请求数据的方法,异步的化则使用RxJava 即可,请求到数据后,再设置到UI中实现View接口中的那些方法上,最后UI执行刷新逻辑,这样就把原本在UI中的数据请求和渲染视图的工作巧妙的拆开了,减轻了UI的代码负担!!!

发布了22 篇原创文章 · 获赞 15 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/m0_37094131/article/details/104071220