2025-09-05 09:04:06
作为一个使用了很多年 Java 的开发者☕️,面向对象思想早已深入人心,但 Kotlin 语言本身是天然支持函数式编程的,如果你对此没什么感觉,那么请多用 Compose 。
但是 Kotlin 的函数式编程思想并不是仅仅可以在 Compose 上发挥作用,我们在任何地方都可以使用函数式编程来解决问题。相对于面向对象,函数式在很多时候都具有独特的优势,我们可以灵活的在合适的地方用合适的方式来编写代码。
倒不是说面向对象不好,面向对象是用来解决特定问题的,它可以很好的对事物做抽象和建模,但我们也不能陷入面向对象的条条框框之中。
在 Android 开发中的函数式也有一些特有的问题需要解决,本文主要就是来介绍 Kotlin 函数式编程以及 Android 开发中的函数式编程。
函数式最早可以追溯到一百多年前,数学家希尔伯特提出了著名的希尔伯特计划:希望建立一种严格的数学基础,保证所有数学问题都能通过形式化方法解决。其中的关键问题之一就是可判定性问题(Entscheidungsproblem)。 这个问题在 1936 年被两个人同时解决,其中一个人是图灵,另一个人就是阿隆佐·邱奇。
邱奇在 1930 年代开发的 λ 演算,是建造自函数应用的一种计算形式系统。在 1937 年,艾伦·图灵证明了 λ 演算和图灵机是等价的计算模型,展示了 λ 演算是图灵完备性的。λ 演算形成了所有函数式编程语言的基础。
Kotlin 并不像 Haskell 一样是纯函数式编程语言,而是混合式的,其语法层面上是都支持的,我们可以灵活的切换编程方式,扬长避短。
函数式编程是一种编程范式,将函数作为一等公民(first-class citizen),避免使用程序的状态和定义对象及变量,函数可以作为参数也可以作为返回值,程序的运行则是利用若干简单的函数计算逐级推导至复杂运算,最终完成计算过程,返回计算结果。
在 Kotlin 中我们会有个顶级函数的概念,也就是在一个 kt 文件中直接声明函数,而不是将函数声明在类的内部。这意味着我们可以直接把函数当作一段逻辑计算的入口,不需要通过类创建对象这样的方式,这也是函数式中的重要概念,也可以说是一个支持函数式编程语言的必备特性。
如果都使用函数,那么就要解决依赖注入的问题,不过我知道的有些依赖注入框架是支持函数注入的,你可以直接在函数中加上参数,然后依赖注入框架会直接帮你注入。
typealias myFunction = () -> Unit
@Inject
fun myFunction(dep: Dep) {
}
当然我们的目的并不是打造一个纯函数式编程程序,既然是混合,那么类是完全可以接受的,我们仍然可以在类中使用依赖注入。
不过 Kotlin 似乎也考虑到了这一点,所以提供了 invoke 运算符重载,用过 UseCase 的人都知道这是怎么回事。
class UpdateActivityPubUserListUseCase @Inject constructor(
private val contentRepo: FreadContentRepo,
) {
suspend operator fun invoke(
content: ActivityPubContent,
allUserCreatedList: List<ListTimeline>
) {
// do something
}
}
看起来就是个普通的类,可以通过依赖注入在构造器内注入参数,区别在于使用了 operator
修饰符,重点是使用的时候可以像一个函数一样使用。
class XXXViewModel @Inject constructor(
private val updateActivityPubUserList: UpdateActivityPubUserListUseCase,
): ViewModel(){
init {
viewModelScope.launch {
// It’s actually calling the invoke method.
updateActivityPubUserList()
}
}
}
函数作为一等公民的意思是函数可以作为一种变量的类型而存在,它并不会因为是函数而有什么特别之处,函数可以作为变量存储、作为参数传递、作为返回值。
val add: (Int, Int) -> Int = { x, y -> x + y }
println(add(2, 3)) // 5
函数也可以接收其他函数作为参数。
fun operate(x: Int, y: Int, f: (Int, Int) -> Int): Int = f(x, y)
println(operate(2, 3, { a, b -> a * b })) // 6
总之在 Kotlin 中使用函数非常灵活,虽然 Kotlin for JVM 也会把函数编译成类对象,但这毕竟是 JVM 的限制,而 Kotlin 本身是跨平台的,只要在源码层面可以将函数视为一等公民那就没什么问题了。
幂等函数和纯函数都是函数式编程中的重要概念。幂等函数是指同样的输入多次执行结果保持不变。纯函数是指输入相同的参数输出的结果必然保持一致,并且没有副作用。
一个干净整洁的函数对于函数式编程来说非常重要,因为在函数式中函数的定义是一段特定的逻辑计算,它只接受函数的入参作为计算的依赖项,然后返回计算结果,通过连续地函数调用来完成整个复杂任务。除此之外不应该有任何其他的操作。既不应该依赖外部的状态,也不应该修改外部的状态。
写过 Compose 或者 React 的开发者应该对副作用的概念很清楚,在声明式 UI 框架中,UI 部分一般都是通过幂等函数来实现,但实际上我们总有一些 UI 之外的事情要做,或者我们总希望能获取到某些 UI 的信息或者时机,但是作为幂等函数我们是不能修改外部变量的,因此提供了专门的 API 来管理副作用。
也就是说,副作用可以理解为幂等函数运行过程中的一些额外的影响,为了保持幂等性,我们需要通过副作用 API 将本次运行函数的影响暴露出来。
@Composable
fun UserDetailPage(viewModel: UserDeailViewModel){
LaunchedEffect(Unit){
viewModel.onPageResume()
}
Scaffold {
// ...
}
}
不过,严格来说,Composable 其实并不能算是纯函数或者幂等函数,他们只是在设计上尽可能按照这个方向来设计,思想上保持一定程度的一致性。因为,首先将 UI 绘制到屏幕上本身就是一种副作用,所以 composable 的目的本来就是为了产生副作用。其次,composable 并没有返回值,不存在相同的输入就有相同的输出,可以说绘制 UI 就是 composable 的输出。只不过我们先了解了函数式编程后再来理解 composable 就容易多了。
函数式编程中的 不可变性 (Immutability) 指的是:一旦数据(属性、对象、集合)被创建,就不能被修改。如果数据发生变化那么应该创建一个新的数据,而不是在原值上改动。这样保证了函数调用之间不会“互相污染”,也避免了一些隐藏的副作用。
这样我们很自然的就想到了 Kotlin 中的 val
和 data class
,通过 val 声明的属性是不可变的,一般来说 data clas 也是不可变的,Kotlin 虽然没有严格的要求,但是强烈推荐 data class 中的所有属性都必须使用 val 声明,否则会出现一些奇怪的问题。
并且 data class 提供了 copy
方法用于复制一个对象副本,在复制的时候可以修改其中的某些属性,这也符合函数式编程中的不可变性。
此外 Kotlin 中的集合默认也都是不可变集合,像 *arrayOf*()
, *listOf*()
, *mapOf*()
这种方式创建的集合都是不可变集合,考虑到方便的创建不可变集合,Kotlin 还贴心的提供了 buildList{ }
这样的函数防止我们为了省事创建了可变集合。
所以我现在在开发过程中已经很少使用变量了,如果看到某个地方声明了一堆 var
真的会引起生理不适🤮。这样的好处就是心智负担很低,不用担心这么多变量组合出千变万化的状态,整个程序逻辑变得异常清晰。
毕竟,多个变量共同作用组合出的状态可以说是指数级增加的,整个系统也会变的更加难以理解难以测试,实际上函数式思想就是将软件变成对数据的计算过程,如果合理的设计函数,那么可读性也会大大提高。
当然了,我们开发软件大部分使用的仍然是面向对象,函数式还是作为面向对象的补充,并且其中的一些思想完全可以借鉴,比如我们虽然还是要创建一个类,但是其中的属性可以尽量不可变,也不必排斥顶级函数,这不是什么语法糖🍬,这是函数式编程的体现。不用坚持某些教条主义,在实践中灵活的改变编程范式或许能让代码更优雅更简洁。
The post Kotlin 函数式编程思想 first appeared on 张可的博客.
2025-08-05 09:42:02
Jetpack Compose Animation 前段时间终于支持了共享元素动画,这很大程度上提高了 Android 应用的用户体验,也降低了开发门槛,以前通过布局计算实现的方案既不优雅也很麻烦。
共享元素动画虽然和导航框架没关系,但是由于其使用上的一些限制,所以导航框架如果能直接支持是最方便的,目前在 Jetpack Navigation 中是已经支持的了,但是 Voyager 没有官方支持,好在 Voyager 的接口足够灵活,我们可以通过一些自定义的方式使其支持共享元素动画。
首先简单介绍一下共享元素动画。共享元素动画是指在不同的布局或者屏幕切换时共享其中的部分 UI 元素,被共享的元素会通过动画随着布局/页面切换过去,从而在视觉上提供一个很漂亮的专场。
在 Compose 中,有几个高级 API 可以帮助你创建共享元素:
SharedTransitionScope
,Composable 需要在 SharedTransitionScope
中才能使用共享元素修饰符。SharedTransitionScope
标记该 Composable 应该与另一个 Composable 进行匹配。SharedTransitionScope
标记该 Composable 的边界应该作为转换发生的容器边界。与 sharedElement()
不同,sharedBounds()
是为视觉上不同的内容而设计的。使用方式如下:
var showDetails by remember { mutableStateOf(false) }
SharedTransitionLayout {
AnimatedContent(
showDetails,
label = "basic_transition"
) { targetState ->
if (!targetState) {
MainContent(
onShowDetails = {
showDetails = true
},
animatedVisibilityScope = this@AnimatedContent,
sharedTransitionScope = this@SharedTransitionLayout
)
} else {
DetailsContent(
onBack = {
showDetails = false
},
animatedVisibilityScope = this@AnimatedContent,
sharedTransitionScope = this@SharedTransitionLayout
)
}
}
}
@Composable
private fun MainContent(
onShowDetails: () -> Unit,
modifier: Modifier = Modifier,
sharedTransitionScope: SharedTransitionScope,
animatedVisibilityScope: AnimatedVisibilityScope
) {
Row {
with(sharedTransitionScope) {
Image(
painter = painterResource(id = R.drawable.cupcake),
contentDescription = "Cupcake",
modifier = Modifier
.sharedElement(
rememberSharedContentState(key = "image"),
animatedVisibilityScope = animatedVisibilityScope
)
.size(100.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop
)
}
}
}
@Composable
private fun DetailsContent(
modifier: Modifier = Modifier,
onBack: () -> Unit,
sharedTransitionScope: SharedTransitionScope,
animatedVisibilityScope: AnimatedVisibilityScope
) {
Column {
with(sharedTransitionScope) {
Image(
painter = painterResource(id = R.drawable.cupcake),
contentDescription = "Cupcake",
modifier = Modifier
.sharedElement(
rememberSharedContentState(key = "image"),
animatedVisibilityScope = animatedVisibilityScope
)
.size(200.dp)
.clip(CircleShape),
contentScale = ContentScale.Crop
)
}
}
}
首先需要解决如何传递 SharedTransitionScope
以及 AnimatedVisibilityScope
的问题。因为 sharedElement
动画需要用到这两个 scope,如果一路传递下去很麻烦,因此我们可以通过过 CompositionLocalProvider
将其传递下去。
val LocalAnimatedVisibilityScope = compositionLocalOf<AnimatedVisibilityScope?> { null }
val LocalSharedTransitionScope = compositionLocalOf<SharedTransitionScope?> { null }
因为 sharedElement
必须发生在 SharedTransitionLayout
内部,即使是不同的页面也需要这样,所以还需要在 Navigator
外层包一个 SharedTransitionLayout
。
SharedTransitionLayout {
CompositionLocalProvider(
LocalSharedTransitionScope provides this
) {
Navigator(
screen = remember { FreadScreen() },
) { navigator ->
CurrentAnimatedScreen(navigator)
}
}
}
剩下的问题就是如何提供 AnimatedVisibilityScope
,我们可以使用 AnimatedContent
composable 来解决这个问题,并且 Voyager 的 Navigator
可以自定义 content
,那么我们可以在自定义 content
内加上 AnimatedContent
。
@Composable
fun CurrentAnimatedScreen(navigator: Navigator) {
val currentScreen = navigator.lastItem
AnimatedContent(
targetState = currentScreen.key,
) { targetScreenKey ->
CompositionLocalProvider(
LocalAnimatedVisibilityScope provides this
) {
val targetScreen = navigator.items.lastOrNull { it.key == targetScreenKey }
if (targetScreen != null) {
navigator.saveableState("currentScreen", screen = targetScreen) {
targetScreen.Content()
}
}
}
}
}
这样我们就可以在给任意一个 composable 加上 sharedElement transitions 了。
当然,为了方便使用,我们还可以提供一个这样的扩展函数。
@Composable
fun Modifier.sharedBoundsBetweenScreen(key: String): Modifier {
val animatedVisibilityScope = LocalAnimatedVisibilityScope.current
val sharedTransitionScope = LocalSharedTransitionScope.current
if (animatedVisibilityScope == null || sharedTransitionScope == null) return this
return with(sharedTransitionScope) {
sharedBounds(
sharedContentState = rememberSharedContentState(key),
animatedVisibilityScope = animatedVisibilityScope,
)
}
}
不过目前还发现了一个问题,在进入新的页面时动画是正常的,但是推出动画却不起作用,目测是 Voyager 本身 pop screen 的机制导致的,composition 没有捕获到退出前的那个 element,因此无法完成动画,这个问题不太容易解决,我试几个方案都不是很好。
The post 在 Voyager 中使用 SharedElement 共享元素动画 first appeared on 张可的博客.
2025-07-15 21:29:25
本文主要介绍一下 KMP/CMP 项目的组织结构和集成方式,从而概览整个技术架构,对其有个总体上的认知。
对于如何创建 KMP 项目可以直接使用官方的工具:
具体的调试步骤和环境可以看官方文档:https://www.jetbrains.com/help/kotlin-multiplatform-dev/quickstart.html
官方的项目结构文档:https://www.jetbrains.com/help/kotlin-multiplatform-dev/multiplatform-discover-project.html
KMP/CMP 参考项目 Fread:https://github.com/0xZhangKe/Fread
首先 KMP 项目目前仍然使用 Gradle 构建,因此项目结构和我们的 Android 项目大体上类似,但是源码文件夹有所不同,毕竟是跨平台项目,肯定存在一些代码是各端独自实现的情况,那么肯定也需要不同的平台文件夹存放该平台特有的代码及实现。
所以一个 KMP 项目的模块结构如下:
目标平台有 Android 和 iOS,commonMain 则表示所有平台的共享代码。
上图中的 commonMain
中存放的就是通用代码,这里的代码可以被所有平台使用,也会参与所有平台的构建流程。
其中的代码与平台无关,也不可以使用任何平台独有的 Api,只能使用其它模块或者支持 KMP 的依赖库中的 common code。当然目前也是好起来了,越来越多的三方库都在已经支持了 KMP,大部分情况下都有现成的库直接用。
对于不同的编译目标平台,commonMain
中的代码也会编译成不同的产物,这一切都是 Kotlin 编译器的功劳。
目标平台定义了 common code 将会被编译到哪些平台。对于不同的平台编译的产物也不同,比如 androidTarget
产物是 aar,iOS 产物是 framework
。
另外 Target
本身只是一个标识符,用于标识不同的目标平台,编译器将会根据这些目标平台的不同来编译出不同的产物。包括依赖包管理也是通过 Target
标识的。
我们可以通过在 gradle 的 kotlin
块内管理 target:
kotlin {
jvm() // Declares a JVM target
iosArm64() // Declares a target that corresponds to 64-bit iPhones
}
通过声明 jvm
和 iosArm64
目标,commonMain
中的代码将被编译为这些目标:
Target 还提供了 Gradle DSL 用于一些配置,其中包括通用配置和平台独有的配置。
通用配置是指在任何 target 内都可以使用的配置。
name | Description |
---|---|
platformType |
目标平台类型,取值范围:jvm 、androidJvm 、js 、wasm 、native 、common
|
artifactsTaskName |
构建此目标的最终产物的任务名称 |
components |
用于设置 Gradle 发布内容的组件 |
compilerOptions |
用于该目标的编译器选项,此声明会覆盖在顶层配置的任何 compilerOptions {} 设置。 |
此外还有一些平台独有的配置。
kotlin {
wasmWasi {
nodejs()
binaries.executable()
}
js().browser {
webpackTask { /* ... */ }
}
linuxX64 {
binaries {
executable {
// Binary configuration.
}
}
}
androidTarget {
publishLibraryVariants("release")
}
}
具体配置可以看官方文档:https://www.jetbrains.com/help/kotlin-multiplatform-dev/multiplatform-dsl-reference.html
Kotlin Source set 是一组具有各自 Target、依赖项和编译器选项的源文件。它是在 KMP 项目中共享代码的主要方式。
Source Set 的特性:
Kotlin 提供了一系列预定义的源集。其中之一是 commonMain
,它存在于所有 KMP 项目中,并编译为所有声明的目标。
在 Gradle 脚本中,我们可以通过 kotlin.sourceSets {}
块内的名称访问 Source set:
kotlin {
sourceSets {
commonMain {
// Configure the commonMain source set
}
}
}
这里的 commonMain 对应的就是我们上面第一张截图中的那个 commonMain 文件夹,一般来说每个常规的 source set 都会有一个自己的代码文件夹,不同模块中的相同的 source set 文件夹中的代码可以互相调用,因为他们最终都会被编译到同一个产物内。
因此 source set 主要是用来管理该 Target 下的代码集合的,比如为该 Target 添加某些依赖库,设置源码路径等。
跟 Target 类似的是,source set 也提供了针对平台特有的设置。
在设置依赖库的时候我们经常会用到这个功能,比如我们现在需要一个视频播放器,但是没有找到适合业务的跨平台播放器组件,以此需要各端独自实现,那么我们在 Android 端需要依赖 media3,iOS 端需要依赖另外一个视频播放器的组件,那么就可以通过 source set 设置不同的依赖。
kotlin {
sourceSets {
androidMain {
dependencies {
implementation(libs.media3)
}
}
iosMain {
dependencies {
implementation(libs.ios.video.player)
}
}
}
}
通过这样的方式,我们就可以针对不同的平台添加不同的依赖,这些依赖可以在各自的文件夹中依赖到,比如我们可以在 androidMain
文件夹中直接使用 media3
,在 iosMain
中使用 iosVideoPlayer
。
在编译阶段,编译器会针对当前编译的 Target 选择使用不同的 source set,比如编译到 Android 平台,那么会选择 commonMain
+androidMain
的代码和依赖库进行编译。
除了上述我们说到的普通 source set 之外,还有一种叫做中间源集。
中间源集的概念其实很简单,比如我们现在的 KMP 项目同时支持了 Android、 iOS 和 macOS,现在我们需要一个用于生成 UUID 的函数,这个函数各端各自实现。但实际上 iOS 和 macOS 的实现是一致的,我们总不能同样的代码在 iosMain
和 macosMain
中各写一份吧,这样很容易出错。
于是 KMP 提供了中间源集用于解决这个问题,在上述情况中,我们可以直接将代码写在 appleMain
中,appleMain
可以使用 Apple 平台的能力,并且在 iosMain
和 macosMain
中都可以依赖到其中的代码。
完整的层次结构如下,其中彩色的部分可以理解为 Intermediate source sets.
从上面的示例和截图中可以看出来,source set 源码文件夹都带了一个 Main/Test 后缀,Main 包含生产代码,Test 后缀就表示该 source set 的测试代码,他们之间会自动建立关联,Test 可以直接依赖到 Main 中的代码。
Kotlin 也提供了支持 KMP 的默认测试框架,其中包含@kotlin.Test
注解以及各种断言方法,例如assertEquals
和assertTrue
。
当然我们也可以像常规测试一样,为每个平台在其各自的 source set 中编写平台特定的测试。也可以为每个 source set 设置平台特定的依赖项,例如 JUnit
针对 JVM 和 XCTest
iOS 的依赖项。要针对特定目标运行测试,请使用<targetName>Test
任务。
dependsOn
是两个 source sets 之间的特定关系,通过 dependsOn
将整个 source sets 关系设置为一个树状结构,不过一般来说我们并不需要手动设置。
kotlin {
// Targets declaration
sourceSets {
// Example of configuring the dependsOn relation
iosArm64Main.dependsOn(commonMain)
}
}
在某些情况下,我们可能需要在项目中设置自定义中间源集。假设有一个项目会编译到 JVM、JS 和 Linux,并且只想在 JVM 和 JS 之间共享一些源。在这种情况下,我们应该为这对目标找到一个特定的源集。
Kotlin 不会自动创建这样的源集,但我们可以使用 by creating
手动创建它:
kotlin {
jvm()
js()
linuxX64()
sourceSets {
// Create a source set named "jvmAndJs"
val jvmAndJsMain by creating {
// …
}
}
}
但此时我们自己创建的 jvmAndJsMain
的依赖关系是独立的,因为我们并未手动指定任何依赖关系,此时就需要使用上面说的 dependsOn
了。
kotlin {
jvm()
js()
linuxX64()
sourceSets {
val jvmAndJsMain by creating {
// Don't forget to add dependsOn to commonMain
dependsOn(commonMain.get())
}
jvmMain {
dependsOn(jvmAndJsMain)
}
jsMain {
dependsOn(jvmAndJsMain)
}
}
}
现在,该项目的依赖关系如下:
首先,我们可以通过如下方式给不同的 source set 添加不同的依赖库:
kotlin {
sourceSets {
commonMain.dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
}
jvmMain.dependencies {
implementation("com.google.guava:guava:32.1.2-jre")
}
}
}
被添加到 commonMain
中的依赖库会被传递到所有 Target 中,而添加到 jvmMain
中的依赖库只会在 jvmMain
文件夹中可见。
Kotlin 会把每个依赖关系解析为一组 source sets,而这组 source sets 中的每一个都必须和当前使用它的源集(consumer source set)在目标平台上兼容。
也就是说你添加的依赖库的 target 列表必须包含你的项目的 target 列表,缺少一个都会被视为不兼容。
举例来说,如果你当前的项目 target 中包含了 jsMain,但是你打算添加的依赖库的 target 中未包含 jsMain,那么就会被视为不兼容,那么就无法使用这个库。
Kotlin Multiplatform 的 common 源集(commonMain
)会被编译多次(为不同 Target),因此依赖库版本必须一致。
为什么要对齐版本(align versions)?
commonMain
会参与多个目标(如 Android、iOS 等)的构建。commonMain
使用的依赖版本不一致,会导致编译出来的 .klib 不一致,从而出问题。commonMain
的地方依赖的库版本相同。举个例子:
首先 commonMain
中声明了如下依赖:
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
同时 androidMain
中又声明了如下依赖:
implementation("androidx.navigation:navigation-compose:2.7.7")
但是这个 navigation
库内部依赖了 coroutines 1.8.0
。
那么问题来了:
androidMain
和 commonMain
是要一起编译的commonMain
也会用 1.8.0 的 coroutines
commonMain
的代码iosMain
等所有平台
Kotlin Multiplatform 项目中,只要有任何 source set 引入了较高版本的共享依赖,Gradle 就会自动将这个版本同步到所有需要它的 source set 上,以确保生成的 common 代码在所有平台上都是一致的。
对于 iOS 平台来说,KMP 项目的代码会直接编译成 framework 产物给到 iOS 项目集成继续参与 Xcode 的编译流程。那么具体来说是如何集成的呢?比如我们直接 clone 一个 KMP/CMP 项目到本地然后直接用 Xcode 打开可以直接运行,那么 Kotlin 代码如何开始编译?
Xcode 构建阶段(Build Phase)可以添加一些自定义脚本,对于一个默认支持了 KMP 的 Xcode 项目来说,构建阶段会被添加一行自定义脚本,该脚本用于执行一个 gradle 命令以此生成 Xcode 需要的 framework 文件。
我们可以打开 Xcode 项目视图中的 Build Phase 看到这个脚本:
其实我们也可以自己运行这个脚本,运行完成后会看到 build 文件夹下生成了 framework 文件。
好了现在 frameworks 已经生成了,剩下的就是如何把这这个文件添加到 Xcode 项目的依赖中去。
我们接着打开 Xcode 的项目视图中的 Build Settings 往下找到 Search Paths 栏,可以看到 gradle build 目录中的 frameworks 路径已经被添加到项目中了。
这样整个 Xcode 的构建环节就搞明白了,首先通过自定义编译脚本调用 gradle 编译出 framework 文件,并且预先将该路径添加到了 Xcode 构建依赖中,然后 Xcode 内就可以把 KMP 模块当作一个普通的 framework 依赖库使用了。
当然这些配置也可以根据自己的项目需要修改,具体的配置也可以在 project.pbxproj 文件内看到。
首选还是直接看官方文档:https://developer.android.com/kotlin/multiplatform/migrate
因为 Kotlin for iOS 代码最终会编译成 framework,所以如果现有的 Xcode 项目如果想集成 KMP 其实也可以不必设置编译脚本,也不必对现有项目做其他的更改,只需要像依赖普通的库一样依赖这个 Kotlin 的产物即可。
The post 一个KMP/CMP项目的组织结构和集成方式 first appeared on 张可的博客.
2025-04-27 13:08:14
大家好,Fread 项目开发到今天已经有两年半了,上线也已经八个月了,目前项目趋于稳定,现在决定使用 Apache 2.0 协议将其开源。
https://github.com/0xZhangKe/Fread
首先介绍下 Fread 的技术栈。
Fread 是一个使用 KMP(Kotlin Multiplatform) 和 CMP(Compose Multiplatform) 的移动客户端应用,所以编程语言使用的是 Kotlin,技术栈也都是基于 Kotlin 的,主要如下。
上面列举了一些具有代表性的依赖库,除此之外还有很多其他的库没有列举出来,从这些具有代表性的库可以大概了解到 Fread 的技术栈。
Fread 是一个去中心化的联邦宇宙 Micro blogging 社交客户端,目前已经支持了 Mastodon、Bluesky、RSS 三种社交平台协议,这意味着你可以在同一个 App 中同时使用这三种社交平台,Fread 不仅提供了 Micro blogging 社交的一致性,也保持了不同平台的特色功能。 更重要的是,Fread 支持创建一个同时包含了三种来自不同平台的 Feeds 流,这打破了协议之间的壁垒,进一步增强了去中心化的能力,另外 Fread 也专注于提供漂亮舒适的 UI/UX。
Fread 之所以现在决定开源,一方面是刚开始没考虑好到底是付费下载还是免费试用,但是上线一周后就直接改成了免费下载,但是之前因为是闭源所以仓库中有一些敏感的数据信息,担心开源后会泄漏出去,现在已经解决了这个问题。另外我对 Fread 有不少设想和规划,开源后需要面临其他人提交 PR 的情况,这可能会打乱开发节奏,所以在最近支持了 Bluesky 之后,项目也稳定下来,才开始着手开源的事情。
目前 Fread 虽然使用了 KMP 跨平台,iOS 也能运行起来,但是只上架了 Android 版,iOS 还有一些适配工作没完成,未来适配完成后会上架 App Store。
之前为了监控线上 Crash 情况和用户量所以接入了 Firebase analytics,后来又因为消息推送接入了 FCM,并且为了中转 FCM 消息我自己搭建了一台中继云服务,这里面因为涉及到我自己的 Firebase 账户和云服务信息,所以这部分的内容从 Fread 仓库中移除了,这个在我的私有仓库,可以作为 Fread 子模块进行编译,并且设置了可选编译,意思是指通过 Fread 主仓也可以编译,功能没有什么变化,只是会缺少 Firebase 的能力,其他没有任何影响,但是我这里可以编译出带有 Firbase 能力的包并且上架 Google Play,未来在 F-droid 上架的版本也会是不包含 Firebase 的版本。
关于 Fread 更多的技术细节大家可以直接查看代码,未来我可能会继续发布一些博客介绍 Fread 中的一些设计和细节。
https://play.google.com/store/apps/details?id=com.zhangke.fread
The post 历时两年半开发,Fread 项目现在决定开源 first appeared on 张可的博客.
2025-04-22 22:25:46
Fread 初版上线之后的大部分时间除了使用 Kotlin Multiplatform 兼容 iOS 版本之外,剩下的绝大部分时间都是在支持 Bluesky 社交平台,这个工作从今年年初开始持续到最近终于完成,现在下载最新版本的 Fread 即可体验。
Fread 是一个去中心化的 Micro blogging 社交客户端,目前已经支持了 Mastodon、Bluesky、RSS 三种社交平台,这意味着你可以在同一个 App 中同时使用这三种社交平台,Fread 不仅提供了 Micro blogging 社交的一致性,也保持了不同平台的特色功能。
更重要的是,Fread 支持创建一个同时包含了三种来自不同平台的 Feeds 流,这打破了协议之间的壁垒,进一步增强了去中心化的能力。另外 Fread 也专注于提供漂亮舒适的 UI/UX,因为 Fread 碰巧有一位专业的设计师。
使用上也非常简单,在添加内容页面可以看到第一个就是 Bluesky,点进去登录即可。
登录完成后进入首页大概长这个样子,这里的 TAB 和在网页端的一致。
Fread 还提供了一个 Bluesky 不太好用的功能,就是拖动 TAB 排序(Bluesky 的排序功能一直报错),在首页左侧的抽屉中点击编辑 Bluesky 按钮,进入编辑 TAB 页面,这里不仅可以新增/删除 Feeds,还可以拖动排序。
Bluesky 特有的引用帖子的功能在新版本中也得到了支持,帖子样式新增了引用类型,另外在帖子下面的操作按钮行中也新增了一个引用按钮,点击后进入发帖页面会有引用帖子的展示。
本次改版还包括对发帖页面的重构,对于回复内容,现在页面会提供被回复内容的展示,如下图所示。
不仅仅是 Bluesky 的发帖页面,Mastodon 的发帖页面同样也包含了这些改动。未来还会继续支持同时将一个帖子发布至多个账号。
此外一些其他的主要功能 Fread 也已经支持,比如通知中心,个人信息修改等等。不过私信功能目前还未支持,这个做起来比较麻烦,会在发帖功能重构完成后开始。
以上就是 Fread 最新版本的主要改动,欢迎大家下载体验,完全免费,近期有计划将 Freed 开源出来,届时也欢迎大家提 bug 和 PR。
大家也可以关注我的长毛象和 Fread 官方账号,同步发布关于 Fread 最新的动态。
https://play.google.com/store/apps/details?id=com.zhangke.fread
The post Fread 最新版本已支持 Bluesky first appeared on 张可的博客.
2025-03-16 20:26:15
距离上一次写电影记录已经好久了,最近又看了好几部影视剧,然后打算把看的书也记录一下,但是好多剧情都忘了,没办法写的很详细,以后打算看完一部就在豆瓣写个短评。
(WordPress 好像还不支持 NeoDB /豆瓣链接的 embed?一个个截图好麻烦)
哲学的历程还在看(已经看了好久了好没看完),最近看了大卫休谟到康德。
康德在整个哲学史中都有非常重要的意义,他把人类从休谟的怀疑论中解救出来,不过他的理论也很难懂,我还没看完,感觉哲学史从黑暗中世界开始开始到康德这里才越来越有意思起来。
一本书速通中国政治体制和主要事物。系统的了解了一下政府结构和行事方式,已经看到了中国改开之后经济发展的大致脉络,挺好的一本书,内容不多,很快就能读完,推荐。
这本书是关于养娃的,有娃之前我老婆就让我看这本书,主要是孩子大脑的特性和家长的教育方式,也会涉及部分孩子大脑的原理,了解了原理就更容易养娃,建议所有家长都去看,很棒的一本书。
还没看完,关于贝叶斯主义的介绍,贝叶斯主义不仅是一种概率计算方式,也是一种思考方式,甚至是看待客观世界的方式,相比较于传统概率论,贝叶斯主义更注重概率随着条件变化而变化,并且会给出概率分布,并且贝叶斯主义会纳入主观偏见调整计算参数,从而得到更准确结论。对我来说,这个想法确实会影响我的思考方式,降低心里负担。
又一部安哲的电影,我太喜欢他的电影中的调调了,他的空镜头和长镜头可以说非常厉害。
雾中风景,绝望、冷漠、孤寂的一部电影,姐弟俩位了寻找一个不存在的父亲踏上了旅程,安哲用很多镜头和片段隐喻电影的主题,未知的旅程就是迷雾,沿途的风景就是他们的经历,这不是喜剧,也不能算是悲剧,这就是人生。弟弟亚历山大似乎有某种宗教隐喻,当然我没看明白。
贾樟柯的新电影,主演仍然是他老婆赵涛,贾樟柯是真的很喜欢拍基层文艺从业者了,全电影通过赵涛视角来了个华语金曲大串烧。我挺喜欢这种平凡视角下的审美,我觉得美是存在于任何一个角落。这个电影故事线很长,社会变迁差不多也是我从童年到现在,所以很有代入感。
终于等来了第二季,没想到还是原来的剧情,而且嘎然而止,没看爽,没了第一季那种新鲜感这部剧也就平平无奇了,孔刘公园踩面包真的很帅。
一个俄罗斯电影,故事发生在俄罗斯北部,这是我最喜欢的地方之一,大片寒冷的平原,冷冽的寒风和刺骨但是干净的海水,这都是我最爱的东西,所以这部电影光看风景就够了。剧情原本以为挺温馨的,结果并不是,有很强的政治隐喻意味,不过不考虑隐喻单纯的剧情也很有意思了。
闲着没事看的喜剧片,没想到这竟然还是个系列电影,不过我只看了第一部,当初上映时热度挺高的,我看了感觉一般,笑点也还行,纯纯的商业片。
姜文和葛优,光看这俩演员就不错了,挺好的电影竟然一直没看。影片是关于秦始皇和高渐离的故事,也是权利和艺术的故事,至少从秦朝开始,文化就是权利的附庸者,对待文人,权利有一百种方式让其臣服,即使今天也是这样。
中年男人的身份认同危机,从好莱坞到百老汇,努力摆脱商业片印象,开始追求艺术,最后甚至付出自己的一切。
超现实主义、荒诞、长镜头、话剧与电影,难怪评分这么高。
Movie – Birdman or (The Unexpected Virtue of Ignorance)
这是一部通过电影从业者们的故事开始的电影艺术史,既有从小人物到好莱坞风云人物的故事,也有从黑白电影到如今 IMAX 3D 电影的故事,好莱坞恢弘的史诗导演完全给拍出来了。不过最后那段混剪虽然剪的很好但有种在看 B 站的感觉。
作为一个占尽性别红利的男人确实不太敢对这部电影有什么评价,但是拍的真不错,喜欢。导演或者编剧真的很懂老中了,很多微妙的小想法小细节都给拍的清清楚楚。
没看过原著和其他版本,开头真的很吓人,但故事还是很温暖的,韩剧太强了。
本来以为会很像迷雾类型的,结果也确实有一点像,不过远没有迷雾那种克苏鲁的恐怖感,也是闲着没事看的,也还可以,没有很大的场面,整个剧情都发生在半座桥上。
太炸裂了,看之前完全没想到会这么炸裂。看了介绍和名字以为是科幻片,看了开头以为是鸟人类型的片,看完发现是惊悚+讽刺男权片。
娱乐圈真的是人吃人(物理上),女演员不得不迎合男性审美,这一切都太过恶心。
太长了导致我现在才看,而且系列电影我更偏向于只看第一部,帕西诺确实很帅,年轻和老了都很帅。
双线叙事,一个是从零创业的年轻人,一个是开拓新市场的年轻人,不同的时代,不同的境况,放在一起还是挺有意思的,比第一部好看点。
Movie – The Godfather: Part II
大年初一就去看了,第一部我是真喜欢,第二部其实也还可以,每个电影都有缺点,第二部的缺点我倒是觉得也还好,无伤大雅,不至于这么低的分数,我应该可以给到 7.5 分,只是女性奉献自己成全男性这样的桥段还是很下头,剧情也比较单调,其实邓婵玉的性格已经塑造的非常血性了,只不过恋爱桥段和最后奉献自己让电影大打折扣,不过还是比哪吒2好多了。
Movie – Creation of The Gods Ⅱ: Demon Forces Demon
作为一个电影院爱好者,院线电影好一点的基本都去看,除非没空。
哪吒 2 票房高的离谱,我最多打 7 分。笑点真的很尬,人物建模真的很丑,屎尿屁笑点真的低俗,剧情也真的简单。打斗场面说来就来,为了燃而燃,所以后面看麻了已经没啥感觉了,总之结尾燃一下就可以高分。
易烊千玺真的很想摆脱路人对他的印象了,这个电影一点也不端着,这一点比别的流量明星好多了。
我挺喜欢这种拍小人物、小故事、少数群体的电影,他们需要被公众看到,他们的需求应该被满足。
电影本身拍的也挺好的,只是为了凸显外婆的特质显得有点跳脱和刻意,脑瘫患儿的恋爱线感觉也很棒,情感需求和性需求都是人类的基本需求,没什么不好意思说的,不必谈性色变。
又一部黑帮电影,之前一直以为看过了呢,结果没看。我挺喜欢这种从小人物视角开始的黑帮电影,剧情更容易理解,人物塑造也能随着剧情推进逐渐丰满。
第二部终于出来了,这次是哪都通的几个顶尖异人联手去碧游村打怪。特效感觉很可以了,以前国产剧特效太过离谱,这个至少看起来很和谐,不奇怪,国产特效要是都能做到这个水准就行了。只是里面没有太大的场面,特效没有那么多的视觉冲击力。
The post 书影音记录:Dec 13, 2024-Mar 15,2025 first appeared on 张可的博客.