從 SwiftUI 到 Jetpack Compose:一份給 iOS 開(kāi)發(fā)者的狀態(tài)管理指南

從 SwiftUI 到 Jetpack Compose:一份給 iOS 開(kāi)發(fā)者的狀態(tài)管理指南

對(duì)于一個(gè)經(jīng)驗(yàn)豐富的 SwiftUI 開(kāi)發(fā)者來(lái)說(shuō),踏入 Android 的 Jetpack Compose 世界是一次既熟悉又新奇的旅程。兩種框架都擁抱了聲明式 UI 的思想,讓我們能夠通過(guò)描述“UI 應(yīng)該是什么樣子”來(lái)構(gòu)建界面,而不是手動(dòng)去一步步操作 UI 組件。

然而,盡管核心理念相似,狀態(tài)管理的具體實(shí)現(xiàn)、術(shù)語(yǔ)和最佳實(shí)踐卻存在著微妙而重要的差異。這篇文章的目的就是為你搭建一座橋梁,將你已有的 SwiftUI 知識(shí)平滑地遷移到 Jetpack Compose 中,讓你能夠自信地處理各種場(chǎng)景下的狀態(tài)。

核心概念對(duì)比:一切從 @State 開(kāi)始

在 SwiftUI 中,最基礎(chǔ)的狀態(tài)管理單位是 @State 屬性包裝器。它讓 View 能夠擁有和觀察一個(gè)本地的、簡(jiǎn)單的值類(lèi)型狀態(tài)。

SwiftUI:

struct CounterView: View {
    // 當(dāng) count 改變時(shí),SwiftUI 會(huì)重新渲染 body
    @State private var count: Int = 0

    var body: some View {
        VStack {
            Text("You clicked \(count) times")
            Button("Click me") {
                count += 1
            }
        }
    }
}

現(xiàn)在,讓我們看看在 Compose 中如何實(shí)現(xiàn)完全相同的功能。

Jetpack Compose:

import androidx.compose.runtime.*
import androidx.compose.material3.*
import androidx.compose.foundation.layout.*

@Composable
fun CounterScreen() {
    // 1. `mutableStateOf` 創(chuàng)建一個(gè)可觀察的狀態(tài)持有者
    // 2. `remember` 告訴 Compose 在重組(recomposition)之間“記住”這個(gè)狀態(tài)
    var count by remember { mutableStateOf(0) }

    Column {
        Text("You clicked $count times")
        Button(onClick = { count++ }) {
            Text("Click me")
        }
    }
}

讓我們分解一下 Compose 的版本,這是理解差異的關(guān)鍵:

  1. mutableStateOf(0): 這是狀態(tài)的真正“容器”。它創(chuàng)建了一個(gè) MutableState<Int> 類(lèi)型的對(duì)象。你可以通過(guò)訪問(wèn)它的 .value 屬性來(lái)讀寫(xiě)其值。
  2. remember { ... }: 這是 Compose 中至關(guān)重要的概念,也是與 SwiftUI @State 最顯著的不同點(diǎn)。 Composable 函數(shù)本質(zhì)上是普通的 Kotlin 函數(shù),每次界面需要更新時(shí)(這被稱(chēng)為重組 / Recomposition),這個(gè)函數(shù)就會(huì)被重新調(diào)用。如果沒(méi)有 remember,mutableStateOf(0) 會(huì)在每次重組時(shí)都被重新執(zhí)行,狀態(tài)將永遠(yuǎn)無(wú)法被保存。remember 的作用就是告訴 Compose:“請(qǐng)?jiān)诙啻沃亟M之間,為我保留這個(gè)代碼塊里創(chuàng)建的對(duì)象實(shí)例?!?/li>
  3. by 關(guān)鍵字: 這是 Kotlin 的 屬性委托 語(yǔ)法糖。它讓我們能夠直接讀寫(xiě) count,而無(wú)需每次都寫(xiě) count.value。這使得代碼看起來(lái)更接近 SwiftUI 的直接屬性訪問(wèn)。

iOS 開(kāi)發(fā)者速記:

  • SwiftUI 的 @StateCompose 的 remember { mutableStateOf(...) }
  • remember 是防止?fàn)顟B(tài)在 UI 刷新時(shí)被重置的關(guān)鍵。忘記它,是每個(gè)初學(xué)者都會(huì)犯的錯(cuò)誤。

深入探討:UI 構(gòu)建塊與“純函數(shù)”的微妙差異

在你深入學(xué)習(xí)狀態(tài)管理之前,理解兩個(gè)框架在 UI 構(gòu)建基石上的哲學(xué)差異至關(guān)重要。這解釋了為什么它們的狀態(tài)管理機(jī)制如此不同。

