再探Kotlin 跨平台——迁移Paging分页库至KMM

news2024/9/22 21:28:32

前言

KMM的发展除了靠官方社区的支持外,一些大企业的开源落地也尤为重要。从这些开源中我们需要借鉴他的设计思想和实现方式。从而在落地遇到问题时,寻得更多的解决办法。

上周,Square正式将Paging分页库迁移到了Kotlin Multiplatform平台,使用在旗下的支付软件Cash App中。

迁移过程

初衷

据Cash App称,他们想在跨平台中使用分页逻辑,但是AndroidX Paging只支持Android平台。所以他们参照AndroidX下Paging库的设计,实现了一套Multiplatform Paging。

模型

与AndroidX下的Paging设计一样,paging-common模块提供存储层、视图模型层;paging-runtim模块提供UI层。

最主要的是,paging-common中的API与AndroidX 下的API完全相同,仅仅是将包从androidx.paging迁移到了app.cash.paging中,所以这部分的使用我们直接按照AndroidX中的Paging使用即可。如果之前项目已经使用了AndroiX的Paging库,则可以在Android平台上无缝迁移。

如果你之前从未使用过Paging库,可以参考许久之前我写的两篇相关文章:

在View中使用Paging3分页库

在Compose中使用分页库

接下来我们就以multiplatform-paging-samples为例,来看如何实现在Multiplatform使用Paging库。

项目分析

项目介绍


multiplatform-paging-samples 项目(Demo)的功能是使用github的接口:api.github.com/search/repositories 查询项目,输出项目路径和start数量。

也就是github主页上的搜索功能。App运行截图如下所示。

 这里我们搜索关键词为“MVI”,左侧输出为作者/项目名 右侧为start数量,且实现了分页功能。接着我们来看这个项目结构是怎么样的。

项目架构

从项目架构中可以看出在共享模块中,只有iosMain并没有AndroidMain,这是因为我们前面所讲到的针对Android平台是可以无缝迁移的。接着我们再来看shared模块中的通用逻辑。

commonMain通用逻辑

models.kt文件中定义了若干数据结构,部分代码如下所示。

sealed interface ViewModel {

  object Empty : ViewModel

  data class SearchResults(
    val searchTerm: String,
    val repositories: Flow<PagingData<Repository>>,
  ) : ViewModel
}

@Serializable
data class Repositories(
  @SerialName("total_count") val totalCount: Int,
  val items: List<Repository>,
)

@Serializable
data class Repository(
  @SerialName("full_name") val fullName: String,
  @SerialName("stargazers_count") val stargazersCount: Int,
)

RepoSearchPresenter类中主要做了三件事:

  • 定义HttpClient对象

  • 定义Pager与PagerSource

  • 定义查询数据的方法

定义HttpClient对象

这里的网络请求框架使用的是Ktor,代码如下所示:

private val httpClient = HttpClient {
  install(ContentNegotiation) {
    val json = Json {
      ignoreUnknownKeys = true
    }
    json(json)
  }
}

定义Pager与PagerSource

pager的声明如下所示:

private val pager: Pager<Int, Repository> = run {
  val pagingConfig = PagingConfig(pageSize = 20, initialLoadSize = 20)
  check(pagingConfig.pageSize == pagingConfig.initialLoadSize) {
    "As GitHub uses offset based pagination, an elegant PagingSource implementation requires each page to be of equal size."
  }
  Pager(pagingConfig) {
      RepositoryPagingSource(httpClient, latestSearchTerm)
  }
}

这里指定了pageSize的大小为20,并调用PagerSource的方法,RepositoryPagingSource声明如下所示:

