Design WanAndroid(WanAndroid的最佳可使用的Android客户端)

🦄Design WanAndroid

前言

  • 背景
    • 之前一直都是使用的Rxjava,响应式编程是真的写起来特别的简洁优雅,而且直观,一个数据流,从发射->中间的数据转换->消费一目了然(当然前提是本身使用恰当),其中各式各样的操作符完美覆盖任何场景。但是使用的多了也免不了会发现一些问题,比如debug的时候简直难受,还有对于当前逻辑没有显式的表明是否为子线程,特别在函数调用链长且在不同类的时候,往往要追溯很久等等此类。当然事物本来也无完美之事,我们能做的便是尽力追求完美的事物。
    • 此外便是目前Android也诞生了很多新东西,所以我打算尝试一下,说不定能带来一些新的视角,因为一件新事物的诞生往往是需要解决旧事物的某处不足。
  • 实现
    • 目前WanAndroid公开的Api均已实现,我需要的是一个完美的App而不是充满着TODO项。

介绍

App内通篇全采用Material Design 3风格,拒绝半完成式Material带来的UI的割裂感。

我见多很多WanAndroid的开源客户端,在UI上都不怎么重视,但是如果要是日常使用的App,没有得体的UI我相信很难有使用的动力,而Material Design无疑是最好的选择

所有Icon取自Material Symbols,统一而规范的设计。

主题色遵循Material3 Color system

默认主题色采用Material Theme Builder从图片取色而成。

实现Dynamic Colors,开启动态主题色后,App主题色自动跟随系统主题色且适配深色模式,保持一贯的视觉体验(Android 12及以上支持)

所以可交互的UI均带有Ripple效果,明确表示这是个可交互控件,且Ripple颜色支持取自当前Dynamic colors的主题色



实现

使用buildSrc,实现全局且统一的依赖管理。

严格遵循Android Architecture Components,逻辑分为:

  • 界面层(UI Layer)

    • APP内实现:视图(Activity/Fragment等) + 数据驱动及处理逻辑的状态容器(ViewModel等)
  • 网域层(Domain Layer) 可选项,用于处理复杂逻辑或支持可重用性吗,当你需要从不同数据源获取数据时如需要同时从数据库和接口请求数据时,推荐使用UseCase进行组合。

    • App内实现:组合或复用数据源(UseCase)
  • 数据层(Data Layer)

    • App内实现:数据源(Repository)

Retorfit + OkHttp

使用通用的网络请求库,Retrofit + OkHttp,这个没什么好说的,其中需要注意的是对异常的处理,无论是请求异常或是业务异常。
我见过大部分开源WanAndroid都是每个接口请求后自己再判断,先try catch异常,然后在里面判断是否有业务异常,这样也不是不行,但是不够优雅,使用起来我就不能直接拿到数据吗?本身这些非业务的异常也不是自身能够处理的。所以需要一个全局的网络异常处理。

CallAdapter, Converter

我们知道Retorfit的强大之一无疑在于其的可定制化强。所以也是从这两个入手。

WanAndroid的接口返回统一结构是:

{ 
    "data": ...,
    "errorCode": 0,
    "errorMsg": "" 
}

这次要做的是把data与error拆开来,正是上面说的使用起来我就不能直接拿到数据吗?
先枚举一下网球请求响应的状态,使用sealed class可以让when表达式穷举且比起enum class更为灵活。

源代码:NetworkResponse

sealed class NetworkResponse<out T: Any> {
    /**
     * 成功
     */
    data class Success<T: Any>(val data: T) : NetworkResponse<T>()

    /**
     * 业务错误
     */
    data class BizError(val errorCode: Int = 0, val errorMessage: String = "") :
        NetworkResponse<Nothing>()

    /**
     * 其他错误
     */
    data class UnknownError(val throwable: Throwable) : NetworkResponse<Nothing>()
}

这是我们的接口返回类型,如果接口errorCode = 0,说明业务逻辑正常,data直接赋值,返回NetworkResponse.Success。

如果errorCode !=0 ,说明有业务错误,data就不需要了,返回NetworkResponse.BizError。

对于非业务的异常,归类为UnknownError,因为对于下游来说是非预期的。

那么,如何让接口返回这个类型?所以需要自定义CallAdapter,可见NetworkResponseAdapter

我们用获取首页置顶文章列表来举例,最终实现效果如下

@GET("article/top/json")
suspend fun getArticleTopList(): NetworkResponse<List<Article>>

现在类型转换有了,那我要怎么做到把接口返回拆分开来?
其实无非还是解析,只是这次我们自己来处理解析的逻辑,所以需要自定义Converter,可见GsonConverterFactory
对于ResponseBody,需要自己

while (jsonReader.hasNext()) {
    when (jsonReader.nextName()) {
        "errorCode" -> errorCode = jsonReader.nextInt()
        "errorMsg" -> errorMsg = jsonReader.nextString()
        "data" -> data = adapter.read(jsonReader)
        else -> jsonReader.skipValue()
    }
}
...
return if (errorCode != 0) {
    NetworkResponse.BizError(errorCode, errorMsg)
} else {
    NetworkResponse.Success(data)
}