純函數(shù)的核心特征是:無(wú)副作用、相同的輸入永遠(yuǎn)產(chǎn)生相同的輸出。

SwiftUI:值類(lèi)型(Struct)作為視圖

SwiftUI 并非一個(gè)純函數(shù)式框架,它是一個(gè)“值類(lèi)型驅(qū)動(dòng)”的框架,并大量借鑒了函數(shù)式思想。

  • 視圖是 struct:你的 View 是一個(gè)值類(lèi)型的結(jié)構(gòu)體。它的 body 計(jì)算屬性很像一個(gè)純函數(shù)——給定的狀態(tài)輸入,會(huì)渲染出確定的 UI 輸出。
  • 狀態(tài)引入副作用:然而,通過(guò) @State 等屬性包裝器,這個(gè) struct 實(shí)際上持有并管理著自己的狀態(tài)。當(dāng)狀態(tài)改變時(shí),SwiftUI 會(huì)創(chuàng)建一個(gè)新的視圖實(shí)例來(lái)替換舊的。這種“狀態(tài)與視圖結(jié)構(gòu)體綁定”的設(shè)計(jì)意味著視圖本身并非無(wú)狀態(tài)的,因此不符合純函數(shù)的嚴(yán)格定義。
  • structclass 的分工:SwiftUI 清晰地利用了 Swift 的類(lèi)型系統(tǒng)。struct 用于定義視圖和輕量數(shù)據(jù)模型,利用其值語(yǔ)義確保數(shù)據(jù)隔離。而 class (特別是 ObservableObject) 則用于跨視圖共享的、更復(fù)雜的狀態(tài)和業(yè)務(wù)邏輯,扮演引用類(lèi)型的角色。

Jetpack Compose:以純函數(shù)為核心

Jetpack Compose 更嚴(yán)格地遵循“純函數(shù)”理念。

  • UI 是 @Composable 函數(shù):你的 UI 組件不是 structclass,它就是一個(gè)被 @Composable 注解的函數(shù)。
  • 函數(shù)不持有狀態(tài):按照設(shè)計(jì),函數(shù)本身不應(yīng)該持有狀態(tài)。它們接收狀態(tài)作為參數(shù),然后根據(jù)這些參數(shù)描述 UI。
  • remember 的角色:那么狀態(tài)存在哪里?remember 就是那個(gè)“魔法”。它告訴 Compose 框架:“請(qǐng)?jiān)谀愕膱?zhí)行上下文中為我保留這個(gè)值,即使我的函數(shù)被反復(fù)調(diào)用?!?這意味著狀態(tài)是被 Compose 運(yùn)行時(shí)所管理,而不是被 Composable 函數(shù)本身所擁有。這使得 Composable 函數(shù)在每次調(diào)用時(shí),其行為更接近純函數(shù)——它的輸出只依賴(lài)于它的輸入?yún)?shù)和由 remember 提供的穩(wěn)定狀態(tài)。

核心差異總結(jié)

維度 SwiftUI Jetpack Compose
UI 載體 以值類(lèi)型結(jié)構(gòu)體 (struct) 為視圖單位,通過(guò) body 計(jì)算屬性描述 UI。 @Composable 函數(shù)為 UI 單位,直接在函數(shù)體內(nèi)構(gòu)建 UI。
狀態(tài)與純函數(shù) 視圖結(jié)構(gòu)體可通過(guò)屬性包裝器持有狀態(tài),狀態(tài)是視圖的一部分,因此視圖本身并非純函數(shù)。 @Composable 函數(shù)本身不持有狀態(tài),狀態(tài)由 remember 在函數(shù)外部的上下文中“暫存”,函數(shù)更接近純函數(shù)。
不可變性處理 依賴(lài)結(jié)構(gòu)體的值不可變性,狀態(tài)變化時(shí)通過(guò)“替換整個(gè)視圖實(shí)例”觸發(fā)更新。 依賴(lài)函數(shù)的冪等性,狀態(tài)變化時(shí)通過(guò)“重新調(diào)用函數(shù)”生成新 UI,函數(shù)本身無(wú)狀態(tài)。
引用類(lèi)型角色 class (ObservableObject) 是狀態(tài)共享的主要載體,與視圖是“觀察與被觀察”關(guān)系。 引用類(lèi)型 (ViewModel) 通常作為參數(shù)傳入 @Composable 函數(shù),函數(shù)僅通過(guò)參數(shù)使用其數(shù)據(jù)。