private class RepositoryPagingSource(
  private val httpClient: HttpClient,
  private val searchTerm: String,
) : PagingSource<Int, Repository>() {

  override suspend fun load(params: PagingSourceLoadParams<Int>): PagingSourceLoadResult<Int, Repository> {
    val page = params.key ?: FIRST_PAGE_INDEX
    println("veyndan___ $page")
    val httpResponse = httpClient.get("https://api.github.com/search/repositories") {
      url {
        parameters.append("page", page.toString())
        parameters.append("per_page", params.loadSize.toString())
        parameters.append("sort", "stars")
        parameters.append("q", searchTerm)
      }
      headers {
        append(HttpHeaders.Accept, "application/vnd.github.v3+json")
      }
    }
    return when {
      httpResponse.status.isSuccess() -> {
        val repositories = httpResponse.body<Repositories>()
        println("veyndan___ ${repositories.items}")
        PagingSourceLoadResultPage(
          data = repositories.items,
          prevKey = (page - 1).takeIf { it >= FIRST_PAGE_INDEX },
          nextKey = if (repositories.items.isNotEmpty()) page + 1 else null,
        ) as PagingSourceLoadResult<Int, Repository>
      }
      httpResponse.status == HttpStatusCode.Forbidden -> {
        PagingSourceLoadResultError<Int, Repository>(
          Exception("Whoops! You just exceeded the GitHub API rate limit."),
        ) as PagingSourceLoadResult<Int, Repository>
      }
      else -> {
        PagingSourceLoadResultError<Int, Repository>(
          Exception("Received a ${httpResponse.status}."),
        ) as PagingSourceLoadResult<Int, Repository>
      }
    }
  }

  override fun getRefreshKey(state: PagingState<Int, Repository>): Int? = null

这部分代码没什么好解释的,和AndroidX的Paging使用是一样的。

定义查询数据的方法

这里还定一个一个查询数据的方法,使用flow分发分发给UI层,代码如下所示:

suspend fun produceViewModels(
    events: Flow<Event>,
  ): Flow<ViewModel> {
    return coroutineScope {
      channelFlow {
        events
          .collectLatest { event ->
            when (event) {
              is Event.SearchTerm -> {
                latestSearchTerm = event.searchTerm
                if (event.searchTerm.isEmpty()) {
                  send(ViewModel.Empty)
                } else {
                  send(ViewModel.SearchResults(latestSearchTerm, pager.flow))
                }
              }
            }
          }
      }
    }
  }
}

这里的Event是定义在models.kt中的密封接口。代码如下所示:

sealed interface Event {

  data class SearchTerm(
    val searchTerm: String,
  ) : Event
}

iosMain的逻辑

在iosMain中仅定义了两个未使用的方法,用于将类型导出到Object-C或Swift,代码如下所示。

@Suppress("unused", "UNUSED_PARAMETER") // Used to export types to Objective-C / Swift.
fun exposedTypes(
  pagingCollectionViewController: PagingCollectionViewController<*>,
  mutableSharedFlow: MutableSharedFlow<*>,
) {
  throw AssertionError()
}

@Suppress("unused") // Used to export types to Objective-C / Swift.
fun <T> mutableSharedFlow(extraBufferCapacity: Int) = MutableSharedFlow<T>(extraBufferCapacity = extraBufferCapacity)

 其实这里我没有理解定义这两个方法的实际意义在哪里,还望大佬们指教。

Android UI层实现

Android UI层的实现比较简单,定义了一个event用于事件分发

val events = MutableSharedFlow<Event>(extraBufferCapacity = Int.MAX_VALUE)
lifecycleScope.launch {
  viewModels.emitAll(presenter.produceViewModels(events))
}

当输入框中的内容改变时,发送事件,收到结果显示数据即可,代码如下所示:

@Composable
private fun SearchResults(repositories: LazyPagingItems<Repository>) {
  LazyColumn(
    Modifier.fillMaxWidth(),
    contentPadding = PaddingValues(16.dp),
    horizontalAlignment = Alignment.CenterHorizontally,
  ) {
    when (val loadState = repositories.loadState.refresh) {
      LoadState.Loading -> {
        item {
          CircularProgressIndicator()
        }
      }
      is LoadState.NotLoading -> {
        items(repositories) { repository ->
          Row(Modifier.fillMaxWidth()) {
            Text(
              repository!!.fullName,
              Modifier.weight(1f),
            )
            Text(repository.stargazersCount.toString())
          }
        }
      }
      is LoadState.Error -> {
        item {
          Text(loadState.error.message!!)
        }
      }
    }
  }
}

iOS平台的实现

AppDelegate.swift文件是程序启动入口文件,RepositoryCell类继承自UICollectionViewCell,并补充了API中返回的字段信息,UICollectionViewCell是iOS中的集合视图,代码如下所示:

class RepositoryCell: UICollectionViewCell {
  @IBOutlet weak var fullName: UILabel!
  @IBOutlet weak var stargazersCount: UILabel!
}

iOS触发查询代码如下所示:

extension RepositoriesViewController: UITextFieldDelegate {
  func textFieldShouldReturn(_ textField: UITextField) -> Bool {
    let activityIndicator = UIActivityIndicatorView(style: .gray)
    textField.addSubview(activityIndicator)
    activityIndicator.frame = textField.bounds
    activityIndicator.startAnimating()

    self.collectionView?.reloadData()

    activityIndicator.removeFromSuperview()

    events.emit(value: EventSearchTerm(searchTerm: textField.text!), completionHandler: {error in
      print("error", error ?? "null")
    })

    presenter.produceViewModels(events: events, completionHandler: {viewModels,_ in
      viewModels?.collect(collector: ViewModelCollector(pagingCollectionViewController: self.delegate), completionHandler: {_ in print("completed")})
    })

    textField.resignFirstResponder()
    return true
  }
}

