Jetpack Compose 入门:声明式 UI 的安卓革命
告别 XML Layout,拥抱 Kotlin 声明式 UI —— Compose 的核心概念、状态管理、布局系统和导航一网打尽。
UIKit 是 iOS 开发的基石,但它的设计哲学诞生于 2008 年——比第一代 iPhone 只晚一年。经过十多年的积累,它有三大顽疾:
SwiftUI(2019 年发布)用声明式解决了这些问题:
// SwiftUI:描述你想要什么
struct ProfileView: View {
@State var name = "Naruto"
var body: some View {
VStack {
Text("こんにちは、\(name)")
TextField("输入名字", text: $name)
.textFieldStyle(.roundedBorder)
}
.padding()
}
}
不用写 frame、不用 Auto Layout、不用 viewDidLoad——你描述 UI 的结构,框架负责渲染。
在 SwiftUI 中,每个 UI 都是一个遵守 View 协议的 struct:
protocol View {
associatedtype Body: View
@ViewBuilder var body: Self.Body { get }
}
一个 View 只做一件事:定义它的 body 属性。body 不是返回 UIView,而是返回另一个 View——形成了一个 View 树。
struct NinjaCard: View {
let name: String
let rank: String
var body: some View {
HStack {
Image(systemName: "person.circle.fill")
.font(.largeTitle)
.foregroundColor(.orange)
VStack(alignment: .leading) {
Text(name)
.font(.headline)
Text(rank)
.font(.subheadline)
.foregroundColor(.secondary)
}
}
.padding()
.background(Color(.systemGray6))
.cornerRadius(12)
}
}
💡 View 是 struct 而不是 class——这意味着它是值类型,在内存中被迅速创建和销毁。SwiftUI 的 diff 机制只更新变化的部分,所以不用担心性能。
这是 SwiftUI 最核心的概念。你怎么声明一个值,决定了它如何驱动 UI 更新。
struct CounterView: View {
@State var count = 0 // 这个 View 拥有这个状态
var body: some View {
VStack {
Text("计数: \(count)")
Button("+1") { count += 1 }
}
}
}
@State 用于当前 View 私有的简单状态。当值改变时,body 自动重新计算。建议用 private 修饰。
struct ToggleSwitch: View {
@Binding var isOn: Bool // 不拥有,只是借用
var body: some View {
Toggle("开关", isOn: $isOn) // $ 取 binding 引用
}
}
// 使用时:
struct ParentView: View {
@State var isEnabled = false
var body: some View {
ToggleSwitch(isOn: $isEnabled)
}
}
@Binding 是两个 View 之间双向传递状态的管道。parent 用 $ 取 child 的 @Binding。
// ViewModel 必须遵守 ObservableObject
class TaskViewModel: ObservableObject {
@Published var tasks: [Task] = [] // @Published 触发 UI 更新
@Published var isLoading = false
func fetchTasks() async {
isLoading = true
// 模拟网络请求
try? await Task.sleep(for: .seconds(1))
tasks = [
Task(title: "修炼螺旋丸", isDone: false),
Task(title: "执行S级任务", isDone: true)
]
isLoading = false
}
}
struct TaskListView: View {
@StateObject var viewModel = TaskViewModel() // View 拥有 ViewModel
var body: some View {
List(viewModel.tasks) { task in
TaskRow(task: task)
}
.refreshable { await viewModel.fetchTasks() }
.task { await viewModel.fetchTasks() }
}
}
区分 @StateObject 与 @ObservedObject:
| @StateObject | @ObservedObject | |
|---|---|---|
| 谁创建 | 当前 View 创建并拥有 | 从外部传入 |
| 生命周期 | 跟随 View,配置变更不丢失 | 不由当前 View 管理 |
| 典型场景 | ViewModel 初始化 | 子 View 接收父 View 的 ViewModel |
// VStack = 垂直
VStack(alignment: .leading, spacing: 12) {
Text("标题")
Text("副标题")
}
// HStack = 水平
HStack(alignment: .center, spacing: 8) {
Image(systemName: "star.fill")
Text("4.9 分")
}
// ZStack = 层叠(类似 z-index)
ZStack(alignment: .bottomTrailing) {
Image("background") // 底层
Text("火の意志") // 顶层
}
配合 Spacer() 可以实现各种弹性布局:
HStack {
Text("左边")
Spacer() // 把 "右边" 推到最右
Text("右边")
}
struct ContentView: View {
var body: some View {
NavigationStack {
List(1...10, id: \.self) { i in
NavigationLink {
DetailView(number: i) // 目标页面
} label: {
Text("任务 #\(i)")
}
}
.navigationTitle("任务列表")
}
}
}
NavigationStack 替代了老旧的 NavigationView,支持更灵活的路径管理。
SwiftUI 天然适合 MVVM:
┌─────────────┐ ┌─────────────────────┐ ┌──────────┐
│ View │ ──→ │ ViewModel │ ──→ │ Model │
│ (struct) │ ←── │ (class, │ ←── │ (struct) │
│ @State, │ │ @Published) │ │ │
│ @Bindable │ │ │ │ │
└─────────────┘ └─────────────────────┘ └──────────┘
完整示例:
// Model
struct Mission: Identifiable {
let id = UUID()
let title: String
let rank: String
var isCompleted: Bool
}
// ViewModel
class MissionViewModel: ObservableObject {
@Published var missions: [Mission] = []
@Published var filterRank: String = "全部"
var filteredMissions: [Mission] {
guard filterRank != "全部" else { return missions }
return missions.filter { $0.rank == filterRank }
}
func toggleComplete(_ mission: Mission) {
if let idx = missions.firstIndex(where: { $0.id == mission.id }) {
missions[idx].isCompleted.toggle()
}
}
}
// View
struct MissionListView: View {
@StateObject var viewModel = MissionViewModel()
var body: some View {
NavigationStack {
List(viewModel.filteredMissions) { mission in
HStack {
Text(mission.title)
Spacer()
if mission.isCompleted {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(.green)
}
}
.onTapGesture { viewModel.toggleComplete(mission) }
}
.navigationTitle("忍务")
}
}
}
| SwiftUI | Compose | Vue 3 | |
|---|---|---|---|
| 创建 UI | struct MyView: View | @Composable fun | <template> + <script setup> |
| 私有状态 | @State | remember { mutableStateOf() } | ref() |
| 共享状态 | @Binding | 传 lambda + state | props + emit |
| 外部对象 | @StateObject / @ObservedObject | collectAsState() (Flow) | reactive() |
| 列表 | List / ForEach | LazyColumn | v-for |
| 导航 | NavigationStack | NavHost | vue-router |
🎯 三者的核心思想完全相同——声明式 UI = f(state)。学好一个框架,理解另外两个只需要掌握语法糖的对应关系。
SwiftUI 不是 UIKit 的包装,它是从头重新设计的 UI 框架。拥抱它需要的心态调整:
起初可能会因为不熟悉而觉得「写个简单的页面怎么这么弯弯绕」,但一旦你的代码量上来——复杂列表、多状态切换、跨页面通信——你会发现 SwiftUI 让你少写了太多 boilerplate。
告别 XML Layout,拥抱 Kotlin 声明式 UI —— Compose 的核心概念、状态管理、布局系统和导航一网打尽。
记录我使用 Nuxt 3 + Tailwind CSS + Nuxt Content 搭建个人博客的完整过程,涵盖项目初始化、文件路由、内容管理和暗色模式配置。
深入理解 React Hooks 的心智模型——闭包陷阱、依赖数组、自定义 Hook,以及和 Vue Composables 的异同对比。