理解這一點(diǎn)后,你就能明白為什么 Compose 如此強(qiáng)調(diào)“狀態(tài)提升”——因?yàn)楹瘮?shù)天然就不適合自己管理狀態(tài),將狀態(tài)“提升”出去,讓函數(shù)只負(fù)責(zé)渲染,是最符合其設(shè)計(jì)哲學(xué)的做法。

狀態(tài)提升:Compose 的 @Binding 模式

在 SwiftUI 中,當(dāng)我們需要將一個(gè)父視圖的狀態(tài)傳遞給子視圖,并允許子視圖修改它時(shí),我們使用 @Binding。

SwiftUI:

struct ParentView: View {
    @State private var name: String = "World"

    var body: some View {
        VStack {
            Text("Hello, \(name)")
            // 通過(guò) $ 符號(hào)傳遞綁定
            EditableChildView(name: $name)
        }
    }
}

struct EditableChildView: View {
    @Binding var name: String

    var body: some View {
        TextField("Enter your name", text: $name)
    }
}

Compose 沒(méi)有一個(gè)直接叫做 Binding 的東西,但它通過(guò)一種更明確的模式實(shí)現(xiàn)了同樣的目標(biāo),這個(gè)模式被稱(chēng)為 “狀態(tài)提升”(State Hoisting)。這完美契合了它以純函數(shù)為中心的設(shè)計(jì)。

狀態(tài)提升的核心思想是:將狀態(tài)移動(dòng)到需要它的所有子組件的“最小共同父級(jí)”,然后通過(guò)參數(shù)將狀態(tài)值修改狀態(tài)的 Lambda 函數(shù)分別傳遞給子組件。這使得子組件本身變成無(wú)狀態(tài)(Stateless)的,更易于測(cè)試和復(fù)用。

Jetpack Compose:

@Composable
fun ParentScreen() {
    var name by remember { mutableStateOf("World") }

    Column {
        Text("Hello, $name")
        // 狀態(tài)提升:將狀態(tài)值和修改它的方法傳下去
        EditableChildComposable(
            name = name,
            onNameChange = { newName -> name = newName }
        )
    }
}

@Composable
fun EditableChildComposable(name: String, onNameChange: (String) -> Unit) {
    // 這個(gè) Composable 自己不持有狀態(tài),它是“被控制”的
    TextField(
        value = name,
        onValueChange = onNameChange, // 當(dāng)文本變化時(shí),調(diào)用傳進(jìn)來(lái)的 lambda
        label = { Text("Enter your name") }
    )
}

iOS 開(kāi)發(fā)者速記:

  • SwiftUI 的 @BindingCompose 的 (value: T, onValueChange: (T) -> Unit) 參數(shù)對(duì)。
  • 狀態(tài)提升是 Compose 的核心設(shè)計(jì)模式。盡可能地讓你的 Composable 變得無(wú)狀態(tài),只接收數(shù)據(jù)并發(fā)出事件。

復(fù)雜狀態(tài)與業(yè)務(wù)邏輯:ViewModel 的角色

當(dāng)狀態(tài)變得復(fù)雜,或者需要跨越多個(gè)屏幕共享,甚至需要在應(yīng)用配置變更(如屏幕旋轉(zhuǎn))后存活時(shí),SwiftUI 開(kāi)發(fā)者會(huì)轉(zhuǎn)向 @StateObject@ObservedObject 來(lái)引入一個(gè) ObservableObject。

SwiftUI:

class ProfileViewModel: ObservableObject {
    @Published var username: String = "SwiftDev"
    @Published var followerCount: Int = 100

    func follow() {
        followerCount += 1
    }
}

struct ProfileView: View {
    @StateObject private var viewModel = ProfileViewModel()

    var body: some View {
        VStack {
            Text("User: \(viewModel.username)")
            Text("Followers: \(viewModel.followerCount)")
            Button("Follow") {
                viewModel.follow()
            }
        }
    }
}

在 Android 生態(tài)中,有一個(gè)專(zhuān)門(mén)為此設(shè)計(jì)的標(biāo)準(zhǔn)架構(gòu)組件:ViewModel。Compose 與 ViewModel 的集成非常成熟。

Jetpack Compose:

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

// Android 的 ViewModel
class ProfileViewModel : ViewModel() {
    private val _followerCount = MutableStateFlow(100) // 使用 StateFlow
    val followerCount: StateFlow<Int> = _followerCount

    val username: String = "ComposeDev"

    fun follow() {
        _followerCount.value += 1
    }
}