写在最后

KMM的发展出除了靠官方社区的支持之外,一些有名项目的落地实践也很重要。目前我们所能做的就是持续关注KMM的动态,探索可尝试落地的组件,为己所用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/12632.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

目标检测论文解读复现之十:基于YOLOv5的遥感图像目标检测

前言 此前出了目标改进算法专栏&#xff0c;但是对于应用于什么场景&#xff0c;需要什么改进方法对应与自己的应用场景有效果&#xff0c;并且多少改进点能发什么水平的文章&#xff0c;为解决大家的困惑&#xff0c;此系列文章旨在给大家解读最新目标检测算法论文&#xff0c…

接上篇文章,完成Hadoop集群部署实验

这里写目录标题一、实验介绍1.1 实验内容1.2 实验知识点1.3 实验环境1.4 实验资源1.5 实验步骤清单二、实验架构三、实验环境准备四、实验步骤4.1 查看环境4.2部署Hadoop集群4.2.1安装hadoop(master)4.2.2创建hdfs数据文件存储目录(master)4.2.3修改配置文件(master)4.2.4主从节…

项目管理:团队执行力差,管理不善是根源

为什么说团队执行力差&#xff0c;多半是管理者的问题&#xff1f;如何提高团队的执行力&#xff1f; 1、员工不知道该做什么 项目开始一段时间&#xff0c;员工都还没弄清楚该做什么&#xff0c;是什么职位啊&#xff1f;为什么会产生这样的问题呢&#xff1f; 这是因为管理…

clickhouse使用clickhouse-keeper代替zookeeper

目录 异常现象&#xff1a; 1. clickhouse的异常日志 2. 追踪对应节点的zookeeper日志 使用clickhouse-keeper代替 zookeeper的步骤&#xff1a; 1&#xff1a; 准备 clickhouse-keeper的配置文件 1.1- 设置通信地址&#xff0c;以便对外通信 1.2- 在config.xml中…

java计算机毕业设计ssm宠物店管理系统element vue前后端分离

项目介绍 宠物店管理平台是使用JAVA的SSM技术,MySQL作为数据库开发,用户通过查看宠物,在线购买商品情况,并可以查看宠物店铺嘻嘻,实现宠物信息化管理。首先对本论文进行分析后,提出平台的相关技术,然后整理系统的需求分析,根据需求进行功能和数据库设计,最后进行系统实现和测试…

Nginx静态资源配置

Nginx配置成系统服务 把Nginx应用服务设置成为系统服务&#xff0c;方便对Nginx服务的启动和停止等相关操作&#xff0c;具体实现步骤: 在/usr/lib/systemd/system目录下添加nginx.service,内容如下: [Unit] # Unit表明该服务的描述&#xff0c;类型描述 Descriptionnginx w…

MQ(二)RabbitMQ快速入门

一、RabbitMQ 概述和安装 RabbitMQ 是基于 Erlang 语言开发的开源消息通信中间件 1. RabbitMQ的结构和概念 RabbitMQ中的几个概念&#xff1a; &#xff08;1&#xff09;channel&#xff1a;操作MQ的工具 &#xff08;2&#xff09;exchange&#xff1a;路由消息到队列中 &…

数据结构知识点补充

(67条消息) c语言怎么return ok,C语言中的 return与exit_哈哈哈可以的博客-CSDN博客 假如把i或i放入到for循环的函数体中&#xff0c;若没有变量或函数来接收其返回值&#xff0c;那么它们的作用效果也是相同的(67条消息) C语言的for循环中i和i的关系_肥嘟嘟的左卫门的博客-CSD…

1226:装箱问题 (贪心)

【题目描述】 一个工厂制造的产品形状都是长方体&#xff0c;它们的高度都是h&#xff0c;长和宽都相等&#xff0c;一共有六个型号&#xff0c;他们的长宽分别为11,22,33,44,55,66。这些产品通常使用一个66h的长方体包裹包装然后邮寄给客户。因为邮费很贵&#xff0c;所以工厂…

爬虫工具-BeautifulSoup

