Kotlin 中 BaseActivity 以及 MVP 封裝

image.png

前言


  • 前面幾章,和大家說了 DataBinding、Dagger2 在的配置,今天就說說在項目中如何使用吧,配合 MVP 模式對 BaseActivtiy 進行封裝。(之前已經(jīng)說過一遍了,是在 Java 平臺下,傳送門在此
  • KotlinTest Github

MVP


  • 那么,我們就從 MVP 開始。

  • 首先,項目中使用 Retrofit + RxJava2 進行網(wǎng)絡(luò)請求,那么,我們在使用的時候就要考慮到 RxJava 的生命周期問題,如果不對它進行處理,那么在應用中進入一個 Activity、請求網(wǎng)絡(luò)、在請求未完成返回這個流程中就會拋出異常,導致應用崩潰,于是,就有了這樣的 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()
    
          /**
           * 界面綁定,關(guān)聯(lián) MVP View
           *
           * @param view MVP View
           */
          fun attach(view: V) {
              mView = view
          }
    
          /**
           * 解除綁定,去除 MVP View 引用
           */
          fun detach() {
              mView = null
          }
    
          /**
           * 檢查請求返回數(shù)據(jù),并在登錄狀態(tài)異常時彈出提示
           *
           * @param data 返回數(shù)據(jù)
           * @param T  返回數(shù)據(jù)類型
           *
           * @return 是否成功
           */
          protected fun <T : BaseEntity> checkResponse(data: T): Boolean {
              return data.code == Constants.ResponseCode.SUCCESS
          }
    
          /**
           * 將網(wǎng)絡(luò)請求添加到 RxJava2 生命周期
           */
          protected fun addDisposable(dis: Disposable) {
              disposables.add(dis)
          }
    
          /**
           * 消費所有事件
           */
          fun dispose() {
              if (!disposables.isDisposed && disposables.size() > 0) {
                  disposables.dispose()
              }
          }
      }
    
  • 為了能夠方便的進行復用,所以 BaseMVPPresenter 中使用泛型來確定 View 以及 Module 的類型。

  • 定義的 attach() 方法在 presenter 使用前調(diào)用,綁定 View,并在 Activity 結(jié)束時使用 detach() 方法接觸綁定,移除引用,調(diào)用 dispose() 方法消費所有事件。

  • 在 Module 中進行網(wǎng)絡(luò)請求以及其他耗時操作,所以有了 BaseMVPModule

      /**
       * MVP Module基類
       */
      open class BaseMVPModule @Inject constructor() {
          @Inject
          lateinit var netClient: NetApi
      }
    
  • 因為在所有 Module 中都會用到網(wǎng)絡(luò)請求,所以將網(wǎng)絡(luò)請求 API 在 BaseMVPModule 中聲明。

  • 其中 NetApi 的依賴注入可以新建一個 NetModule

      /**
       * 網(wǎng)絡(luò)模塊依賴注入
       */
      @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 {
    
          /**
           * 網(wǎng)絡(luò)請求結(jié)束
           */
          fun onNetFinished()
    
          /**
           * 網(wǎng)絡(luò)故障
           */
          fun onNetError()
    
          /**
           * 無數(shù)據(jù)
           */
          fun onNoData()
    
          /**
           * 加載中
           */
          fun onLoading()
      }
    
  • 在 BaseMVPView 中,定義了一系列通用方法,即每個界面都會用到的方法。