@Composable
fun ProfileScreen(
    // 使用官方庫(kù)提供的 viewModel() 函數(shù)獲取實(shí)例
    // 它會(huì)自動(dòng)處理生命周期和配置變更
    viewModel: ProfileViewModel = viewModel()
) {
    // 使用 collectAsStateWithLifecycle 將 Flow 轉(zhuǎn)換為 Compose 的 State
    val followerCount by viewModel.followerCount.collectAsStateWithLifecycle()

    Column {
        Text("User: ${viewModel.username}")
        Text("Followers: $followerCount")
        Button(onClick = { viewModel.follow() }) {
            Text("Follow")
        }
    }
}

注意: collectAsStateWithLifecycle 是一個(gè)推薦使用的擴(kuò)展函數(shù),需要添加 androidx.lifecycle:lifecycle-runtime-compose 依賴(lài)。

關(guān)鍵點(diǎn)分析:

  1. ViewModel: 這是 Android 架構(gòu)組件的一部分,它的生命周期被設(shè)計(jì)為比 Composable 更長(zhǎng)。例如,當(dāng)屏幕旋轉(zhuǎn)導(dǎo)致 Activity 重建時(shí),ViewModel 實(shí)例可以存活下來(lái),從而保留其內(nèi)部數(shù)據(jù)。
  2. StateFlow: 在現(xiàn)代 Android 開(kāi)發(fā)中,通常使用 Kotlin Coroutines 的 Flow(特別是 StateFlowSharedFlow)來(lái)暴露 ViewModel 中的狀態(tài)。StateFlow 是一個(gè)可觀察的數(shù)據(jù)持有者,非常適合與 Compose 結(jié)合。
  3. viewModel(): 這是一個(gè)特殊的 Composable 函數(shù),它負(fù)責(zé)提供 ViewModel 實(shí)例。你不需要手動(dòng)創(chuàng)建它,這個(gè)函數(shù)會(huì)智能地返回現(xiàn)有的 ViewModel 或創(chuàng)建一個(gè)新的,并將其作用域與正確的生命周期(如 Activity、Fragment 或?qū)Ш綀D)綁定。
  4. collectAsStateWithLifecycle(): 這個(gè)擴(kuò)展函數(shù)安全地從 Flow 中收集值,并將其轉(zhuǎn)換為 Compose 的 State。它還具有生命周期感知能力,當(dāng)你的 App 進(jìn)入后臺(tái)時(shí),它會(huì)自動(dòng)停止收集,以節(jié)省資源。

iOS 開(kāi)發(fā)者速記:

  • SwiftUI 的 @StateObject / ObservableObjectCompose 的 ViewModel + StateFlow
  • 在 Android 中,將業(yè)務(wù)邏輯和屏幕級(jí)狀態(tài)放在 ViewModel 中是標(biāo)準(zhǔn)的、強(qiáng)烈推薦的最佳實(shí)踐。

環(huán)境值:@EnvironmentObject vs CompositionLocal

有時(shí),我們需要將一個(gè)值深層地傳遞給組件樹(shù)中的很多子組件,而不想通過(guò)每一層都手動(dòng)傳遞參數(shù)(這被稱(chēng)為“屬性鉆探”/ Prop Drilling)。SwiftUI 為此提供了 @EnvironmentObject。

Compose 的對(duì)應(yīng)物是 CompositionLocal。它允許你創(chuàng)建一個(gè)可以在 Composable 樹(shù)的某個(gè)子樹(shù)中“隱式”提供的值。

Jetpack Compose:

// 1. 創(chuàng)建一個(gè) CompositionLocal key
private val LocalUser = compositionLocalOf<User> { error("No user found!") }

data class User(val name: String)

@Composable
fun MyApp() {
    val currentUser = User("Admin")
    // 2. 在樹(shù)的頂層提供值
    CompositionLocalProvider(LocalUser provides currentUser) {
        // 這個(gè)子樹(shù)中的任何 Composable 都可訪問(wèn)到 LocalUser
        HomeScreen()
    }
}

@Composable
fun HomeScreen() {
    // ...
    UserProfile()
}

@Composable
fun UserProfile() {
    // 3. 在任何深度的子組件中消費(fèi)值
    val user = LocalUser.current
    Text("Welcome, ${user.name}")
}

CompositionLocal 非常適合傳遞那些不經(jīng)常變化且普遍需要的數(shù)據(jù),例如主題(MaterialTheme 就是通過(guò)它實(shí)現(xiàn)的)、本地化設(shè)置或用戶(hù)信息。

iOS 開(kāi)發(fā)者速記:

  • SwiftUI 的 @EnvironmentObject / .environmentObject()Compose 的 CompositionLocal / CompositionLocalProvider。

