Kotlin 中 BaseActivity 以及 MVP 封装

image.png

前言


  • 前面几章,和大家说了 DataBinding、Dagger2 在的配置,今天就说说在项目中如何使用吧,配合 MVP 模式对 BaseActivtiy 进行封装。(之前已经说过一遍了,是在 Java 平台下,传送门在此
  • KotlinTest Github

MVP


  • 那么,我们就从 MVP 开始。

  • 首先,项目中使用 Retrofit + RxJava2 进行网络请求,那么,我们在使用的时候就要考虑到 RxJava 的生命周期问题,如果不对它进行处理,那么在应用中进入一个 Activity、请求网络、在请求未完成返回这个流程中就会抛出异常,导致应用崩溃,于是,就有了这样的 BaseMVPPresenter。

      /**
       * Presenter基类
       *
       * @param V MVP View类型 继承[BaseMVPView]
       * @param M MVP Module 继承[BaseMVPModule]
       */
      open class BaseMVPPresenter<V : BaseMVPView, M : BaseMVPModule> {
    
          /** MVP View 对象  */
          protected var mView: V? = null
    
          /** MVP Module 对象  */
          @Inject
          protected lateinit var mModule: M
    
          /** RxJava2 生命周期管理  */
          private val disposables: CompositeDisposable = CompositeDisposable()
    
          /**
           * 界面绑定,关联 MVP View
           *
           * @param view MVP View
           */
          fun attach(view: V) {
              mView = view
          }
    
          /**
           * 解除绑定,去除 MVP View 引用
           */
          fun detach() {
              mView = null
          }
    
          /**
           * 检查请求返回数据,并在登录状态异常时弹出提示
           *
           * @param data 返回数据
           * @param T  返回数据类型
           *
           * @return 是否成功
           */
          protected fun <T : BaseEntity> checkResponse(data: T): Boolean {
              return data.code == Constants.ResponseCode.SUCCESS
          }
    
          /**
           * 将网络请求添加到 RxJava2 生命周期
           */
          protected fun addDisposable(dis: Disposable) {
              disposables.add(dis)
          }
    
          /**
           * 消费所有事件
           */
          fun dispose() {
              if (!disposables.isDisposed && disposables.size() > 0) {
                  disposables.dispose()
              }
          }
      }
    
  • 为了能够方便的进行复用,所以 BaseMVPPresenter 中使用泛型来确定 View 以及 Module 的类型。

  • 定义的 attach() 方法在 presenter 使用前调用,绑定 View,并在 Activity 结束时使用 detach() 方法接触绑定,移除引用,调用 dispose() 方法消费所有事件。

  • 在 Module 中进行网络请求以及其他耗时操作,所以有了 BaseMVPModule

      /**
       * MVP Module基类
       */
      open class BaseMVPModule @Inject constructor() {
          @Inject
          lateinit var netClient: NetApi
      }
    
  • 因为在所有 Module 中都会用到网络请求,所以将网络请求 API 在 BaseMVPModule 中声明。

  • 其中 NetApi 的依赖注入可以新建一个 NetModule

      /**
       * 网络模块依赖注入
       */
      @Module
      class NetModule {
          @Provides
          @Singleton
          fun netClient(): NetApi {
              val okHttpClient = OkHttpClient.Builder()
                      .addInterceptor(ParametersInterceptor())
                      .addInterceptor(LogInterceptor())
                      .build()
              val retrofit = Retrofit.Builder()
                      .baseUrl(UrlDefinition.BASE_URL)
                      .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
                      .addConverterFactory(GsonConverterFactory.create())
                      .client(okHttpClient)
                      .build()
              return retrofit.create(NetApi::class.java)
          }
      }
    
  • 这里的 @Singleton 注解表明使用单例模式,即 NetApi 对象是单例的。

  • 然后还需要将 NetModule 添加到 ApplicationSub 中

      /**
       * Application Dagger2 组件
       */
      @Singleton
      @Component(modules = arrayOf(
              ActivityModule::class,
              SupportFragmentModule::class,
              NetModule::class,
              AndroidSupportInjectionModule::class))
      interface ApplicationSub : AndroidInjector<MyApplication> {
          @Component.Builder
          abstract class Builder : AndroidInjector.Builder<MyApplication>()
      }
    
  • 注意:Module 中使用了 @Singleton 注解,ApplicationSub 中也要添加 @Singleton 注解。

  • 然后,就是 BaseMVPView

      /**
       * MVP View基类
       */
      interface BaseMVPView {
    
          /**
           * 网络请求结束
           */
          fun onNetFinished()
    
          /**
           * 网络故障
           */
          fun onNetError()
    
          /**
           * 无数据
           */
          fun onNoData()
    
          /**
           * 加载中
           */
          fun onLoading()
      }
    
  • 在 BaseMVPView 中,定义了一系列通用方法,即每个界面都会用到的方法。

BaseActivity


  • 上面说完了 MVP 模式相关的模块基类封装,现在说说 Activity 的基类封装。

  • 我们为什么要封装基类?当然是为了开发方便,防止重复的模版代码,那么首先就要把每个界面都需要的抽取出来,标题栏那是肯定的,还有加载数据的不同状态,即上面 BaseMVPView 中所定义的几个状态。把这些抽取成 layout_base.xml

      <LinearLayout
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical">
    
              <RelativeLayout
                  android:layout_width="match_parent"
                  android:layout_height="@dimen/dp_48">
    
                  标题栏
    
              </RelativeLayout>
    
              <RelativeLayout
                  android:layout_width="match_parent"
                  android:layout_height="match_parent">
    
                  <FrameLayout
                      android:id="@+id/fl_content"
                      android:layout_width="match_parent"
                      android:layout_height="match_parent"
                      android:orientation="vertical"/>
    
                  <LinearLayout
                      android:layout_width="match_parent"
                      android:layout_height="match_parent">
    
                      网络异常
    
                  </LinearLayout>
    
                  <LinearLayout
                      android:layout_width="match_parent"
                      android:layout_height="match_parent">
    
                      无数据
    
                  </LinearLayout>
    
                  <LinearLayout
                      android:layout_width="match_parent"
                      android:layout_height="match_parent">
    
                      加载中
    
                  </LinearLayout>
    
              </RelativeLayout>
    
      </LinearLayout>
    
  • 新建 BaseActivity 继承 DaggerAppCOmpatActivity (支持 Dagger2),使用泛型确定 DataBinding、MVPPresenter 类型。

      /**
       * Activity 基类
       */
      abstract class BaseActivity<P : BaseMVPPresenter<*, *>, DB : ViewDataBinding>
          : DaggerAppCompatActivity(),
              BaseMVPView,
              RootHandler.OnTitleClickListener {
    
          /** 当前界面 Context 对象*/
          protected lateinit var mContext: AppCompatActivity
    
          /** 当前界面 Presenter 对象 */
          @Inject
          protected lateinit var presenter: P
    
          /** 根布局 DataBinding 对象 */
          protected lateinit var rootBinding: LayoutBaseBinding
          /** 当前界面布局 DataBinding 对象 */
          protected lateinit var mBinding: DB
    
          /**
           * 重写 onCreate() 方法,添加了 Dagger2 注入、Activity 管理以及根布局等初始化操作
           */
          override fun onCreate(savedInstanceState: Bundle?) {
              super.onCreate(savedInstanceState)
    
              // 保存当前 Context 对象
              mContext = this
    
              // 添加到 AppManager 应用管理
              AppManager.addActivity(this)
    
              // 加载根布局,初始化 DataBinding
              rootBinding = DataBindingUtil.inflate(
                      LayoutInflater.from(mContext),
                      R.layout.layout_base, null, false
              )
              // 绑定事件处理
              rootBinding.handler = RootHandler(this)
          }
    
          /**
           * 重写 onDestroy() 方法,移除 Activity 管理以及 MVP 生命周期管理
           */
          override fun onDestroy() {
    
              // 从应用管理移除当前 Activity 对象
              AppManager.removeActivity(this)
      
              // 界面销毁时,消费所有事件,清空引用
              presenter.dispose()
              presenter.detach()
    
              super.onDestroy()
          }
    
          /**
           * 重写 setContentView(layoutResID) 方法,使其支持 DataBinding 以及标题栏、状态栏初始化操作
           */
          override fun setContentView(layoutResID: Int) {
    
              // 初始化标题栏
              initTitleBar()
    
              // 加载布局,初始化 DataBinding
              mBinding = DataBindingUtil.inflate(
                      LayoutInflater.from(mContext),
                      layoutResID, null, false
              )
    
              // 将当前布局添加到根布局
              rootBinding.flContent.removeAllViews()
              rootBinding.flContent.addView(mBinding.root)
    
              // 设置布局
              super.setContentView(rootBinding.root)
    
              // 初始化状态栏
              initStatusBar()
          }
    
          /**
           * 初始化标题栏,抽象方法,子类实现标题栏自定义
           */
          protected abstract fun initTitleBar()
    
          /**
           * 初始化状态栏,默认主题色、不透明,修改需重写
           */
          protected fun initStatusBar() {
              setStatusBar()
          }
    
          /**
           * 设置状态栏,默认主题色、不透明
           *
           * @param colorResId    状态栏颜色,默认主题色
           * @param alpha         状态栏透明度,默认不透明,取值范围 0~255
           */
          protected fun setStatusBar(@ColorRes colorResId: Int = R.color.colorTheme, alpha: Int = 0) {
              if (alpha !in 0..255) {
                  RuntimeException("The value of the alpha must between 0 and 255")
              } else {
                  StatusBarUtil.setResColor(this, colorResId, alpha)
              }
          }
    
          /**
           * 显示标题栏
           */
          protected fun showTitle() {
              rootBinding.handler?.showTitle = true
          }
    
          /**
           * 设置标题文本
           *
           * @param strResID 标题文本资源id
           */
          protected fun setTitleStr(@StringRes strResID: Int) {
              rootBinding.handler?.showTvTitle = true
              rootBinding.handler?.tvTitle = getString(strResID)
          }
    
          /**
           * 设置标题文本
           *
           * @param str      标题文本
           */
          protected fun setTitleStr(str: String) {
              rootBinding.handler?.showTvTitle = true
              rootBinding.handler?.tvTitle = str
          }
    
          /**
           * 设置标题栏左侧图标,默认返回按钮
           *
           * @param resID     标题栏左侧图标资源id,默认返回按钮
           */
          protected fun setIvLeft(@DrawableRes resID: Int = R.mipmap.arrow_left_white) {
              rootBinding.handler?.showIvLeft = true
              rootBinding.handler?.ivLeftResID = resID
          }
    
          /**
           * 设置右侧图标
           *
           * @param resID 图片资源id
           */
          protected fun setIvRight(@DrawableRes resID: Int) {
              rootBinding.handler?.showIvRight = true
              rootBinding.handler?.ivRightResID = resID
          }
    
          /**
           * 设置右侧文本
           *
           * @param strResID 文本资源id
           */
          protected fun setTvRight(@StringRes strResID: Int) {
              rootBinding.handler?.showTvRight = true
              rootBinding.handler?.tvRight = getString(strResID)
          }
    
          /**
           * 重写BaseMvpView中方法,网络异常时调用
           */
          override fun onNetError() {
              val handler = rootBinding.handler
              handler?.let {
                  if (handler.showNoData) {
                      handler.showNoData = false
                  }
                  if (handler.showLoading) {
                      val drawable = rootBinding.ivLoading.drawable
                      (drawable as? AnimationDrawable)?.stop()
                      handler.showLoading = false
                  }
                  if (!handler.showNetError) {
                      handler.showNetError = true
                  }
                  onListComplete()
              }
          }
    
          /**
           * 重写BaseMvpView中方法,无数据时调用
           */
          override fun onNoData() {
              val handler = rootBinding.handler
              handler?.let {
                  if (handler.showNetError) {
                      handler.showNetError = false
                  }
                  if (handler.showLoading) {
                      val drawable = rootBinding.ivLoading.drawable
                      (drawable as? AnimationDrawable)?.stop()
                      handler.showLoading = false
                  }
                  if (!handler.showNoData) {
                      handler.showNoData = true
                  }
                  onListComplete()
              }
          }
    
          /**
           * 重写BaseMvpView中方法,加载数据时调用
           */
          override fun onLoading() {
              val handler = rootBinding.handler
              handler?.let {
                  if (handler.showNetError) {
                      handler.showNetError = false
                  }
                  if (handler.showNoData) {
                      handler.showNoData = false
                  }
                  if (!handler.showLoading) {
                      val drawable = rootBinding.ivLoading.drawable
                      (drawable as? AnimationDrawable)?.start()
                      handler.showLoading = true
                  }
              }
          }
    
          /**
           * 重写BaseMvpView中方法,网络请求结束后调用,隐藏其他界面
           */
          override fun onNetFinished() {
              val handler = rootBinding.handler
              handler?.let {
                  if (handler.showNetError) {
                      handler.showNetError = false
                  }
                  if (handler.showNoData) {
                      handler.showNoData = false
                  }
                  if (handler.showLoading) {
                      val drawable = rootBinding.ivLoading.drawable
                      (drawable as? AnimationDrawable)?.stop()
                      handler.showLoading = false
                  }
                  onListComplete()
              }
          }
    
          /**
           * 使用SwipeToLoadView时重写,完成刷新步骤
           */
          protected fun onListComplete() {}
    
          /**
           * 标题栏左侧点击事件,默认结束当前界面
           */
          override fun onLeftClick() {
              finish()
          }
    
          /**
           * 标题栏右侧点击事件
           */
          override fun onRightClick() {}
    
          /**
           * 无数据界面点击事件,默认显示加载中
           */
          override fun onNoDataClick() {
              onLoading()
          }
    
          /**
           * 网络异常界面点击事件,默认显示加载中
           */
          override fun onNetErrorClick() {
              onLoading()
          }
      }
    
  • 就这样完成了 BaseActivity 的封装,使用起来也很简单:

      /**
       * 主界面
       */
      class MainActivity : BaseActivity<BlankPresenter, ActivityMainBinding>() {
    
          override fun onCreate(savedInstanceState: Bundle?) {
              super.onCreate(savedInstanceState)
              setContentView(R.layout.activity_main)
    
              val mFrags = ArrayList<Fragment>()
              mFrags.add(MoviesListFragment())
    
              mBinding.vp.adapter = FragVpAdapter.Builder()
                      .manager(supportFragmentManager)
                      .frags(mFrags)
                      .build()
    
          }
    
          override fun initTitleBar() {
              showTitle()
              setTitleStr("高评分电影")
          }
      }
    
  • 详细代码大家可以看我的 Github项目:KotlinTest

语法解析


  • 在上面的代码中有几个简单的 Kotlin 语法要和大家说明:

      // Java 中,如果类声明有泛型,而在使用时不需要泛型,那么直接不声明即可
      abstract class BaseActivity<P extends BaseMVPPresenter, DB extends ViewDataBinding> {}
      // 但是在 Kotlin 中必须使用 * 代替
      abstract class BaseActivity<P : BaseMVPPresenter<*,*>, DB : ViewDataBinding> {}
    
      /*
       * 在 DataBinding 中,Handler 的类型是可为空的,即 var handler: Handler? 
       * 在 Kotlin 中,调用可空类型的方法或属性必须使用安全调用 ?. 
       */
      handler?.showTvTitle = true // 等价于 if(null != handler) handler.showTvTitle = true
    
      /*
       * Kotlin 还提供了 let 函数,{}使用了 lambda 表达式, Kotlin 是默认支持的
       */
      handler?.let {
          dosomething...
      } 
      // 等价于
      if(null != handler) {
          dosomething
      }
    
      /*
       * Kotlin 中的类型转换使用 as 关键字,如果类型错误抛出异常
       * 同时 Kotlin 也提供了安全转换 as? ,转换成功返回该类型对象,转换失败返回 null
       */
      (drawable as? AnimationDrawable)?.start()
      // 等价于 Kotlin
      if(drawable is AnimationDrawable) { // 判断是否是该类型
          drawable.start() // 使用 is 关键字判断类型,如果为 true 则自动转换为该类型使用
      }
      // 等价于 Java
      if(drawable instanceof AnimationDrawable) {
          ((AnimationDrawable) drawable).start();
      }
    
      /* 
       * Kotlin 中有智能类型转换,即在变量声明时就赋值,可以省略声明时的类型,Kotlin 会根据赋的值确定变量的类型
       */
      var str = "" // 自动确定为 String 类型
      val handler  = mBinding.handler // 自动确定为 Handler 类型
    

最后


  • 到这里,Kotlin 下的项目框架封装就基本完成了,接下来会给大家带来详细的 Kotlin 语法解析,欢迎关注。
  • 有疑问的可以在评论区提问哦。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,681评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,710评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,623评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,202评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,232评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,368评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,795评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,461评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,647评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,476评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,525评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,226评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,785评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,857评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,090评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,647评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,215评论 2 341

推荐阅读更多精彩内容