)
Compose Multiplatform 实战用 Kotlin 构建跨平台 GitHub Issues 客户端JetIssues【免费下载链接】compose-multiplatformCompose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.项目地址: https://gitcode.com/GitHub_Trending/co/compose-multiplatform本篇技术指南以 Compose Multiplatform 官方仓库中的 examples/issues 示例为主线完整讲解如何基于 Jetpack Compose UI 库构建一个可运行于 Android 与桌面JVM平台的 GitHub Issues 查看器 JetIssues。你将掌握多平台共享 UI 代码的工程结构、Apollo GraphQL 客户端对接 GitHub GraphQL API、基于 Compose 的响应式双栏/单栏布局以及通过./gradlew :desktop:run一键运行、打包桌面分发的完整流程。工程概览一个 UI 代码高度复用的多平台示例JetIssues 是一个用 Jetpack Compose UI 库编写的 GitHub Issues 查看器示例。它的核心特点在于绝大多数 UI 与业务逻辑都在共享代码中实现Android 与 Desktop 两个平台只提供最小的宿主壳host。从 examples/issues/settings.gradle.kts 可以看到工程由三个模块组成rootProject.name issues include(:common, :android, :desktop):common核心模块包含共享 UI、数据层与 GraphQL 查询定义:androidAndroid 宿主应用仅包含MainActivity与资源文件:desktop桌面 JVM 宿主仅包含main()入口函数与打包配置。settings.gradle.kts还通过 version catalog 从gradle.properties读取 Kotlin、Compose、AGP 版本versionCatalogs { create(libs) { version(kotlin, extra[kotlin.version].toString()) version(compose, extra[compose.version].toString()) version(agp, extra[agp.version].toString()) } }版本定义集中在 examples/issues/gradle.properties同时引入了 JetBrains 的 CMPCompose Multiplatform开发仓库kotlin.version2.3.20 agp.version9.2.1 compose.version1.10.1pluginManagement { repositories { gradlePluginPortal() maven(https://packages.jetbrains.team/maven/p/cmp/dev) google() } }运行桌面应用两种启动方式原 README 给出了两种运行桌面程序的方式方式一命令行运行./gradlew :desktop:runGradle 会编译:common与:desktop随后拉起桌面窗口默认标题JetIssues初始尺寸 1440×768。方式二IDE 运行配置在 IntelliJ IDEA 中导入工程后选择desktop运行配置直接启动即可。IDE 配置界面如图所示提示由于示例使用了依赖 JVM 声明的中间源集jvmAndAndroidMainIDE 中可能出现 expect/actual 解析为红色的现象这属于已知的正常情况不影响编译运行。源码文件 IssuesRepository.kt 与 JetIssuesView.kt 头部注释对此有专门说明。构建原生桌面发行版打包要生成当前操作系统的原生可执行程序执行./gradlew :desktop:packageDistributionForCurrentOS # outputs are written to desktop/build/compose/binaries打包产物输出到desktop/build/compose/binaries目录该目录由 Compose Gradle 插件自动生成无需手动创建。打包配置参考 examples/issues/desktop/compose-desktop.pro其中保留了运行时需要的 ProGuard/R8 规则例如# 资源以相对路径加载必须保留该类的包名 -adaptresourcefilenames okhttp3/internal/publicsuffix/PublicSuffixDatabase.gz # OkHttp 平台相关与安全提供者仅在 JVM 上使用 -dontwarn okhttp3.internal.platform.** -dontwarn org.conscrypt.** -dontwarn org.bouncycastle.** # PrettyTime 的 i18n 资源类需要保留 -keep class org.ocpsoft.prettytime.i18n**桌面程序最终效果如下共享 UI 源码解析从入口到响应式布局桌面端入口在 Main.ktAndroid 端入口在 MainActivity.kt。两者高度对称——都用CompositionLocalProvider注入数据仓库再渲染同一个JetIssuesView()// desktop 端 val repo IssuesRepositoryImpl(defaultRepo.first, defaultRepo.second, System.getenv(GITHUB_TOKEN) ?: defaultAuth) fun main() { application { Window( onCloseRequest ::exitApplication, title JetIssues, state WindowState(size DpSize(1440.dp, 768.dp)) ) { CompositionLocalProvider(Repository provides repo) { JetIssuesView() } } } exitProcess(0) // force close Apollo Client }// android 端 class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { CompositionLocalProvider(Repository provides repo) { JetIssuesView() } } } }核心 UI 全部位于 JetIssuesView.kt其中用compositionLocalOf声明了一个全局仓库依赖val Repository compositionLocalOfIssuesRepository { error(Undefined repository) }整个界面由Main()组合函数驱动利用BoxWithConstraints读取最大宽度实现响应式布局切换——宽度超过 1000dp 用双栏否则用单栏Composable fun Main() { val currentIssue: MutableStateIssuesQuery.Node? remember { mutableStateOf(null) } BoxWithConstraints { if (maxWidth.value 1000) { TwoColumnsLayout(currentIssue) } else { SingleColumnLayout(currentIssue) } } }双栏布局左侧 40% 宽度展示 Issue 列表右侧展示当前 Issue 详情单栏布局无选中项时显示列表选中后切换到带返回箭头ArrowBack的详情页。此外列表区还提供了FilterTabsOpen/Closed 状态过滤、OrderButton按创建时间 ASC/DESC 切换、MoreButton游标分页加载更多、Labels将 GitHub Label 十六进制颜色解析后渲染为圆角标签等组件。其中 Label 颜色按亮度自动选择前景色val color parseColor(it.color) val textColor if (color.luminance() 0.5) Color.Black else Color.White详情页还通过SelectionContainer让标题与正文可选中复制并配合自定义滚动条滚动条通过 expect/actual 定义桌面端实现见 Platform.ktdesktopMain声明见 Platform.ktcommonMain。数据层Apollo GraphQL 对接 GitHub API数据层在 IssuesRepository.kt 中实现通过 Apollo Client 访问 GitHub GraphQL 端点https://api.github.com/graphql。接口定义如下interface IssuesRepository { fun getIssues(state: IssuesState, order: OrderDirection, cursor: String? null, callback: (ResultIssues) - Unit) fun getIssue(id: Int, callback: (ResultIssueQuery.Issue) - Unit) }实现IssuesRepositoryImpl的关键点鉴权用 OkHttp 网络拦截器在每个请求头注入Authorization: bearer $token自定义类型适配器为 GraphQL 的DateTime自定义类型提供Date转换Date.from(Instant.parse(v))结果封装用密封类Result.Success / Result.Error承载回调结果仓库或 Issue 不存在时抛出UnknownRepo/UnknownIssue游标分页getIssues传入after游标与first: 20一起查询返回pageInfo.endCursor供MoreButton继续加载。Token 的默认来源为System.getenv(GITHUB_TOKEN)未设置时回退到内置的演示认证串源码中通过字符位移混淆private fun decode(input: String) input.toCharArray().map { it 1 }.joinToString() val defaultAuth decode(/4/81b6db605e8d6bdc7ecba8d2/a7/37020) val defaultRepo Pair(JetBrains, compose-multiplatform)即默认查询JetBrains/compose-multiplatform仓库的 Issue。GraphQL 查询定义在 issues.graphql对应schema.json由 Apollo 插件在构建期生成类型安全的查询类IssuesQuery/IssueQueryquery Issues($owner: String!, $repo: String!, $direction: OrderDirection!, $after: String, $state: IssueState!) { repository(owner: $owner, name: $repo) { issues(first: 20, orderBy: { direction: $direction, field: CREATED_AT}, after: $after, filterBy: {states: [$state]}) { nodes { number, title, createdAt, closed, author { login }, comments { totalCount }, labels(first: 5) { nodes { name, color } } } pageInfo { endCursor } totalCount } } }可见每页拉取 20 条、按创建时间排序并携带作者、评论数、标签颜色等列表所需字段详情查询Issue则单独获取正文body。UI 状态管理Composable 驱动的异步数据流Effects.kt 定义了一套轻量的Composable 内加载异步数据模式用密封类UiState表达三种状态sealed class UiStateout T { object Loading : UiStateNothing() data class Successout T(val data: T) : UiStateT() data class Error(val exception: Exception) : UiStateNothing() }核心工具函数uiStateFrom把仓库回调封装成DisposableEffectComposable fun T uiStateFrom( vararg inputs: Any?, repositoryCall: RepositoryCallT ): MutableStateUiStateT { val state: MutableStateUiStateT remember { mutableStateOf(UiState.Loading) } DisposableEffect(*inputs) { state.value UiState.Loading repositoryCall { result - state.value when (result) { is Result.Success - UiState.Success(result.data) is Result.Error - UiState.Error(result.exception) } } onDispose { } } return state }当过滤状态issuesState或排序方向issuesOrder等inputs变化时DisposableEffect自动重启并回到 Loading 状态界面据此渲染加载指示器CircularProgressIndicator、错误提示或成功列表。LoadError/Error组件、Loader组件均位于 JetIssuesView.kt 中供列表与详情页复用。主题定制与时间显示示例在共享代码中自定义了一套 Material 浅色主题以 GitHub 红为品牌色见 JetIssuesView.ktval lightThemeColors lightColors( primary Color(0xFFDD0D3C), primaryVariant Color(0xFFC20029), secondary Color.White, error Color(0xFFD00036) )Issue 创建时间使用ocpsoft/prettytime库格式化为相对时间如 3 days ago并以灰色斜体呈现详情页作者信息通过AnnotatedString混排普通文本与加粗作者名private val timePrinter PrettyTime() private val ISSUE_DATE_STYLE TextStyle(color Color.Gray, fontStyle FontStyle.Italic) Composable fun CreatedBy(issue: IssuesQuery.Node) { val text AnnotatedString.Builder().apply { pushStyle(ISSUE_DATE_STYLE.toSpanStyle()) append(timePrinter.format(issue.createdAt as Date)) pop() issue.author?.login?.let { append( by ) pushStyle(SpanStyle(fontWeight FontWeight.Bold)) append(it) } }.toAnnotatedString() Text(text text) }常见问题与排查建议IDE 中 expect/actual 显示红色这是 HMPP层级多平台项目JVM 与 Android 中间源集共享代码的已知限制见 IssuesRepository.kt 头部注释不代表配置错误编译可正常通过。API 限流或鉴权失败GitHub GraphQL API 需要有效 token。建议设置环境变量GITHUB_TOKEN否则会使用内置演示认证串并受限于默认仓库JetBrains/compose-multiplatform。需要查看其他仓库可修改IssuesRepositoryImpl构造参数owner、name指向目标仓库。修改查询字段调整 issues.graphql 后构建期 Apollo 插件会重新生成类型安全代码。小结JetIssues 示例完整演示了 Compose Multiplatform 的核心开发范式一份共享 UI 源码JetIssuesView.kt 两个极薄平台入口 类型安全的 GraphQL 数据层。通过它你可以快速掌握多模块工程组织、BoxWithConstraints响应式布局、CompositionLocal 依赖注入、DisposableEffect驱动的异步 UI 状态管理以及桌面应用从:desktop:run开发调试到packageDistributionForCurrentOS打包交付的完整链路。其工程骨架:common:android:desktop可直接作为你构建自有桌面/Android 双端 Compose 应用的起点。【免费下载链接】compose-multiplatformCompose Multiplatform, a modern UI framework for Kotlin that makes building performant and beautiful user interfaces easy and enjoyable.项目地址: https://gitcode.com/GitHub_Trending/co/compose-multiplatform创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考