一个轻量级的JetPackMVVMLight

文章共2301个字,预计阅读需约13分钟

一个引入了优质开源库的框架

retrofit2
网络请求库

glide
图片加载库

BaseRecyclerViewAdapterHelper
adapter库

permissionsdispatcher-ktx
配合kotlin使用的权限请求库

mmkv
使用腾讯的mmkv替代SharePreference的高性能key-value组件

smartRefresh
上拉加载下拉刷新库

shimmerlayout
*度高,方便使用的骨骼加载

liveEvent
消息总线,基于LiveData,具有生命周期感知能力

UnPeek-LiveData
解决LiveData数据倒灌
disklrucache
硬盘缓存

QMUI
module的方式引入QMUI,因为QMUI有很多功能,所以单独抽取出一些经常用的功能

你可以轻松做这些

一目了然的项目结构

方便编写,方便维护

一个轻量级的JetPackMVVMLight

类很少

写一个界面你需要创建三个文件,xml布局,activity/fragment,viewmodel,没有其他的了。

一个轻量级的JetPackMVVMLight
一个轻量级的JetPackMVVMLight

LiveEventBus

消息总线,相比于evnetBus和RxBus,具有生命周期感知,而且依赖度小,因为他是依赖于google,而RxBus是依赖于RxJava

//发送消息
LiveEventBus.get(Constant.DEL_ADDRESS_EVENT).post("")

//接受消息
LiveEventBus.get(Constant.EVENT)
    .observe(this, Observer {     
     
})

网络请求代码简约

viewmodel两行代码即可完成基本网络调用并把数据通过liveData回传给界面。demo中举例了三种请求情况,单个实体,接收null,分页。分页请求与不分页的结构相差不大,代码不需要变动太大。

单个实体

val singEntityLiveData = StateLiveData<User>()
fun singEntity() = launchUI(
        response = { mApi.singEntity().data!! },
        liveData = singEntityLiveData
    )

接收null

接收null时,StateLiveData要写死Any?,?是kotlin允许为null。设置allowNull为true,否则livedata不会回传

val mayNullLiveData = StateLiveData<Any?>().allowNull(true)
fun mayNull() = launchUI(
        response = { mApi.mayNull().data },
        liveData = mayNullLiveData
    )

分页

val pageEntityLiveData = StateLiveData<Page<PageUser>>()
fun pageUser(pageNum: Int) = launchUIPage(
        pageNum,
        response = { mApi.pageEntity(pageNum).data!! },
        liveData = pageEntityLiveData
    )

页面缓存(离线缓存)

需求:

  • 网络请求与读取缓存必须是同时执行,不能同步执行。

  • 如果网络请求比读取缓存快,那么缓存的数据不能覆盖网络请求的数据。

  • 如果没有网,读取缓存,接口提示网络错误。

  • 每次的新数据会覆盖之前缓存的数据。

fun cacheData() = launchUIPageCache(
        cache = {
            CacheManager.getInstance().getCacheList(
                Constant.CACHE,
                CacheEntity::class.java
            )
        },
        response = { mApi.cacheEntity().data!! },
        liveData = cacheEntityLiveData
    )

轻松实现监听接口状态

before 请求之前,response 请求结果,error 错误结果,complete 接口请求后(成功/失败)执行,设置 retry 错误重试(可以单独设置或全局设置最大次数和可自定义设置条件或无条件)

fun singEntity() {
        launchUI(
            response = { mApi.singEntity().data!! },
            liveData = singEntityLiveData,
            error = { e, code -> 
                //错误逻辑处理
            },
            retry = { e, code ->
                //重试逻辑
                RetryUtil(3).retryToException(e is SocketTimeoutException) {
                    singEntity()
                }
            },
            complete = {
                //接口执行完毕
            }
        )
    }

使用StateLiveData

界面对接口各种状态的处理,complete(),finishMore(),setPageData()均为kt拓展方法

  /**
   * @param state 状态类
   * @param before 接口请求前
   * @param complete 接口请求后(成功/失败)执行
   * @param hasMore 是否有下一页,做分页
   * @param stateLayout 状态页面
   */
  fun apiState(
      state: State,
      before: (() -> Unit)? = null,
      complete: (() -> Unit)? = null,
      error: ((code: Int) -> Unit)? = null,
      hasMore: ((page: Page<*>) -> Unit)? = null,
      stateLayout: ((layout: Int) -> Unit)? = null
  ) {
      when (state.stateCode) {
          Constant.BEFORE -> before?.invoke()
          Constant.COMPLETE -> complete?.invoke()
          Constant.ERROR -> error?.invoke(state.errorCode)
          Constant.HAS_MORE -> hasMore?.invoke(state.page)
          Constant.STATE_LAYOUT -> stateLayout?.invoke(state.stateLayout)
      }
  }
      mModel.run {
              /**
               * 分页回调
               */
              pageEntityLiveData.observeInActivity(this@NetWorkActivity) { setData(it.list) }
              pageEntityLiveData.state.observeInActivity(this@NetWorkActivity) { state ->
                  apiState(state,
                      complete = { refresh.complete() },
                      stateLayout = { mAdapter.setEmptyView(it) },
                      hasMore = { refresh.finishMore(it) }
                  )
              }

              /**
               * 单个实体回调
               */
              singEntityLiveData.observeInActivity(this@NetWorkActivity) {

              }
              singEntityLiveData.state.observeInActivity(this@NetWorkActivity) {
                  apiState(
                      it,
                      before = { showLoading() },
                      complete = { dismissLoading() })
              }

              /**
               * 可以为null时回调
               */
              mayNullLiveData.observeInActivity(this@NetWorkActivity) {

              }
              mayNullLiveData.state.observeInActivity(this@NetWorkActivity) {
                  apiState(
                      it,
                      before = { showLoading() },
                      complete = { dismissLoading() })
              }

              /**
               * 缓存方式回调
               */
              cacheEntityLiveData.observeInActivity(this@NetWorkActivity) {
                  //这里会执行两次回调数据,缓存数据+网络数据
              }
              cacheEntityLiveData.state.observeInActivity(this@NetWorkActivity) {
                  apiState(
                      it,
                      before = { showLoading() },
                      complete = { dismissLoading() })
              }

              //初始调用接口
              singEntity()
              mayNull()
              cache()
          }