BaseActivity


  • 上面說完了 MVP 模式相關(guān)的模塊基類封裝,現(xiàn)在說說 Activity 的基類封裝。

  • 我們?yōu)槭裁匆庋b基類?當然是為了開發(fā)方便,防止重復的模版代碼,那么首先就要把每個界面都需要的抽取出來,標題欄那是肯定的,還有加載數(shù)據(jù)的不同狀態(tài),即上面 BaseMVPView 中所定義的幾個狀態(tài)。把這些抽取成 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">
    
                      網(wǎng)絡(luò)異常
    
                  </LinearLayout>
    
                  <LinearLayout
                      android:layout_width="match_parent"
                      android:layout_height="match_parent">
    
                      無數(shù)據(jù)
    
                  </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 以及標題欄、狀態(tài)欄初始化操作
           */
          override fun setContentView(layoutResID: Int) {
    
              // 初始化標題欄
              initTitleBar()
    
              // 加載布局,初始化 DataBinding
              mBinding = DataBindingUtil.inflate(
                      LayoutInflater.from(mContext),
                      layoutResID, null, false
              )
    
              // 將當前布局添加到根布局
              rootBinding.flContent.removeAllViews()
              rootBinding.flContent.addView(mBinding.root)
    
              // 設(shè)置布局
              super.setContentView(rootBinding.root)
    
              // 初始化狀態(tài)欄
              initStatusBar()
          }
    
          /**
           * 初始化標題欄,抽象方法,子類實現(xiàn)標題欄自定義
           */
          protected abstract fun initTitleBar()
    
          /**
           * 初始化狀態(tài)欄,默認主題色、不透明,修改需重寫
           */
          protected fun initStatusBar() {
              setStatusBar()
          }
    
          /**
           * 設(shè)置狀態(tài)欄,默認主題色、不透明
           *
           * @param colorResId    狀態(tài)欄顏色,默認主題色
           * @param alpha         狀態(tài)欄透明度,默認不透明,取值范圍 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
          }
    
          /**
           * 設(shè)置標題文本
           *
           * @param strResID 標題文本資源id
           */
          protected fun setTitleStr(@StringRes strResID: Int) {
              rootBinding.handler?.showTvTitle = true
              rootBinding.handler?.tvTitle = getString(strResID)
          }
    
          /**
           * 設(shè)置標題文本
           *
           * @param str      標題文本
           */
          protected fun setTitleStr(str: String) {
              rootBinding.handler?.showTvTitle = true
              rootBinding.handler?.tvTitle = str
          }
    
          /**
           * 設(shè)置標題欄左側(cè)圖標,默認返回按鈕
           *
           * @param resID     標題欄左側(cè)圖標資源id,默認返回按鈕
           */
          protected fun setIvLeft(@DrawableRes resID: Int = R.mipmap.arrow_left_white) {
              rootBinding.handler?.showIvLeft = true
              rootBinding.handler?.ivLeftResID = resID
          }
    
          /**
           * 設(shè)置右側(cè)圖標
           *
           * @param resID 圖片資源id
           */
          protected fun setIvRight(@DrawableRes resID: Int) {
              rootBinding.handler?.showIvRight = true
              rootBinding.handler?.ivRightResID = resID
          }
    
          /**
           * 設(shè)置右側(cè)文本
           *
           * @param strResID 文本資源id
           */
          protected fun setTvRight(@StringRes strResID: Int) {
              rootBinding.handler?.showTvRight = true
              rootBinding.handler?.tvRight = getString(strResID)
          }
    
          /**
           * 重寫B(tài)aseMvpView中方法,網(wǎng)絡(luò)異常時調(diào)用
           */
          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()
              }
          }
    
          /**
           * 重寫B(tài)aseMvpView中方法,無數(shù)據(jù)時調(diào)用
           */
          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()
              }
          }
    
          /**
           * 重寫B(tài)aseMvpView中方法,加載數(shù)據(jù)時調(diào)用
           */
          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
                  }
              }
          }
    
          /**
           * 重寫B(tài)aseMvpView中方法,網(wǎng)絡(luò)請求結(jié)束后調(diào)用,隱藏其他界面
           */
          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() {}
    
          /**
           * 標題欄左側(cè)點擊事件,默認結(jié)束當前界面
           */
          override fun onLeftClick() {
              finish()
          }
    
          /**
           * 標題欄右側(cè)點擊事件
           */
          override fun onRightClick() {}
    
          /**
           * 無數(shù)據(jù)界面點擊事件,默認顯示加載中
           */
          override fun onNoDataClick() {
              onLoading()
          }
    
          /**
           * 網(wǎng)絡(luò)異常界面點擊事件,默認顯示加載中
           */
          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 中,調(diào)用可空類型的方法或?qū)傩员仨毷褂冒踩{(diào)用 ?. 
       */
      handler?.showTvTitle = true // 等價于 if(null != handler) handler.showTvTitle = true
    
      /*
       * Kotlin 還提供了 let 函數(shù),{}使用了 lambda 表達式, Kotlin 是默認支持的
       */
      handler?.let {
          dosomething...
      } 
      // 等價于
      if(null != handler) {
          dosomething
      }
    
      /*
       * Kotlin 中的類型轉(zhuǎn)換使用 as 關(guān)鍵字,如果類型錯誤拋出異常
       * 同時 Kotlin 也提供了安全轉(zhuǎn)換 as? ,轉(zhuǎn)換成功返回該類型對象,轉(zhuǎn)換失敗返回 null
       */
      (drawable as? AnimationDrawable)?.start()
      // 等價于 Kotlin
      if(drawable is AnimationDrawable) { // 判斷是否是該類型
          drawable.start() // 使用 is 關(guān)鍵字判斷類型,如果為 true 則自動轉(zhuǎn)換為該類型使用
      }
      // 等價于 Java
      if(drawable instanceof AnimationDrawable) {
          ((AnimationDrawable) drawable).start();
      }
    
      /* 
       * Kotlin 中有智能類型轉(zhuǎn)換,即在變量聲明時就賦值,可以省略聲明時的類型,Kotlin 會根據(jù)賦的值確定變量的類型
       */
      var str = "" // 自動確定為 String 類型
      val handler  = mBinding.handler // 自動確定為 Handler 類型
    

最后


  • 到這里,Kotlin 下的項目框架封裝就基本完成了,接下來會給大家?guī)碓敿毜?Kotlin 語法解析,歡迎關(guān)注。
  • 有疑問的可以在評論區(qū)提問哦。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容