这样,我们实现了拆分,成功的请求我就不需要关注errorCode,业务错误的请求我同样不需要关注data。

再加一点扩展函数方便使用

inline val NetworkResponse<*>.isSuccess: Boolean
    get() {
        return this is NetworkResponse.Success
    }

fun <T : Any> NetworkResponse<T>.getOrNull(): T? =
    when (this) {
        is NetworkResponse.Success -> data
        is NetworkResponse.BizError -> null
        is NetworkResponse.UnknownError -> null
    }

fun <T : Any> NetworkResponse<T>.getOrThrow(): T =
    when (this) {
        is NetworkResponse.Success -> data
        is NetworkResponse.BizError -> throw ApiException(errorCode, errorMessage)
        is NetworkResponse.UnknownError -> throw throwable
    }
.............

这样我们就实现了一个较为统一且优雅的接口请求的异常处理。

Hilt

这个我为啥要提呢?因为我觉得依赖注入是推荐的应用架构不可分割的一部分,其中还有一层可选项叫网域层(Domain Layer)。

用于负责封装复杂的业务逻辑,或者由多个 ViewModel 重复使用的简单业务逻辑UseCase等。它是可选的,因为并非所有应用都有这类需求。请仅在需要时使用该层,例如处理复杂逻辑或支持可重用性。
既然需要可重用性,那不可避免的会需要很多依赖项,而Hilt正是为了解决这个问题而来的。
使用可参考官方文档使用 Hilt 实现依赖项注入

Flow & LiveData

LiveData虽可以被Flow代替,但是它足够的轻量,很适合One-Shot型数据,比如只是需要获取一次的接口数据,而且本身也可以搭配协程使用将协程与 LiveData 一起使用,所以还是有其用武之地的。
而复杂数据流就使用flow,其实类似Rxjava,使用起来区别不大,只是有较多的@ExperimentalCoroutinesApi。。。希望能早日stable。对于flow主要的还是StateFlow 和 SharedFlow,其实和Rxjava的冷,热流概念是一致的。

Paging

RecyclerView本身是很灵活的,但是由于其灵活所以写起来还是有一点繁琐,这也是Paging出现的原因,使用它能够很方便的实现分页请求,状态管理且支持Flow等数据流式处理。

但是它对于多类型RecyclerView还是没给出一个较好的方案,即使是目前的ConcatAdapter,感觉也是不够好,所以引入了MultiType,但新的问题随之而来,MultiType并不支持Paging,所以决定定制化MultiType,使其支持Paging的机制。

通过查看PagingDataAdapter的源码,可以发现其本身功能不多,全由AsyncPagingDataDiffer来实现,所以实现起来不麻烦,先将MultiTypeAdapter注意逻辑抽出为基类,(主要逻辑在于register,抽出来也不麻烦),然后子类继承并实现PagingDataAdapter的功能即可。具体源码可见PagingMultiTypeAdapter

同时对于PagingSource的使用简单封装了一个Key值为Int类型的PagingSource,因为PagingSource本身也很简单,位于的区别在于load,所以直接暴露给外部,外部提供返回值就行了。
IntKeyPagingSource

class IntKeyPagingSource<S : BaseService, V : Any>(
    private val pageStart: Int = BaseService.DEFAULT_PAGE_START_NO_1,
    private val service: S,
    private val load: suspend (S, Int, Int) -> List<V>
) : PagingSource<Int, V>() {

    override fun getRefreshKey(state: PagingState<Int, V>): Int? {
        return state.anchorPosition?.let { anchorPosition ->
            state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
                ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
        }
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, V> {
        val page = params.key ?: pageStart
        return try {
            val data = load(service, page, params.loadSize)
            LoadResult.Page(
                data = data,
                prevKey = if (page == pageStart) null else page - 1,
                nextKey = if (data.isEmpty()) null else page + 1
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }
}

DataStore

SharedPreference不用说了,已经被抛弃了,替代品正是与协程相结合的DataStore使用协程和Flow 以异步、一致的事务方式存储数据。

目前项目内只用到Preferences DataStore

使用下来怎么说呢?它本身使用使用协程和Flow处理,这既是优点也是缺点,因为它未实现SharedPreferences,所以你想简单的像原来一样getString或是putString,不行,你需要开启协程,开启协程肯定也要提供作用域吧?这样下来其实写起来就特别的麻烦。

目前实现具体可参考采用项目内使用DataStore持久化cookie - CookieJarImpl,还是有那么一点别扭的。

更搞的是PreferenceFragmentCompat它还是用的SharePreference,你还没办法用DataStore,所以想要使用DataStore的话我建议还是搭配MMKV一起。

最后

感谢WanAndroid网站提供的开放API

项目地址: 🦄Design WanAndroid

有任何问题欢迎提Issue,喜欢的话也可以点个⭐Star~

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

推荐阅读更多精彩内容