自定义很多kotlin拓展方法(更多方法请见demo)

比如你想加载一个图片

imageView.loadImage("url")

倒计时

直接在activity/fragment界面调用lifecycleCountdown方法,5个参数:时间,间隔,开始监听,进度监听,结束监听,自动感知生命周期,不需要担心页面关闭后倒计时还在跑。

lifecycleCountdown(10, 1,         
        start = {},
        schedule = {},
        completion = {})

重写方法

重写某个接口发现要重写所有但却只需要一两个?

viewpager2.setOnPageChangeListener { 
            
}

editText.setOnTextChanged { s, start, before, count ->
            
}

点击事件

view.onClick{}

设置文字的删除线

textView.centerLine()

轻松实现骨骼加载

骨骼加载与smartRefresh同时使用可能会遇到上拉加载会置顶的问题,需要调整骨骼加载hide的时机

recyclerView.initSkeletonRecyclerView(adapter)

下载文件功能

fileDownloader(context, downloadUrl,
    completed = {},
    pending = {},
    progress = { soFarBytes, totalBytes ->
		
    })

《用户协议》与《隐私政策》显示并点击很麻烦?

textView.protocol("...《用户协议》和《隐私政策》...") {
            when(it){
                0 -> WebViewActivity.start(context,Constant.USER_PROTOCOL)
                1 -> WebViewActivity.start(context,Constant.PRIVACY_PROTOCOL)
            }
        }

为什么要自己写框架

菜鸟级

那是我刚入行,不知道大家是不是同款安卓。

那时的我并无框架概念,通常项目都是直接引入的库。所以所有的代码都在Activity,也没有进行很好的封装。那造成一个问题就是,简单的功能要写好多重复的代码。

刚入行,即使知道问题所在,但是很多问题需要花精力去解决。

熟练工

就这样过了差不多快一年,开始尝试模仿mvc,mvp,mvvm模式。但是写出来的东西总觉得不是自己想要的(过度的追求框架的标准,设计模式,思想)。

我常常也会逛一逛技术帖子,github,找到了一些优秀的框架,如MVPArms,Jetpack-MVVM-Best-Practice…
就这样,开始一点点的理解他们的框架以及逻辑,思想并进行实战项目,比如这个MVPArms,我用了两年。

进阶

随着工龄和经验的增加,慢慢开始有了属于自己的代码风格和方法论。

例如MVPArms,这个框架确实很优秀,但是总归是别人写的,不可能100%的符合自己,当时框架中使用的是dagger2和rxjava,它们倆都太庞大了,rxjava我也只是使用过几个操作符。

其实我倒认为功能单一的App是未来的发展趋势,尽管国内都是一些超级App(功能聚合)。

轻量的App也比较符合我的个人习惯,我一般都是下载lite版的App,不管是某东还是其他。App虽然功能单一,简单。从美学角度,简单并不意味着简陋,每一个轻量应用的设计都值得我们推敲。

想明白这些之后,我发现我需要的是一个轻量框架,加上这些年我开发的项目都是中小型的,轻量可能更加适合我。

未来

在我尝试写各种框架的过程中,也遇到过很多问题,如同上面说的,刚开始我过度追求框架的标准,设计模式,思想,导致写出来的框架不是有问题,就是怪怪的,当然,这些问题都是归咎于我自己的技术问题,没有对框架真正的深入理解,没有理解底层原理。

后来我自我反省,总想着这些也不是办法,不能为了框架而写框架,所以还是一点点的来,从简陋到简约。未来的我还是要不断的学习,要符合Google的框架思想,尝试写出适合更大项目开发的框架。目前的计划是,jetpack compose发布稳定版后,会第一时间更新到我的框架中,如果公司愿意,也想对项目进行实战。

一直都觉得安卓要有自己的风格,而不是遵从iOS的风格,或者两端长得一摸一样,5.0之前安卓确实挺丑的,5.0之后加入了Material Design感觉很不一样,当时以为主流的软件都会向MD风格靠齐,结果现在已经过去这么多年了,主流的软件还是很少看到MD风格。当然不是说安卓就一定要遵从标准或MD,只是我个人更喜欢而已,我觉得推特,电报,google家的一些APP,体验很好,在我手机上其他的软件可能会卡,但是他们不会卡,而且交互很棒,我的框架也加入了一些MD的界面,未来还会加入更多。

计划

  • 1.不断的修善框架,尽量减少不可预期的报错。
  • 2.尝试加入新的技术让框架更健壮,但不丢失轻量,简约。
  • 3.加入更多Material Design样式,交互(完善中)
  • 4.尝试使用jetpack compose重构布局(等待稳定版)
  • 5.更加需要优化代码
  • 6…

总结

我是有开源精神的一个人,无奈自身技术不强,所以很多时候不好意思分享自己学习到的技术。刚开始做这行的时候,那时候啥也不懂,学到了啥就写个博客,写个demo,当时也帮助到了一些人,反而是经验越足就越不敢写博客了。

github地址:JetPackMVVMLight

上一篇:【docker基础】之安装MySQL


下一篇:AGC034E Complete Compres(dp)