BeautifulSoup&#xff1a; 1. 是一个高效的网页解析库&#xff0c;可以从HTML或者XML文件中提取数据 2. 支持不同的解析器&#xff0c;可以对HTML、XML等进行解析 3. 是一个敏感又方便的网页解析库&#xff0c;处理高效&#xff0c;支持多种解析器 4. 利用它在不编写正则表达式…

Unreal 和 Unity 3D 各有什么特点?如何选择?

一、两个引擎简述 Unreal Engine VS Unity Unity是实时3D互动内容创作和运营平台。包括游戏开发、美术、建筑、汽车设计、影视在内的所有创作者&#xff0c;借助Unity将创意变成现实。Unity平台提供一整套完善的软件解决方案&#xff0c;可用于创作、运营和变现任何实时互动的…

【设计模式】软件设计原则

1. 软件设计原则&#xff1a; 为了提高系统软件的可维护性、可复用性、可扩展性和灵活性 1.1 开闭原则&#xff1a; 对扩展开放&#xff0c;对修改关闭 &#xff08;热插拔&#xff09; 实现方式&#xff1a; 通过定义接口和实现类的方式实现开闭原则&#xff0c;我们再进行…

ASEMI代理艾赛斯IXTY02N50D-TRL车规级MOSFET

编辑-Z 艾赛斯车规级MOS管IXTY02N50D-TRL参数&#xff1a; 型号&#xff1a;IXTY02N50D-TRL 漏极-源极电压&#xff08;VDS&#xff09;&#xff1a;500V 连续漏电流&#xff08;ID&#xff09;&#xff1a;200mA 功耗&#xff08;PD&#xff09;&#xff1a;25W 工作结温…

如何运营推特营销主账号

在推特上做产品推广&#xff0c;Twitter群&#xff08;发&#xff09;推王建议&#xff1a;一般需要两类账号。 主账号&#xff1a;也就是官方号&#xff0c;用来聚集粉丝&#xff0c;日常产品信息、新闻资讯发布的&#xff1b; 批量推广号&#xff1a;也叫小号或者广告号&am…

oracle数据库定义语言—DDL

文章目录1、数据定义语言2 创建表2.2 oracle 数据库中的表2.2.1 常见的数据字典表2.3 用查询创建表2.3.1 示例3 ALTER TABLE 语句3.1 添加一个新的列3.2 修改数据类型3.3 修改默认值3.4 修改列名3.5 删除一个列4、修改名称5、截断表6 、删除表&#xff08;删除表以及表结构&…

《垃圾回收算法手册 自动内存管理的艺术》——引用计数与垃圾回收器的比较(笔记)

文章目录五、引用计数5.1 引用计数算法的优缺点5.2 提升效率5.3 延迟引用计数5.4 合并引用计数5.5 环状引用计数**步骤**5.6受限域引用计数六、垃圾回收器的比较6.1 吞吐量6.2 停顿时间6.3 内存空间6.4 回收器的实现6.5 自适应系统6.6 统一垃圾回收理论6.6.1 垃圾回收的抽象6.6…

神经网络在故障诊断中的应用

人工神经网络是近年来发展迅速&#xff0c;广泛地应用于各个领域&#xff0c;包括航天&#xff0c;自动控制&#xff0c;金融&#xff0c;电子&#xff0c;制造&#xff0c;医药等多个行业&#xff0c;已经有许多成功的先例&#xff0c;并展示了更为广阔的应用前景。基于神经网…

【区块链 | 智能合约】Ethereum源代码(8)- Ethereum服务和以太坊P2P协议发送广播源码分析

在“【区块链 | 智能合约】Ethereum源代码(2)- go-ethereum 客户端入口代码和Node分析”一文中,我们提到Ethereum作为一个service,被Node 注册进去。Node start的时候会启动其注册的所有服务,Ethereum service也是一样。 一、ethereum service的初始化和启动 func geth(c…

【MySQL数据库笔记 - 进阶篇】(二)索引

✍个人博客&#xff1a;https://blog.csdn.net/Newin2020?spm1011.2415.3001.5343 &#x1f4da;专栏地址&#xff1a;暂定 &#x1f4dd;视频地址&#xff1a;黑马程序员 MySQL数据库入门到精通 &#x1f4e3;专栏定位&#xff1a;这个专栏我将会整理 B 站黑马程序员的 MySQL…

day064:File类与I/O流、绝对路径与相对路径、File类的成员方法

目录 一、什么是File类、IO流 1.File类 2.I/O流 3.File类的构造方法 二、绝对路径与相对路径 三、File类的成员方法 1.File类的创建功能 &#xff08;1&#xff09;createNewFile() 创建空的文件 &#xff08;2&#xff09;mkdir() 创建单级文…