不同場(chǎng)景下的狀態(tài)設(shè)計(jì)注意事項(xiàng)

  1. 簡(jiǎn)單、臨時(shí)的 UI 狀態(tài):

    • 場(chǎng)景: 一個(gè) Switch 的開(kāi)關(guān)狀態(tài),一個(gè) TextField 的輸入內(nèi)容,一個(gè)動(dòng)畫(huà)的當(dāng)前值。
    • 策略: 使用 remember { mutableStateOf(...) }。讓這個(gè)狀態(tài)盡可能地靠近使用它的地方,如果只有一個(gè) Composable 需要它,就讓它成為該 Composable 的私有狀態(tài)。
  2. 需要在多個(gè)子組件間共享的狀態(tài):

    • 場(chǎng)景: 一個(gè)表單中有多個(gè)輸入框,一個(gè)“提交”按鈕的啟用狀態(tài)取決于所有輸入框的有效性。
    • 策略: 狀態(tài)提升。將所有相關(guān)的狀態(tài)提升到它們的共同父 Composable 中的這個(gè)父 Composable 持有狀態(tài),并將狀態(tài)值和事件回調(diào)傳遞給子組件。
  3. 屏幕級(jí)別的、需要長(zhǎng)生命周期的狀態(tài):

    • 場(chǎng)景: 從網(wǎng)絡(luò)或數(shù)據(jù)庫(kù)加載的數(shù)據(jù),復(fù)雜的業(yè)務(wù)邏輯和用戶(hù)輸入驗(yàn)證。
    • 策略: 使用 ViewModel。將所有業(yè)務(wù)邏輯、數(shù)據(jù)獲取和復(fù)雜狀態(tài)保存在 ViewModel 中。通過(guò) StateFlow 將數(shù)據(jù)暴露給 Composable。這是最常見(jiàn)和最健壯的模式。
  4. 應(yīng)用級(jí)別的、跨屏幕共享的狀態(tài):

    • 場(chǎng)景: 用戶(hù)登錄狀態(tài),應(yīng)用主題(暗/亮模式),購(gòu)物車(chē)內(nèi)容。
    • 策略:
      • 共享的 ViewModel: 你可以將一個(gè) ViewModel 的作用域綁定到導(dǎo)航圖(NavGraph)甚至 Activity,讓多個(gè)屏幕共享同一個(gè) ViewModel 實(shí)例。
      • 依賴(lài)注入 (DI): 使用 Hilt 或 Koin 等依賴(lài)注入框架提供單例(Singleton)的 RepositoryService。ViewModel 再?gòu)倪@些單例中獲取數(shù)據(jù)。這是大型應(yīng)用的首選方案。
      • CompositionLocal: 適合用于傳遞不經(jīng)常變化的、與 UI 相關(guān)的數(shù)據(jù),如主題配置。

總結(jié):給 iOS 開(kāi)發(fā)者的備忘錄

SwiftUI 概念 Compose 等價(jià)物/模式 關(guān)鍵區(qū)別與注意事項(xiàng)
@State remember { mutableStateOf(...) } remember 是必須的,用于在重組間保持狀態(tài)。
@Binding 狀態(tài)提升 (value, onValueChange) Compose 鼓勵(lì)創(chuàng)建無(wú)狀態(tài)子組件,模式更明確。
@StateObject viewModel() + ViewModel ViewModel 是 Android 官方架構(gòu)組件,生命周期更長(zhǎng)。
@ObservedObject 傳遞 ViewModel 實(shí)例 概念類(lèi)似,但通常通過(guò) viewModel() 獲取頂層實(shí)例。
ObservableObject ViewModel + StateFlow StateFlow 是 Kotlin Coroutines 的一部分,是現(xiàn)代 Android 的首選。
@Published MutableStateFlow 功能相似,都是可觀察的數(shù)據(jù)持有者。
@EnvironmentObject CompositionLocalProvider 用于隱式地向下傳遞數(shù)據(jù)。

從 SwiftUI 切換到 Jetpack Compose,最大的思維轉(zhuǎn)變?cè)谟诶斫獠肀?strong>以函數(shù)為中心的設(shè)計(jì)哲學(xué),這自然而然地引出了對(duì)狀態(tài)提升模式的嚴(yán)格遵循,以及將 ViewModel 作為屏幕級(jí)狀態(tài)管理的標(biāo)準(zhǔn)實(shí)踐。一旦你掌握了這些核心理念,你會(huì)發(fā)現(xiàn)自己可以很快地運(yùn)用 SwiftUI 積累的聲明式編程經(jīng)驗(yàn),在 Android 平臺(tái)上構(gòu)建出同樣優(yōu)雅和健壯的應(yīng)用。

祝你在 Compose 的世界里編碼愉快!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容