Skip to content

完全使用Jetpack Compose构建的WanAndroid客户端

Notifications You must be signed in to change notification settings

Dokiwei/DokiWei_WanAndroid

Repository files navigation

DokiWei-WanAndroid

Github DokiWei

目录

简介

本软件完全遵循Material Design 3的设计规范, 并且在Android12+支持Dynamic colors

在架构模式上使用了MVI架构, 并且以个人的理解实现了Google推荐的数据管理方式, 即唯一可信数据源, 单向数据流

UI完全由用 Jetpack Compose 以声明式编程的方式构建, Jetpack Compose 是 Android 推荐的用于构建本机 UI 的现代工具包。它简化并加速了 Android 上的 UI 开发。通过更少的代码、强大的工具和直观的 Kotlin API,快速使您的应用栩栩如生。Jetpack Compose 可以大幅提升界面的复用性, 在嵌套布局中几乎不会消耗性能,但是要注意的是, Compose 组件会多次重组, 所以请不要将不必要的数据传入或写进组件内, 如果实在需要请使用remember来避免不必要的重复赋值。Jetpack Compose 可以完美的契合 Kotlin 的 Flow 数据流,省去了其他繁琐的数据状态观察方式

网络请求框架使用Retrofit2, 此框架可以快速方便的构建网络请求

数据持久层使用了Room数据库来存储可能需要多次访问的数据, 并配合Paging3的RemoteMediator来实现从多个数据源获取数据

特点

  • 使用现代化的开发架构以及插件进行App的构建
  • 利用 Kotlin 的inline内联函数进行API数据实体类转化, 可以大幅提高 Lambda 函数运行的效率, 并且在函数中添加了API数据errorCode的监控, 以便在用户未登录时或其他问题时提醒用户
  • 充分利用 Kotlin 的协程进行耗时操作, 避免堵塞主线程造成应用卡顿
  • 自定义了一个网络拦截器用来输出网络日志方便调试
  • 利用 MVI 架构精细的控制应用状态, 并且集中的 Intent 解决了 MVVM 调试难以溯源的问题

主要框架及插件

  • Compose
  • Kotlin
  • Gradle
  • Room
  • Paging
  • Retrofit

截图

启动页 首页 项目 导航 账号
启动页 首页 项目 导航 账号
启动页-深色 首页-深色 项目-深色 导航-深色 账号-深色
启动页-深色 首页-深色 项目-深色 导航-深色 账号-深色

详细

Room具有以下优势:

  • 针对 SQL 查询的编译时验证。
  • 可最大限度减少重复和容易出错的样板代码的方便注解。
  • 简化了数据库迁移路径。

Room可以利用注解以及查询语句快速的自动生成java对象供开发者使用

@Dao
interface HomeDao {

    @Query("SELECT * FROM home_article_table")
    fun getAll(): PagingSource<Int, HomeEntity>

    @Insert(onConflict = REPLACE)
    suspend fun insertAll(articles: List<HomeEntity>)

    @Query("DELETE FROM home_article_table")
    suspend fun clearAll()

}

Paging具有以下功能:

  • 分页数据的内存中缓存。该功能有助于确保您的应用在处理分页数据时高效使用系统资源。
  • 内置的请求去重功能,可确保您的应用高效利用网络带宽和系统资源。
  • 可配置的 RecyclerView 适配器,会在用户滚动到已加载数据的末尾时自动请求数据。
  • 对 Kotlin 协程和数据流以及 LiveData 和 RxJava 的一流支持。
  • 内置对错误处理功能的支持,包括刷新和重试功能。

我们只需要构建一个BasePagingMediator就可以为后来的RemoteMediator提供一个简单的多数据源模板

/**
 * @param T:Entity
 * @param R:RemoteKeys
 * @param E:ApiData
 */
