咱们在开发Android应用程序的时候其实会有不少通用的代码,比方说很常见的页面的几种基本状态的切换:正常、加载失败、加载中、空页面。又或者是下拉刷新和若是数据须要分页而带来的上拉加载更多数据等等操做。固然,这其中最繁琐的仍是关于MVP相关模板代码的编写,熟悉Android中MVP架构的小伙伴们应该都知道,严格按照MVP架构的话,咱们每个Activity或者Fragment都须要多写一个接口和两个实现类:MVPContract、MVPModel和MVPPresenter。而这些Contract、Model和Presenter又不近类似,因此在我以前的开发中,若是一个新的APP有30个页面,那么加上这些MVP架构所需的代码,我须要多添加90个文件,即便是复制粘贴这些代码当时也耗费了我将近2个多小时的时间(固然不只仅是复制,还包括文件名,方法名称的修改等等所需的细节)。固然,这也是促使我开源出KCommon这个使用Kotlin编写的,基于MVP架构的极速开发框架的主要缘由。前端
allprojects {
repositories {
//添加这一行依赖
maven { url "https://jitpack.io" }
}
}
复制代码
CommonLibrary.instance.initLibrary(this,
BuildConfig.APP_URL,
ApiService::class.java,
CacheService::class.java,
spName = "KCommonDemo",
errorHandleMap = hashMapOf<Int, (exception: IApiException) -> Unit>(401 to { exception ->
}, 402 to { exception ->
}, 403 to { exception ->
}),
isDebug = BuildConfig.DEBUG)
复制代码
若是只是针对不一样的模块进行介绍的话,可能不是那么容易理解,这里我结合一个Kotlin编写的Demo,来一步一步详细演示如何使用这个极速开发框架。java
首先咱们这个APP的需求很明确,要有统一的网络错误处理、页面的不一样状态切换、下拉刷新和上拉加载更多、处理网络请求时的Loading效果、在无网络时加载缓存数据,和使用MVP架构来编写代码。在这里我使用Kotlin编写整个APP的代码,对Kotlin不熟悉的同窗也不用惧怕,Java和Kotlin的写法基本是一致的,而且个人MVP模板文件也提供了Kotlin和Java两个版本的选项。react
这两步在集成方法中已经介绍过了。android
因为KCommon为了方便开发依赖了不少开发中经常使用的第三方库,完整的依赖以下所示:git
dependencies {
api fileTree(include: ['*.jar'], dir: 'libs')
api 'com.android.support:appcompat-v7:27.1.1'
api 'com.android.support:recyclerview-v7:27.1.1'
api 'org.jetbrains.anko:anko:0.10.3'
api 'androidx.core:core-ktx:0.3'
api 'com.android.support:multidex:1.0.3'
api 'com.squareup.okhttp3:okhttp:3.10.0'
api 'com.squareup.okhttp3:logging-interceptor:3.9.1'
api 'com.squareup.retrofit2:retrofit:2.4.0'
api 'com.squareup.retrofit2:converter-gson:2.4.0'
api 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'
api 'io.reactivex.rxjava2:rxandroid:2.0.1'
api 'com.github.VictorAlbertos.RxCache:runtime:1.8.3-2.x'
api 'io.reactivex.rxjava2:rxjava:2.1.7'
api 'com.github.VictorAlbertos.Jolyglot:gson:0.0.4'
api 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
api 'com.tbruyelle.rxpermissions2:rxpermissions:0.9.4@aar'
api 'org.greenrobot:eventbus:3.0.0'
api 'com.github.CymChad:BaseRecyclerViewAdapterHelper:2.9.35'
api 'com.github.Kennyc1012:MultiStateView:1.3.0'
api 'com.github.ybq:Android-SpinKit:1.1.0'
api 'com.blankj:utilcode:1.17.1'
api 'com.github.bumptech.glide:glide:3.8.0'
api 'com.github.anzaizai:EasySwipeMenuLayout:1.1.2'
api 'com.trello.rxlifecycle2:rxlifecycle:2.2.1'
api 'com.trello.rxlifecycle2:rxlifecycle-components:2.2.1'
api 'com.trello.rxlifecycle2:rxlifecycle-kotlin:2.2.1'
api 'com.trello.rxlifecycle2:rxlifecycle-android-lifecycle-kotlin:2.2.1'
api 'org.jetbrains.kotlin:kotlin-stdlib:1.2.51'
api 'com.android.support:cardview-v7:27.1.1'
api 'com.hx.multi-image-selector:multi-image-selector:1.2.1'
api 'com.android.support:design:27.1.1'
}
复制代码
因此方法数基本上是要超过65535的,所以须要配置MultiDex:github
android {
compileSdkVersion 27
buildToolsVersion '27.0.3'
defaultConfig {
minSdkVersion 19
targetSdkVersion 27
versionCode 1
versionName "1.0"
//在这里配置multiDex
multiDexEnabled true
}
}
复制代码
因为开发中须要配合KCommonTemplate一键生成相关MVP代码使用,因此对整个项目的目录结构有着要求(若是项目目录不正确的话,一键生成的模板代码文件的位置会错位)。后端
首先在项目的主包名下建立4个平级的package:app 、common 、mvp 、ui 。api
根据名称你们也很好理解,app 包中存放咱们自定义的Application,common 包中存放一些通用的基础代码,好比常量、数据类、网络访问接口类等等,mvp 包中存放咱们MVP架构所需的组件类,ui 包中存放咱们的Activity、Fragment和Adapter等等与界面相关的类。缓存
上面这些目录结构都是在一个新的项目开发前必须建立好的。有的同窗可能看到个人Demo中在一些目录中也添加了别的一些包,好比在 common 包下建立了 constant 、 util 、 entity 等等包。其实除了以前提到的必须的目录结构,你在Demo中看到的别的包都是可选的,这个随你,只不过这些都是我我的的开发习惯。我习惯在 common 包下存放我项目中的常量、工具类、和数据实体类,同理,我也喜欢在 ui 包下存放adapter和自定义view。固然,这些都是经验之谈,我推荐你跟我采用相同的结构。咱们最后来看一张图片有个更明确的概念。 bash
class App : Application() {
companion object {
fun startLoginActivity(context: Context, loginClazz: Class<*>) {
CommonLibrary.instance.headerMap = hashMapOf(
"token" to SPUtils.getInstance("KCommonDemo").getString("token", "123"))
context.startActivity(
Intent(
context,
loginClazz).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK))
}
}
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
MultiDex.install(this)
}
override fun onCreate() {
super.onCreate()
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this)
BlockCanary.install(this, AppBlockCanaryContext()).start()
CommonLibrary.instance.initLibrary(this,
BuildConfig.APP_URL,
ApiService::class.java,
CacheService::class.java,
spName = "KCommonDemo",
errorHandleMap = hashMapOf<Int, (exception: IApiException) -> Unit>(401 to { exception ->
}, 402 to { exception ->
}, 403 to { exception ->
}),
isDebug = BuildConfig.DEBUG)
}
}
复制代码
上面是Demo中自定义的Application,首先要重写这个方法,处理Multidex:
//Multidex
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
MultiDex.install(this)
}
复制代码
而后再onCreate方法中初始化咱们的 CommonLibrary :
CommonLibrary.instance.initLibrary(this,
BuildConfig.APP_URL,
ApiService::class.java,
CacheService::class.java,
spName = "KCommonDemo",
errorHandleMap = hashMapOf<Int, (exception: IApiException) -> Unit>(401 to { exception ->
}, 402 to { exception ->
}, 403 to { exception ->
}),
isDebug = BuildConfig.DEBUG)
复制代码
这里传入的第一个参数是Application自己,第二个参数是BaseUrl,以后是咱们以前提到的APIService和CacheService,以后传入了一个spName,这个表示SharedPrefrence的文件名称,以后是errorHandleMap,这存放了根据不一样的网络错误码对应的回调,最后传入一个isDebug表示debug环境下会开启网络日志输入,release环境下会关闭网络日志输出。下面是详细说明:
/**
* 初始化
* @param context Application
* @param baseUrl retrofit所需的baseUrl
* @param apiClass retrofit使用的ApisService::Class.java
* @param cacheClass rxcache使用的CacheService::Class.java
* @param spName Sharedpreference文件名称
* @param isDebug 是debug环境仍是release环境。debug环境有网络请求的日志,release反之
* @param startPage 分页列表的起始页,有多是0,或者是2,这个看后台
* @param pageSize 分页大小
* @param headerMap 网络请求头的map集合,便于在网络请求添加统一的请求头,好比token之类
* @param errorHandleMap 错误处理的map集合,便于针对相关网络请求返回的错误码来作相应的处理,好比错误码401,token失效须要从新登陆
* @param onPageCreateListener 对应页面activity或fragment相关生命周期的回调,便于在页面相关时机作一些统一处理,好比加入友盟统计须要在全部页面的相关生命周期加入一些处理
* @param onPageDestroyListener 对应页面activity或fragment相关生命周期的回调,便于在页面相关时机作一些统一处理,好比加入友盟统计须要在全部页面的相关生命周期加入一些处理
* @param onPageResumeListener 对应页面activity或fragment相关生命周期的回调,便于在页面相关时机作一些统一处理,好比加入友盟统计须要在全部页面的相关生命周期加入一些处理
* @param onPagePauseListener 对应页面activity或fragment相关生命周期的回调,便于在页面相关时机作一些统一处理,好比加入友盟统计须要在全部页面的相关生命周期加入一些处理
*
*/
fun initLibrary(
context: Application,
baseUrl: String,
apiClass: Class<*>,
cacheClass: Class<*>,
spName: String = "kcommon",
isDebug: Boolean = true,
startPage: Int = 1,
pageSize: Int = 20,
headerMap: Map<String, String>? = null,
errorHandleMap: Map<Int, (exception: IApiException) -> Unit>? = null,
onPageCreateListener: OnPageCreateListener? = null,
onPageDestroyListener: OnPageDestroyListener? = null,
onPageResumeListener: OnPageResumeListener? = null,
onPagePauseListener: OnPagePauseListener? = null)
复制代码
固然这些参数中前4个参数都是必须的,由于很明显嘛,它们都没有默认值。其他的参数若是有须要的话是能够按需配置的。
若是是跟着Demo一块儿看的话,有的小伙伴可能会对APP这段代码中的:
companion object {
fun startLoginActivity(context: Context, loginClazz: Class<*>) {
CommonLibrary.instance.headerMap = hashMapOf(
"token" to SPUtils.getInstance("KCommonDemo").getString("token", "123"))
context.startActivity(
Intent(
context,
loginClazz).addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK))
}
}
复制代码
感到疑惑,这里实际上是一个伴生对象,能够理解为Java中的静态方法,主要是方便当token过时的时候跳转到登陆页面并将以前网络请求中的token这个请求头清空。固然,在咱们正在开发的这个Demo中是没有用到的,这里只是提供一种token过时,跳转登陆页的思路。
在 common 包下建立 entity 数据类包,以后再 entity 包下建立 net 网络数据类包,在 net 中建立 DataEntity 这个数据文件。
这个数据文件以下所示:
//最外层数据类
data class HttpResultEntity<T>(
private var code: Int = 0,
private var message: String = "",
private var error: Boolean = false,
private var results: T) : IHttpResultEntity<T> {
override val isSuccess: Boolean
get() = !error
override val errorCode: Int
get() = code
override val errorMessage: String
get() = message
override val result: T
get() = results
}
data class DataItem(
@SerializedName("desc") var desc: String = "",
@SerializedName("ganhuo_id") var ganhuoId: String = "",
@SerializedName("publishedAt") var publishedAt: String = "",
@SerializedName("readability") var readability: String = "",
@SerializedName("type") var type: String = "",
@SerializedName("url") var url: String = "",
@SerializedName("who") var who: String = ""
)
复制代码
我我的习惯将全部的数据类都写到一个文件中,由于数据类都是很简单的,所有写在一个文件中看起来比较清晰,也便于管理。
DataEntity 中有两个数据类,第一个是咱们网络返回数据的最外层数据类,能够看到,它实现了一个 KCommon 库中定义的一个接口 IHttpResultEntity ,咱们来看一下这个接口:
interface IHttpResultEntity<T> {
//网络请求结果是否成功
val isSuccess: Boolean
//错误码
val errorCode: Int
//错误信息
val errorMessage: String
//返回的有效数据
val result: T
}
复制代码
这个接口的意思其实很明显了,注释写的很清楚。那么有些还没进入公司的小伙伴们可能会有问题了:为何必需要添加一个实现了 IHttpResultEntity 接口的数据类呢?
咱们先来看一下公司开发中实际返回的数据结构:
{"data":null,"code":200,"message":null,"success":true}
复制代码
能够看到返回的数据包含4个部分,正好对应着接口中的4个部分。大多数的公司都会返回相似的数据结构,固然有些会字段名称不同,也有一些会缺一些字段,这时候咱们应该灵活应变。
第二个 DataItem 是 GankApi 返回的数据结构,比方说下面就是一条数据:
{
"desc": "\u8fd8\u5728\u7528ListView\uff1f",
"ganhuo_id": "57334c9d67765903fb61c418",
"publishedAt": "2016-05-12T12:04:43.857000",
"readability": "",
"type": "Android",
"url": "http://www.jianshu.com/p/a92955be0a3e",
"who": "\u9648\u5b87\u660e"
}
复制代码
咱们日常写MVP架构的代码,虽然总体页面逻辑看起来很是清晰:Model只管理数据的获取、Presenter管理数据和页面的交互逻辑、View只处理ui相关的事件。但这个清晰是有代价的,文章开篇已经提到过了:咱们要多写3个文件 -> Contract 、 Model 、 Presenter 。对,,没错,一个Activity或者Fragment就要多写三个文件,在我短短的开发生涯中,除此以外我还遇到过更过度的,说到这里,可能有的小伙伴要跟我想到一块去了:没错,就是 Dagger2 ,这个东西首先理解起来有些费劲,其次就是一个Activity或者Fragment也是要多配置2个文件: Component 和 Module 。相信使用过 Dagger2 的朋友都懂我在说什么,那么问题来了,我只想写一个页面,但却要多写5个文件,这简直不可忍受( Dagger2 我已经在新开发的项目中移除了,并且之后也不打算再使用,缘由嘛很简单:使用很繁琐,并且基本上没什么好的效果,本质上就是把new对象的代码放在了别处。若是没有用过 Dagger2 的同窗,请你听我一句劝:珍惜生命,远离 Dagger2 )。
那回到咱们的主题,咱们如今要建立一个主页面。这个主页面要有如下几点功能:
要建立这样一个页面咱们有这么几个步骤:
接下来就是见证奇迹的时刻,你会发现你的mvp包中生成了相关MVP的代码,而且在Activity中默认会有一些配置代码,而且XML文件中也生成了便于切换页面Loading、成功、失败、空布局的代码,简而言之,一键生成了你所需的一切。
接下来咱们来看一下这些生成的文件。
interface MainContract {
interface IMainModel
interface IMainPresenter : IBasePresenter
interface IMainView : IBaseView<Any?>
}
复制代码
这是一个主页面的 Contract 接口,包含了咱们 mvp 的三个接口,因为咱们在 MainActivity 中并不作关于网络请求相关的操做,因此咱们并无修改这个接口中的任何代码。
class MainModel : BaseModel<ApiService, CacheService>(), MainContract.IMainModel
复制代码
能够看到咱们的 MainModel 继承了 BaseModel 而且实现了咱们 MainContract 中的 MainContract.IMainModel 。因为并不涉及网络数据的获取,因此并无任何内容。
class MainPresenter(iMainView: MainContract.IMainView) :
BasePresenter<MainContract.IMainModel, MainContract.IMainView>(iMainView),
MainContract.IMainPresenter {
override val model: MainContract.IMainModel
get() = MainModel()
override fun initData(dataMap: Map<String, String>?) {
}
}
复制代码
能够看到生成的 MainPresenter 继承了 BasePresenter 而且实现了咱们 MainContract 中的 MainContract.IMainPresenter 。同时还持有了一个 MainModel 的引用对象 model ,这个主要是方便咱们在presenter中调用model中的方法获取网络数据。
还有一个 initData(dataMap: Map<String, String>?) 方法,这个方法从名称也能够理解,页面加载数据的方法,须要传入一个 Map 类型的参数。为何要传一个 Map 对象呢?主要缘由仍是在实际开发中咱们请求网络接口所需的参数个数是不肯定的,可能不须要传参数,比方说退出登陆的接口实际上是不须要传参的;固然也可能传个数不一样的参数,比方说你有一个根据条件查询数据的接口,然而这些条件并非必须的,能够有,也能够不传,这个时候针对参数个数不一样的问题,咱们须要一个数据结构来知足咱们的需求,而 Map 这个数据结构正好知足咱们的需求。
一样的状况,因为在 MainActivity 中咱们并不处理网络,因此代码是不须要修改的。
class MainActivity : BaseActivity<ApiService, CacheService, MainPresenter, Any?>(),
MainContract.IMainView {
private val AVATAR_URL = "https://avatars2.githubusercontent.com/u/17843145?s=400&u=d417a5a50d47426c0f0b6b9ff64d626a36bf0955&v=4"
private val ABOUT_ME_URL = "https://github.com/BlackFlagBin"
private val READ_ME_URL = "https://github.com/BlackFlagBin/KCommonProject/blob/master/README.md"
private val MORE_PROJECT_URL = "https://github.com/BlackFlagBin?tab=repositories"
private val mTypeArray: Array<String> by lazy {
arrayOf("all", "Android", "iOS", "休息视频", "福利", "拓展资源", "前端", "瞎推荐", "App")
}
override val swipeRefreshView: SwipeRefreshLayout?
get() = null
override val multiStateView: MultiStateView?
get() = null
override val layoutResId: Int
get() = R.layout.activity_main
override val presenter: MainPresenter
get() = MainPresenter(this)
override fun initView() {
super.initView()
setupSlidingView()
setupViewPager()
rl_right.onClick {
startActivity(
WebActivity::class.java, bundleOf("url" to ABOUT_ME_URL, "title" to "关于做者"))
}
ll_read_me.onClick {
startActivity(
WebActivity::class.java, bundleOf("url" to READ_ME_URL, "title" to "ReadMe"))
}
ll_more_project.onClick {
startActivity(
WebActivity::class.java, bundleOf("url" to MORE_PROJECT_URL, "title" to "更多项目"))
}
ll_clear_cache.onClick { clearCache() }
}
override fun initData() {
}
override fun showContentView(data: Any?) {
}
private fun setupSlidingView() {
val slidingRootNav = SlidingRootNavBuilder(this).withToolbarMenuToggle(
tb_main).withMenuOpened(
false).withContentClickableWhenMenuOpened(false).withMenuLayout(
R.layout.menu_main_drawer).inject()
ll_menu_root.onClick { slidingRootNav.closeMenu(true) }
Glide.with(this).load(
AVATAR_URL).placeholder(
R.mipmap.avatar).error(R.mipmap.avatar).dontAnimate().transform(
GlideCircleTransform(
this)).into(iv_user_avatar)
}
private fun setupViewPager() {
vp_content.adapter = MainPagerAdapter(supportFragmentManager)
tl_type.setupWithViewPager(vp_content)
vp_content.offscreenPageLimit = mTypeArray.size - 1
}
private fun clearCache() {
val cache = CacheUtils.getInstance(cacheDir)
val cacheSize = Formatter.formatFileSize(
this, cache.cacheSize)
cache.clear()
toast("清除缓存$cacheSize")
}
}
复制代码
咱们须要关注的是带 override 部分的成员和方法:
override val swipeRefreshView: SwipeRefreshLayout?
get() = null
复制代码
若是须要下拉刷新,须要在布局XML文件中加入 SwipeRefreshView 并将这个View赋值给它。咱们的 MainActivity 不须要下拉刷新,因此默认是 null 。
override val multiStateView: MultiStateView?
get() = null
复制代码
这是负责页面Loading、成功、失败、空布局切换的一个自定义View, 须要在布局文件中加入 并赋值给它。额,实际上由于模板生成的布局文件中会默认带有 MultiStateView ,因此其实不须要咱们主动在布局文件中加入。由于 MainActivity 并无网络数据的加载,不须要切换页面状态,因此赋值为 null 。
override val layoutResId: Int
get() = R.layout.activity_main
override val presenter: MainPresenter
get() = MainPresenter(this)
复制代码
这两个放在一块儿,前者是布局文件的 id ,后者是咱们当前页面的 presenter ,都是模板自动生成的,没什么可多说的。
override fun initView() {
super.initView()
setupSlidingView()
setupViewPager()
rl_right.onClick {
startActivity(
WebActivity::class.java, bundleOf("url" to ABOUT_ME_URL, "title" to "关于做者"))
}
ll_read_me.onClick {
startActivity(
WebActivity::class.java, bundleOf("url" to READ_ME_URL, "title" to "ReadMe"))
}
ll_more_project.onClick {
startActivity(
WebActivity::class.java, bundleOf("url" to MORE_PROJECT_URL, "title" to "更多项目"))
}
ll_clear_cache.onClick { clearCache() }
}
复制代码
从名字能够看出来,初始化界面布局,全部关于页面 不须要网络数据 的UI的初始化代码推荐放在这里。
override fun initData() {
}
复制代码
很明显了,在 initData 中咱们推荐的是加载网络数据,一般会调用 mPresenter.initData(mDataMap)
来实现咱们网络数据的加载。但 MainActivity 不须要加载网络数据,因此咱们保持空置。
override fun showContentView(data: Any?) {
}
复制代码
这个方法的调用时机是在咱们请求网络接口返回正确的数据以后,因此在这个方法中咱们能够获取所需的网络数据,并修改UI。这里一样由于 MainActivity 不须要加载网络数据,因此咱们保持空置。
<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.kennyc.view.MultiStateView
android:id="@+id/multi_state_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:msv_emptyView="@layout/layout_empty"
app:msv_errorView="@layout/layout_error"
app:msv_loadingView="@layout/layout_loading"
app:msv_viewState="content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/white"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="3dp"
android:orientation="vertical">
<android.support.v7.widget.Toolbar
android:id="@+id/tb_main"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@android:color/transparent"
android:elevation="10dp">
<RelativeLayout
android:id="@+id/rl_right"
android:layout_width="50dp"
android:layout_height="match_parent"
android:layout_gravity="right"
android:gravity="center">
<ImageView
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@mipmap/about_me"/>
</RelativeLayout>
</android.support.v7.widget.Toolbar>
</LinearLayout>
<android.support.design.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.AppBarLayout
android:id="@+id/appbar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.design.widget.CollapsingToolbarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="0dp"
app:layout_scrollFlags="scroll|enterAlways|snap">
<android.support.design.widget.TabLayout
android:id="@+id/tl_type"
android:layout_width="match_parent"
android:layout_height="48dp"
android:layout_gravity="center_horizontal"
android:background="@color/white"
android:elevation="1dp"
app:layout_collapseMode="parallax"
app:layout_collapseParallaxMultiplier="0.1"
app:tabGravity="center"
app:tabIndicatorHeight="0dp"
app:tabMode="scrollable"
app:tabSelectedTextColor="@color/colorPrimary"
app:tabTextColor="@color/gray_text">
</android.support.design.widget.TabLayout>
</android.support.design.widget.CollapsingToolbarLayout>
</android.support.design.widget.AppBarLayout>
<android.support.v4.view.ViewPager
android:id="@+id/vp_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
</android.support.v4.view.ViewPager>
</android.support.design.widget.CoordinatorLayout>
</LinearLayout>
</com.kennyc.view.MultiStateView>
</LinearLayout>
</android.support.design.widget.CoordinatorLayout>
复制代码
值得注意的是 MultiStateView 中的这几个属性:
app:msv_emptyView="@layout/layout_empty"
app:msv_errorView="@layout/layout_error"
app:msv_loadingView="@layout/layout_loading"
app:msv_viewState="content"
复制代码
须要咱们传入咱们本身编写的空布局、错误布局、加载中布局和当前页面的状态,固然你能够直接使用我写的默认布局。页面的状态有4种 content 、loading 、error 和 empty ,你须要当前页面的初始状态是什么,就改变 msv_viewState 这个属性值便可。通常来讲若是须要加载网络数据,初始状态应该是 loading ,若是不须要加载网络数据,就像咱们的 MainActivity 同样的话,初始状态应该是 content 。
因为这个App采用的是ViewPager+Fragment的UI结构,因此咱们还须要建立一个 MainPageFragment 。和建立 MainActivity 的时候几乎是如出一辙的,惟一的区别在于以选择一键生成的选项不是 Kotlin MVP Activity 而是 Kotlin RefreshAndLoadMore MVP Fragment ,以下图所示:
由于 MainPageFragment 须要下拉刷新和上拉加载更多,因此咱们建立的是 Kotlin RefreshAndLoadMore MVP Fragment 而不是 Kotlin MVP Fragment 。这里须要注意一点的是有的同窗会建立成 MainFragment ,结构发现fragment的MVP代码覆盖了 MainActivity 的MVP代码,因此在建立Activity或者Fragment的时候应该避免两者前面的名字重复,不然后者的MVP代码会覆盖前者。
和一键生成 MainActivity 时候同样,这时候项目目录中也会出现 MainPageContract 、 MainPageModel 、 MainPagePresenter 和咱们的 MainPageFragment 。跟以前的流程同样,咱们来看一下这些生成的代码:
interface MainPageContract {
interface IMainPageModel {
fun getData(type: String, pageNo: Int, limit: Int): Observable<Optional<List<DataItem>>>
}
interface IMainPagePresenter : IBaseRefreshAndLoadMorePresenter
interface IMainPageView : IBaseRefreshAndLoadMoreView<List<DataItem>>
}
复制代码
跟以前的 MainContract 相似,区别在于咱们的页面是带 RefreshAndLoadMore 的,因此能够看到 presenter 接口继承了 IBaseRefreshAndLoadMorePresenter ,而 view 接口继承了 IBaseRefreshAndLoadMoreView 。值得注意的是 ** IBaseRefreshAndLoadMoreView <List<DataItem>> ** 带有一个泛型 ** List<DataItem> ** ,这个类型与咱们的 override fun showContentView(data: List<DataItem> ) 中参数的类型对应。
一样要注意的点是在 IMainPageModel 这个接口中咱们定义了一个 getData(type: String, pageNo: Int, limit: Int) 的方法用于获取分页数据。要强调的是网络接口实际返回的是 **List<DataItem> ** 类型的数据,但这个 getData 的返回值咱们须要在外面包上一层 Optional。这个 Optional 是 KCommon 库中定义的一个类,其实很简单:
/**
* Created by blackflagbin on 2018/3/29.
* 解决rxjava2不能处理null的问题,咱们把全部返回的有效数据包一层Optional,经过isEmpty判断是否为空
*/
data class Optional<T>(var data: T) {
fun isEmpty(): Boolean {
return data == null
}
}
复制代码
这其实就是在网络返回的原始数据上包了一层,那么有的人会不理解了,为何要包这一层,这不是画蛇添足么?
使用过 RxJava 的同窗应该很清楚,在咱们实际开发中,网络接口间的顺序调用是一个很常见的事情。比方说我在首页想展现一个用户当前小区下的通知公告,那其实确定要有两个接口,获取用户当前小区的接口、根据小区id获取通知公告的接口。这两个接口之间存在着前后的逻辑关系,必须先拿到小区id,才能获取通知公告。
一般来讲,用过 RxJava 的同窗会使用 flatMap 这个操做符,即便是用户没有小区,接口返回的小区数据为 null ,这在 RxJava1 的时候是不会存在任何问题的。然而,在新的 RxJava2 中,一旦接口返回的是 null ,而你又使用了 flatMap ,那么很抱歉,程序会报错,缘由就是在 RxJava2 中不支持 null 值事件的传递。
其实咱们可让后台不给咱们返回 null 来避免这个问题,但每每在实际开发中后台为了图省事也是不会处理这种事情的,并且最多见的理由就是为何IOS能够返回 null,Android就不行?
因此为了不跟后端的同窗过多的撕逼,咱们只能在返回的真实数据上包上一层 Optional ,来处理 RxJava2 中没法传递 null 事件的问题。这也是我目前能够想到的最好的处理方案,若是你们有更好的处理办法,不妨经过留言告诉我,咱们共同窗习,共同进步。
class MainPageModel : BaseModel<ApiService, CacheService>(), MainPageContract.IMainPageModel {
override fun getData(type: String, pageNo: Int, limit: Int): Observable<Optional<List<DataItem>>> {
return if (NetworkUtils.isConnected()) {
mCacheService.getMainDataList(
mApiService.getMainDataList(
type, limit, pageNo).compose(DefaultTransformer()),
DynamicKeyGroup(type, pageNo),
EvictDynamicKeyGroup(true)).subscribeOn(Schedulers.io()).observeOn(
AndroidSchedulers.mainThread())
} else {
mCacheService.getMainDataList(
mApiService.getMainDataList(
type, limit, pageNo).compose(DefaultTransformer()),
DynamicKeyGroup(type, pageNo),
EvictDynamicKeyGroup(false)).subscribeOn(Schedulers.io()).observeOn(
AndroidSchedulers.mainThread())
}
}
}
复制代码
在咱们的 MainPageModel 中,实现了 getData 这个在 IMainPageModel 中定义的接口方法。由于页面的逻辑是网络正常时获取网络数据,无网络时加载缓存数据,因此方法中会有关于网络状态的判断。咱们须要注意的是两个变量: mApiService 和 mCacheService 。这两个变量都是 BaseModel 中的成员变量,咱们在 BaseModel 的继承类中均可以直接拿来使用。关于网络请求和缓存我使用的是 Retrofit 和 RxCache ,若是有不太了解的同窗能够自行查看相关的文档,我这里就再也不赘述了。这里会有小伙伴问:若是我不须要缓存怎么办?若是不须要缓存就更好办了,这个方法直接返回
mApiService.getMainDataList(
type, limit, pageNo).compose(DefaultTransformer())
复制代码
就能够了。
有的同窗可能还会有疑问,为何请求接口后面要加上 compose(DefaultTransformer())
这么一句,可不能够省略?其实这一句是 KCommon 中网络处理的关键部分,这句代码对咱们网络请求返回的结果作了相应的错误处理和 Optional 的包装,因此这一句是必不可少的。对它的实现原理感兴趣的同窗能够直接经过 Android Studio 查看源码,原理并不繁琐。
class MainPagePresenter(iMainPageView: MainPageContract.IMainPageView) :
BasePresenter<MainPageContract.IMainPageModel, MainPageContract.IMainPageView>(iMainPageView),
MainPageContract.IMainPagePresenter {
override val model: MainPageContract.IMainPageModel
get() = MainPageModel()
override fun initData(dataMap: Map<String, String>?) {
initData(dataMap, CommonLibrary.instance.startPage)
}
override fun initData(dataMap: Map<String, String>?, pageNo: Int) {
if (!NetworkUtils.isConnected()) {
mView.showTip("网络已断开,当前数据为缓存数据")
}
if (pageNo == CommonLibrary.instance.startPage) {
//若是请求的是分页的首页,必须先调用这个方法
mView.beforeInitData()
mModel.getData(
dataMap!!["type"].toString(),
pageNo,
CommonLibrary.instance.pageSize).bindToLifecycle(mLifecycleProvider).subscribeWith(
NoProgressObserver(mView, object : ObserverCallBack<Optional<List<DataItem>>> {
override fun onNext(t: Optional<List<DataItem>>) {
mView.showSuccessView(t.data)
mView.dismissLoading()
}
override fun onError(e: Throwable) {
mView.showErrorView("")
mView.dismissLoading()
}
}))
} else {
mModel.getData(
dataMap!!["type"].toString(),
pageNo,
CommonLibrary.instance.pageSize).bindToLifecycle(mLifecycleProvider).subscribeWith(
NoProgressObserver(
mView, mIsLoadMore = true))
}
}
}
复制代码
能够看到和 MainPresenter 的结构大同小异,区别在于多了 override fun initData(dataMap: Map<String, String>?, pageNo: Int)
这个方法,很明显这是加载分页用的。要注意我这个方法中代码的写法,须要注意的有这么几个点:
mView.beforeInitData()
在请求分页的首页时,必须先调用这行代码进行数据的整理。以后再去使用 mModel 中的方法请求网络。
.bindToLifecycle(mLifecycleProvider)
这句的目的是将网络请求和当前页面(Activity或Fragment)的生命周期绑定,当页面结束的时候会终止网络请求,防止内存泄漏,使用的是 RxLifeCycle ,有兴趣的同窗能够自行研究。个人建议是全部的 presenter 中的网络请求调用中都要加上这一句,防止内存泄漏。
NoProgressObserver
因为整个网络框架使用的是 Rxjava+Retrofit+OKHttp ,因此最终是须要一个 Observer 来最终处理咱们网络请求返回的数据。在 KCommon 中内置了两种 Observer :NoProgressObserver 和 ProgressObserver 。从名字也能够明白,分别是没有加载动画的和有加载动画的 Observer 。那么两者的使用时机分别是什么呢?简而言之就是当你请求一个网络时页面须要有Loading动画的显示时使用 NoProgressObserver ,请求网络时不须要Loading动画时使用 ProgressObserver 。
mIsLoadMore = true
这是 NoProgressObserver 和 ProgressObserver 中都存在的一个默认参数,默认为 false ,意思是 当前网络请求是不是加载更多的请求 。当咱们加载非首页的时候将之置为 true 。
mView.showSuccessView(t.data)
当网络请求成功时必须调用。做用是将当前页面从Loading状态切换到成功的状态。
mView.showErrorView("网络链接异常")
当网络请求失败时必须调用。做用是将当前页面从Loading状态切换到失败的状态。传入的参数咱们能够自定义错误的缘由,这个随便写。
mView.dismissLoading()
这个主要是带有下拉刷新的页面中当获取到网络数据(不管成功或者失败)后,必须调用。
@SuppressLint("ValidFragment")
class MainPageFragment() :
BaseRefreshAndLoadMoreFragment<ApiService, CacheService, MainPageContract.IMainPagePresenter, List<DataItem>>(),
MainPageContract.IMainPageView {
private val mTypeArray: Array<String> by lazy {
arrayOf("all", "Android", "iOS", "休息视频", "福利", "拓展资源", "前端", "瞎推荐", "App")
}
private lateinit var mType: String
override val adapter: BaseQuickAdapter<*, *>?
get() = MainPageAdapter(arrayListOf())
override val recyclerView: RecyclerView?
get() = rv_list
override val layoutManager: RecyclerView.LayoutManager?
get() = FixedLinearLayoutManager(activity)
override val swipeRefreshView: SwipeRefreshLayout?
get() = swipe_refresh
override val multiStateView: MultiStateView?
get() = multi_state_view
override val layoutResId: Int
get() = R.layout.fragment_main_page
override val presenter: MainPageContract.IMainPagePresenter
get() = MainPagePresenter(this)
constructor(position: Int) : this() {
mType = mTypeArray[position]
}
override fun initData() {
mDataMap["type"] = mType
mPresenter.initData(mDataMap)
}
override fun showContentView(data: List<DataItem>) {
mAdapter?.onItemClickListener = BaseQuickAdapter.OnItemClickListener { adapter, view, position ->
startActivity(
WebActivity::class.java,
bundleOf(
"url" to (mAdapter?.data!![position] as DataItem).url,
"title" to (mAdapter?.data!![position] as DataItem).desc))
}
}
}
复制代码
能够看到,和 MainActivity 类似处不少,我这里着重说明一下不一样的地方:
override val adapter: BaseQuickAdapter<*, *>?
get() = MainPageAdapter(arrayListOf())
复制代码
由于要有上拉加载更多的列表,因此很明显须要一个 Adapter 。 KCommon 中依赖了 BaseRecyclerViewAdapterHelper 这个第三方库,我日常用起来挺方便的,并且功能很强大,在这里也推荐你们使用。这个三方库的具体使用方法我就不赘述了,你们能够自行去 GitHub 上查看它的文档。
override val recyclerView: RecyclerView?
get() = rv_list
复制代码
不用多说了吧,须要一个 RecyclerView 对象。
override val layoutManager: RecyclerView.LayoutManager?
get() = FixedLinearLayoutManager(activity)
复制代码
你们确定都知道要使用 LayoutManager ,但这里必须使用我在 KCommon 中定义的几个带 Fixed 打头的 LayoutManager ,这样会避免一些诡异的异常。
override fun initData() {
mDataMap["type"] = mType
mPresenter.initData(mDataMap)
}
复制代码
在 initData 中咱们首先将类型参数存放进了 mDataMap 中,而后调用了 mPresenter.initData(mDataMap) 进行了网络请求。
没错,只须要配置这么几个参数,咱们的一个带有下拉刷新和上拉加载更多的页面就完成了。相信作过相似功能页面的同窗应该很清楚要实现一样的功能若是所有本身写的话会很繁琐,但使用了 KCommon ,一切都会变得很是容易。
到此为止,经过一个简单Demo的讲解, KCommon 基础的用法已经所有介绍完毕了,除了我上面说的以外,在 KCommon 中还集成依赖了一些我我的在实际项目开发中常常用到的,并且很是好用的第三方库,这里你们有兴趣的话能够尝试了解一下。
最后要说的是我会长期维护和改进 KCommon ,若是你们在使用的过程当中存在疑惑,能够在 GitHub 上提出 issue ,我会一一解答。感谢你们花时间看这么一篇文章,若是个人努力解决了你们实际开发中的问题,提升了你们的效率,但愿能够顺手给个 star ,谢谢。