@OptIn(ExperimentalPagingApi::class)
abstract class BaseRemoteMediator<T : Any, R : Any, E : Any>(
    private val articleDatabase: ArticleDatabase
) : RemoteMediator<Int, T>() {


    abstract suspend fun getApi(currentPage: Int): Result<List<E>>
    abstract fun convertToEntity(data: E): T
    abstract fun convertToRemoteKeys(data: T, prevPage: Int?, nextPage: Int?): R

    abstract suspend fun clearAll()
    abstract suspend fun insertAll(remoteKeys: List<R>, data: List<T>)

    abstract suspend fun lastUpdated(): Long

    override suspend fun initialize(): InitializeAction {
        val cacheTimeout = TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS)
        return if (System.currentTimeMillis() - lastUpdated() <= cacheTimeout) {
            InitializeAction.SKIP_INITIAL_REFRESH
        } else {
            InitializeAction.LAUNCH_INITIAL_REFRESH
        }
    }

    override suspend fun load(
        loadType: LoadType,
        state: PagingState<Int, T>
    ): MediatorResult {
        return try {
            val currentPage = when (loadType) {
                LoadType.REFRESH -> {
                    val remoteKeys = getRemoteKeyClosestToCurrentPosition(state)
                    remoteKeys?.minus(1) ?: 0
                }

                LoadType.PREPEND -> {
                    val remoteKeys = getRemoteKeyForFirstItem(state)
                    val pervPage = remoteKeys ?: return MediatorResult.Success(
                        endOfPaginationReached = false
                    )
                    pervPage
                }

                LoadType.APPEND -> {
                    val remoteKeys = getRemoteKeyForLastItem(state)
                    val nextPage = remoteKeys ?: return MediatorResult.Success(
                        endOfPaginationReached = false
                    )
                    nextPage
                }
            }

            val result = getApi(currentPage)
            val endOfPaginationReached = result.getOrNull().isNullOrEmpty()
            val prevPage = if (currentPage == 0) null else currentPage - 1
            val nextPage = if (endOfPaginationReached) null else currentPage + 1
            articleDatabase.withTransaction {
                if (loadType == LoadType.REFRESH) {
                    clearAll()
                }
                result.getOrNull()?.let {
                    val entity = it.map { data -> convertToEntity(data) }
                    val keys = entity.map { data ->
                        convertToRemoteKeys(data, prevPage, nextPage)
                    }
                    insertAll(keys, entity)
                }
            }
            MediatorResult.Success(endOfPaginationReached)
        } catch (e: Exception) {
            MediatorResult.Error(e)
        }
    }


    /**
     *
     *@return nextPage
     */
    abstract suspend fun getRemoteKeyClosestToCurrentPosition(state: PagingState<Int, T>): Int?

    /**
     *@return pervPage
     */
    abstract suspend fun getRemoteKeyForFirstItem(state: PagingState<Int, T>): Int?

    /**
     *@return nextPage
     */
    abstract suspend fun getRemoteKeyForLastItem(state: PagingState<Int, T>): Int?


}

关于架构模式

MVI

MVI架构是什么?

MVIMVVM 很相似,其借鉴了前端框架的思想,更加强调数据的单向流动和唯一数据源,架构图如下所示 img 其主要分为以下几部分

  1. Model: 与MVVM中的Model不同的是,MVIModel主要指UI状态(State)。例如页面加载状态、控件位置等都是一种UI状态
  2. View: 与其他MVX中的View一致,可能是一个Activity或者任意UI承载单元。MVI中的View通过订阅Model的变化实现界面刷新
  3. Intent: 此Intent不是ActivityIntent,用户的任何操作都被包装成Intent后发送给Model层进行数据请求

单向数据流

MVI强调数据的单向流动,主要分为以下几步:

  1. 用户操作以Intent的形式通知Model
  2. Model基于Intent更新State
  3. View接收到State变化刷新UI。

数据永远在一个环形结构中单向流动,不能反向流动: img

上面简单的介绍了下MVI架构,下面我们一起来看下具体是怎么使用MVI架构的

img

我们使用ViewModel来承载MVIModel层,总体结构也与MVVM类似,主要区别在于ModelView层交互的部分

  1. Model层承载UI状态,并暴露出ViewStateView订阅,ViewState是个data class,包含所有页面状态
  2. View层通过Action更新ViewState,替代MVVM通过调用ViewModel方法交互的方式

此段关于MVI架构的解释部分引用自掘金用户:程序员江同学的MVVM 进阶版:MVI 架构了解一下~

鸣谢

鸿洋大佬的WanAndroid提供的开放Api

About

完全使用Jetpack Compose构建的WanAndroid客户端

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published

Languages