iOS vs Android — 核心差异对比
作为拥有 10 年 iOS 开发经验的工程师,你已经掌握了移动开发的核心思维。Android 开发与 iOS 开发在理念上高度相似,但在工具链、API 设计和生态系统上有显著差异。本章系统梳理两者的对应关系,帮助你快速建立心智模型。
iOS 和 Android 开发的核心思想是相通的:组件化、生命周期管理、响应式 UI、异步编程。掌握了这些原理,Android 学习将是一次"换语法"的旅程,而非从零开始。
1.1 开发语言对比:Swift vs Kotlin
Swift 和 Kotlin 都是现代的、类型安全的编程语言,设计哲学高度相似。两者都强调空安全、函数式编程特性、类型推断和简洁语法。
| 特性 | Swift | Kotlin |
|---|---|---|
| 空安全 | Optional (T?) | Nullable (T?) |
| 变量/常量 | var / let | var / val |
| 函数定义 | func name() {} | fun name() {} |
| 字符串插值 | \(variable) | $variable / ${expr} |
| 类扩展 | extension | 扩展函数 fun Type.name() |
| 枚举 | enum + associated values | sealed class / enum class |
| 结构体 | struct (值类型) | data class (引用类型) |
| 协议 | protocol | interface |
| 闭包 | { } / trailing closure | lambda { } / trailing lambda |
| 异步 | async/await (Swift 5.5+) | suspend + 协程 |
| 泛型 | func name<T>() | fun <T> name() |
| 单例 | static let shared = MyClass() | companion object / object |
1.2 IDE 对比:Xcode vs Android Studio
| 功能 | Xcode | Android Studio |
|---|---|---|
| 基础 | Apple 专有 IDE | 基于 IntelliJ IDEA(JetBrains) |
| 模拟器 | iOS Simulator | Android Virtual Device (AVD) |
| 界面设计 | Interface Builder / SwiftUI Preview | Layout Editor / Compose Preview |
| 性能分析 | Instruments | Android Profiler |
| 调试 | LLDB | ADB + JDWP debugger |
| 代码补全 | 基础 AI 补全 | AI Assistant (Gemini) |
| 快捷键 | Cmd+R 运行 / Cmd+B 构建 | Shift+F10 运行 / Ctrl+F9 构建 |
| 重构 | 有限重构 | 强大的 IntelliJ 重构 |
| 平台 | 仅 macOS | macOS / Windows / Linux |
1.3 UI 框架对比
| 框架类型 | iOS | Android |
|---|---|---|
| 命令式 UI | UIKit | View System (XML + View) |
| 声明式 UI | SwiftUI | Jetpack Compose |
| 布局文件 | XIB / Storyboard | XML Layout |
| 自动布局 | Auto Layout / NSLayoutConstraint | ConstraintLayout |
| 列表 | UITableView / UICollectionView | RecyclerView |
| 导航 | UINavigationController | Navigation Component |
| 标签栏 | UITabBarController | BottomNavigationView |
| 图片显示 | UIImageView | ImageView |
| 文本 | UILabel / UITextField | TextView / EditText |
1.4 应用生命周期对比
| 概念 | iOS | Android |
|---|---|---|
| 应用入口 | AppDelegate / @main | Application 类 / AndroidManifest.xml |
| 场景管理 | SceneDelegate (iOS 13+) | Activity(每个界面独立) |
| 视图控制器 | UIViewController | Activity / Fragment |
| 视图加载 | viewDidLoad() | onCreate() |
| 即将显示 | viewWillAppear() | onStart() / onResume() |
| 已经显示 | viewDidAppear() | onResume() |
| 即将消失 | viewWillDisappear() | onPause() |
| 已经消失 | viewDidDisappear() | onStop() |
| 销毁 | deinit | onDestroy() |
1.5 完整概念映射表
| iOS 概念 | Android 对应 | 说明 |
|---|---|---|
| AppDelegate | Application | 全局应用入口,管理生命周期 |
| UIViewController | Activity | 界面容器,管理视图层级 |
| UIViewController (子视图) | Fragment | 可复用的 UI 片段,寄生在 Activity 中 |
| XIB / Storyboard | XML Layout | 声明式 UI 布局文件 |
| Auto Layout | ConstraintLayout | 基于约束的响应式布局系统 |
| UITableView | RecyclerView (LinearLayoutManager) | 垂直滚动列表 |
| UICollectionView | RecyclerView (GridLayoutManager) | 网格/灵活布局列表 |
| UITableViewDelegate/DataSource | RecyclerView.Adapter | 列表数据源与事件 |
| Delegate Pattern | Interface / Listener | 回调通信模式 |
| NotificationCenter | EventBus / LiveData / Flow | 跨组件事件传递 |
| Combine / RxSwift | Flow / LiveData / RxJava | 响应式编程框架 |
| Core Data | Room Database | 本地关系型数据库 ORM |
| URLSession / Alamofire | OkHttp / Retrofit | 网络请求库 |
| GCD / OperationQueue | Kotlin Coroutines | 异步并发处理 |
| UserDefaults | SharedPreferences / DataStore | 轻量级键值对存储 |
| Info.plist | AndroidManifest.xml | 应用配置清单文件 |
| NSBundle | Intent / Bundle | 数据传递容器 |
| Segue / present() | Intent / Navigation Component | 页面导航跳转 |
| CocoaPods / SPM | Gradle / Maven | 依赖管理系统 |
| IBOutlet / IBAction | View Binding / View.setOnClickListener | 视图绑定与事件 |
| @State / @Binding (SwiftUI) | remember / mutableStateOf (Compose) | 声明式 UI 状态管理 |
| @ObservedObject (SwiftUI) | ViewModel + collectAsState | ViewModel 状态观察 |
| SDWebImage / Kingfisher | Glide / Coil / Picasso | 异步图片加载库 |
| Swinject / 手动 DI | Hilt / Dagger / Koin | 依赖注入框架 |
| Swift Concurrency | Kotlin Coroutines + Flow | 结构化并发框架 |
| TestFlight | Firebase App Distribution / Internal Test | 内测分发平台 |
| App Store | Google Play | 正式应用商店 |
1.6 平台分发对比
| 分发环节 | iOS (App Store) | Android (Google Play) |
|---|---|---|
| 签名工具 | Certificates + Provisioning Profile | Keystore (.jks / .keystore) |
| 构建产物 | .ipa 文件 | .apk / .aab (Android App Bundle) |
| 审核时间 | 1-3 天(人工审核) | 几小时-1 天(自动为主) |
| 内测分发 | TestFlight(最多 10000 人) | Internal Testing / Closed Testing |
| 多渠道 | 不支持 | Build Variants / Flavors |
| 侧载安装 | 企业证书或越狱 | 直接安装 APK(需开启"未知来源") |
| 代码混淆 | 编译时优化 | ProGuard / R8 |
| 年费 | $99/年(个人/公司) | $25 一次性注册费 |
1. 先把 Kotlin 语法过一遍(约 2 天),与 Swift 对比学习效率最高
2. 掌握 Activity + Fragment 生命周期(类比 UIViewController)
3. 学习 ConstraintLayout(与 Auto Layout 思想完全相同)
4. 掌握 RecyclerView(Android 中最常用的控件之一)
5. 理解 MVVM + ViewModel + LiveData/Flow 架构
6. 最后学 Jetpack Compose(有 SwiftUI 基础会很快)
开发环境搭建
Android Studio 是官方 IDE,基于 JetBrains IntelliJ IDEA 构建。如果你习惯了 Xcode,Android Studio 的功能更为强大,重构工具尤其出色。
2.1 Android Studio 安装
访问 developer.android.com/studio 下载最新版 Android Studio。推荐版本:Android Studio Hedgehog (2023.1.1) 及以上。
# 检查 Java 版本(Android Studio 自带 JDK,通常不需要额外安装)
java -version
# 推荐:JDK 17 或以上
# 检查 Android SDK 工具
adb version
# 如果找不到 adb,将以下路径加入 PATH:
# macOS: ~/Library/Android/sdk/platform-tools
# Windows: %LOCALAPPDATA%\Android\Sdk\platform-tools
# macOS .zshrc 配置
export ANDROID_HOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/platform-tools
export PATH=$PATH:$ANDROID_HOME/tools
export PATH=$PATH:$ANDROID_HOME/tools/bin
2.2 AVD 虚拟设备创建
AVD(Android Virtual Device)等同于 iOS Simulator,但更灵活,可以模拟不同 API 级别和硬件配置。
设备:Pixel 7 或 Pixel 8
API Level:API 34 (Android 14) 或 API 35 (Android 15)
ABI:arm64-v8a(真机对应)或 x86_64(模拟器性能更好)
RAM:至少 2GB,推荐 4GB
# 列出所有 AVD
avdmanager list avd
# 列出可用的系统镜像
sdkmanager --list | grep system-images
# 创建新 AVD(命令行方式)
avdmanager create avd \
-n "Pixel_7_API34" \
-k "system-images;android-34;google_apis;x86_64" \
-d "pixel_7"
# 启动 AVD
emulator -avd Pixel_7_API34
# 连接真机后查看设备
adb devices
# 输出示例:
# List of devices attached
# emulator-5554 device ← 模拟器
# R5CR10ABCDE device ← 真机
# 安装 APK 到设备
adb install -r app-debug.apk
# 查看日志(对应 iOS Console)
adb logcat | grep MyApp
2.3 项目结构详解
Android 项目结构与 Xcode 项目有较大差异,但逻辑相似。理解目录结构是高效开发的前提。
MyApp/ # 项目根目录
├── app/ # 主模块(对应 Xcode 的主 Target)
│ ├── src/
│ │ ├── main/
│ │ │ ├── java/ # Kotlin/Java 源代码
│ │ │ │ └── com/example/myapp/
│ │ │ │ ├── MainActivity.kt
│ │ │ │ ├── data/ # 数据层(Room、Retrofit、Repository)
│ │ │ │ ├── domain/ # 业务逻辑层(UseCase)
│ │ │ │ ├── ui/ # UI 层(Fragment、ViewModel、Adapter)
│ │ │ │ └── di/ # 依赖注入模块(Hilt)
│ │ │ ├── res/ # 资源文件(对应 Assets.xcassets)
│ │ │ │ ├── layout/ # XML 布局文件(对应 XIB/Storyboard)
│ │ │ │ │ └── activity_main.xml
│ │ │ │ ├── values/ # 值资源
│ │ │ │ │ ├── strings.xml # 字符串(对应 Localizable.strings)
│ │ │ │ │ ├── colors.xml # 颜色定义
│ │ │ │ │ ├── dimens.xml # 尺寸定义
│ │ │ │ │ └── themes.xml # 主题(对应 Info.plist UIAppearance)
│ │ │ │ ├── drawable/ # 图片/矢量图(对应 Assets.xcassets)
│ │ │ │ ├── mipmap-*/ # 应用图标(各分辨率)
│ │ │ │ └── navigation/ # 导航图(对应 Storyboard segue)
│ │ │ └── AndroidManifest.xml # 应用清单(对应 Info.plist)
│ │ ├── test/ # 单元测试
│ │ └── androidTest/ # 集成测试(对应 XCUITest)
│ └── build.gradle # 模块级构建脚本
├── build.gradle # 项目级构建脚本(对应 Podfile)
├── settings.gradle # 模块注册
├── gradle.properties # Gradle 全局属性
└── local.properties # 本地配置(SDK 路径,不提交 git)
2.4 AndroidManifest.xml 详解
AndroidManifest.xml 是 Android 应用的"身份证",类似于 iOS 的 Info.plist,但功能更加丰富,需要声明所有 Activity、权限、服务等组件。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- 权限声明(对应 iOS Info.plist 的 NSCameraUsageDescription 等) -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<!-- 硬件特性要求(Google Play 筛选) -->
<uses-feature android:name="android.hardware.camera" android:required="false" />
<application
android:name=".MyApplication" <!-- 自定义 Application 类 -->
android:allowBackup="true" <!-- 允许备份 -->
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" <!-- 应用图标 -->
android:label="@string/app_name" <!-- 应用名称 -->
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" <!-- 支持 RTL 布局 -->
android:theme="@style/Theme.MyApp" <!-- 全局主题 -->
tools:targetApi="31">
<!-- 主 Activity(对应 iOS 的 Main.storyboard Initial ViewController) -->
<activity
android:name=".MainActivity"
android:exported="true" <!-- 可被外部启动(主 Activity 必须为 true) -->
android:windowSoftInputMode="adjustResize" <!-- 软键盘弹出时调整布局 -->
android:screenOrientation="portrait"> <!-- 竖屏锁定 -->
<intent-filter>
<!-- 标记为启动 Activity -->
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Deep Link 配置(对应 iOS Universal Links) -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="www.example.com"
android:pathPrefix="/app" />
</intent-filter>
</activity>
<!-- 其他 Activity -->
<activity android:name=".ui.detail.DetailActivity"
android:exported="false"
android:parentActivityName=".MainActivity" /> <!-- 返回导航父 Activity -->
<!-- 后台服务 -->
<service android:name=".service.DownloadService"
android:exported="false" />
<!-- 广播接收器 -->
<receiver android:name=".receiver.AlarmReceiver"
android:exported="false" />
<!-- Provider(内容提供者,用于跨应用共享数据) -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
2.5 Gradle 构建系统详解
Gradle 是 Android 的构建系统,类似于 CocoaPods + Makefile 的结合体。它管理依赖、构建变体、签名等所有构建相关事务。
| 功能 | iOS | Android |
|---|---|---|
| 依赖管理 | CocoaPods / SPM / Carthage | Gradle (Maven Central / JitPack) |
| 配置文件 | Podfile / Package.swift | build.gradle / build.gradle.kts |
| 锁定文件 | Podfile.lock | gradle.lockfile(可选) |
| 构建变体 | Schemes + Configurations | Build Types + Product Flavors |
| 构建脚本语言 | Ruby (Podfile) | Groovy / Kotlin DSL |
// 插件声明(对应 CocoaPods 的 use_frameworks! 等指令)
plugins {
alias(libs.plugins.android.application) // Android 应用插件
alias(libs.plugins.kotlin.android) // Kotlin 支持
alias(libs.plugins.kotlin.kapt) // Kotlin 注解处理器(用于 Room、Hilt)
alias(libs.plugins.hilt.android) // Hilt 依赖注入
alias(libs.plugins.navigation.safeargs) // Navigation Safe Args
}
android {
namespace = "com.example.myapp" // 包名(对应 iOS Bundle ID)
compileSdk = 35 // 编译 SDK 版本
defaultConfig {
applicationId = "com.example.myapp" // 应用唯一标识(对应 iOS Bundle ID)
minSdk = 26 // 最低支持系统版本(对应 Deployment Target)
targetSdk = 35 // 目标 SDK 版本
versionCode = 1 // 版本号(整数,对应 iOS Build Number)
versionName = "1.0.0" // 版本名称(对应 iOS Version)
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
// 构建类型(对应 iOS Build Configurations: Debug / Release)
buildTypes {
release {
isMinifyEnabled = true // 开启代码压缩(ProGuard/R8)
isShrinkResources = true // 移除未使用资源
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
signingConfig = signingConfigs.getByName("release")
}
debug {
applicationIdSuffix = ".debug" // Debug 包使用不同包名,可与 Release 共存
isDebuggable = true
}
}
// 产品风味(对应 iOS Targets 或 Schemes,用于多渠道/白标应用)
flavorDimensions += "environment"
productFlavors {
create("dev") {
dimension = "environment"
applicationIdSuffix = ".dev"
versionNameSuffix = "-dev"
buildConfigField("String", "BASE_URL", "\"https://dev-api.example.com/\"")
}
create("prod") {
dimension = "environment"
buildConfigField("String", "BASE_URL", "\"https://api.example.com/\"")
}
}
// Kotlin 编译选项
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
// ViewBinding(推荐启用,替代 findViewById)
buildFeatures {
viewBinding = true
buildConfig = true
compose = true // 启用 Jetpack Compose
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.8"
}
}
// 依赖声明(对应 Podfile 中的 pod 'xxx' 或 SPM 中的 .package)
dependencies {
// AndroidX 核心库
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.appcompat)
implementation(libs.material) // Material Design 组件
// 生命周期组件
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.lifecycle.livedata.ktx)
// Navigation Component
implementation(libs.androidx.navigation.fragment.ktx)
implementation(libs.androidx.navigation.ui.ktx)
// Room 数据库(对应 Core Data)
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
kapt(libs.androidx.room.compiler) // 注解处理器,生成 Room 代码
// Hilt 依赖注入
implementation(libs.hilt.android)
kapt(libs.hilt.compiler)
// 网络请求(对应 Alamofire + Codable)
implementation(libs.retrofit)
implementation(libs.retrofit.converter.gson)
implementation(libs.okhttp)
implementation(libs.okhttp.logging.interceptor)
// 协程
implementation(libs.kotlinx.coroutines.android)
// 图片加载(对应 Kingfisher / SDWebImage)
implementation(libs.glide)
kapt(libs.glide.compiler)
// Jetpack Compose
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.compose.ui)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.ui.tooling.preview)
// 测试
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.test.ext.junit)
androidTestImplementation(libs.androidx.espresso.core)
}
// 项目级 build.gradle.kts
// 声明所有子模块共用的插件版本(不应用插件,只声明)
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.kotlin.android) apply false
alias(libs.plugins.hilt.android) apply false
}
// settings.gradle.kts - 仓库配置
// (将以下内容放在 settings.gradle.kts 中)
/*
dependencyResolutionManagement {
repositories {
google() // Google Maven 仓库(AndroidX、Material 等)
mavenCentral() // Maven 中央仓库(Retrofit、OkHttp 等)
maven { url = uri("https://jitpack.io") } // JitPack(GitHub 上的库)
}
}
*/
2.6 Version Catalog 版本目录
Android 推荐使用 libs.versions.toml 统一管理所有依赖版本,类似于 iOS SPM 的 Package.resolved。
[versions]
# 各库版本号统一管理
androidGradlePlugin = "8.3.0"
kotlin = "1.9.22"
hilt = "2.51"
room = "2.6.1"
retrofit = "2.9.0"
okhttp = "4.12.0"
navigation = "2.7.7"
lifecycle = "2.7.0"
compose-bom = "2024.02.02"
glide = "4.16.0"
[libraries]
# 依赖库定义
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "core" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" }
hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" }
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
androidx-room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" }
retrofit = { group = "com.squareup.retrofit2", name = "retrofit", version.ref = "retrofit" }
retrofit-converter-gson = { group = "com.squareup.retrofit2", name = "converter-gson", version.ref = "retrofit" }
[plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
navigation-safeargs = { id = "androidx.navigation.safeargs.kotlin", version.ref = "navigation" }
Android API Level 与 iOS Deployment Target 类似,但 Android 碎片化更严重。
API 26 (Android 8.0) 覆盖约 98% 设备(推荐 minSdk)
API 29 (Android 10) 覆盖约 95% 设备
API 33 (Android 13) 覆盖约 60% 设备(2024年数据)
可在 developer.android.com/about/dashboards 查看最新分布数据。
Kotlin 语言精要(为 Swift 开发者)
Kotlin 与 Swift 在设计哲学上高度相似,都是现代的、类型安全的、多范式编程语言。对于 Swift 开发者来说,学习 Kotlin 的核心工作量在于适应 JVM 的思维模型和 Kotlin 特有的语法糖。
3.1 变量与类型
// 常量
let name: String = "Alice"
let age = 25 // 类型推断
// 变量
var score: Int = 0
var message = "Hello"
// 多行字符串
let text = """
Hello,
World!
"""
// 字符串插值
let greeting = "Hello, \(name)! Age: \(age)"
// 基本类型
let isActive: Bool = true
let pi: Double = 3.14159
let ratio: Float = 0.5
let bigNum: Int64 = 9_000_000_000
// 常量(不可变,对应 Swift let)
val name: String = "Alice"
val age = 25 // 类型推断
// 变量(可变,对应 Swift var)
var score: Int = 0
var message = "Hello"
// 多行字符串(三引号)
val text = """
Hello,
World!
""".trimIndent() // 移除每行公共缩进
// 字符串模板(对应 Swift 字符串插值)
val greeting = "Hello, $name! Age: $age"
val expr = "Result: ${1 + 2 * 3}" // 复杂表达式用 ${}
// 基本类型(Kotlin 类型首字母大写)
val isActive: Boolean = true
val pi: Double = 3.14159
val ratio: Float = 0.5f // Float 字面量需要 f 后缀
val bigNum: Long = 9_000_000_000L // Long 字面量用 L 后缀
3.2 空安全 — Kotlin vs Swift Optional
这是 Kotlin 与 Swift 最重要的共同特性之一。Kotlin 的空安全更加强制,不允许任何可空类型不经检查就使用。
// Optional 声明
var name: String? = nil
var count: Int? = 42
// 安全解包
if let n = name {
print(n)
}
// guard let
func process(name: String?) {
guard let n = name else { return }
print(n)
}
// 可选链
let length = name?.count
// nil 合并
let display = name ?? "Unknown"
// 强制解包(危险!)
let forcedName = name! // 如果为 nil 会崩溃
// if let 多条件
if let a = optA, let b = optB, a > 0 {
// ...
}
// 可空类型声明(在类型后加 ?)
var name: String? = null // null 对应 Swift nil
var count: Int? = 42
// 安全调用(对应 Swift 可选链 ?.)
val length = name?.length // 如果 name 为 null,返回 null
// Elvis 运算符(对应 Swift nil 合并 ??)
val display = name ?: "Unknown"
// 非空断言(对应 Swift 强制解包 !)
val forcedName = name!! // 如果为 null 会抛 NullPointerException
// let 作用域函数(配合空安全,对应 if let)
name?.let { n ->
println(n) // n 是非空的 String
}
// 函数中的空安全检查(替代 guard let)
fun process(name: String?) {
name ?: return // 如果为 null,直接返回
println(name) // 这里 name 自动智能转换为非空 String
}
// 智能转换(Smart Cast)
if (name != null) {
// 在这个块内,name 自动被视为 String(非空)
println(name.length) // 无需额外解包
}
// 多空安全检查
val result = optA?.let { a ->
optB?.let { b -> a + b }
}
3.3 函数
// 基本函数
func greet(name: String) -> String {
return "Hello, \(name)"
}
// 参数标签
func move(from start: Point, to end: Point) {}
// 默认参数
func log(_ msg: String, level: String = "INFO") {}
// 可变参数
func sum(_ numbers: Int...) -> Int {
return numbers.reduce(0, +)
}
// 多返回值
func minMax(_ arr: [Int]) -> (min: Int, max: Int) {
return (arr.min()!, arr.max()!)
}
// 扩展
extension String {
func trimmed() -> String {
self.trimmingCharacters(in: .whitespaces)
}
}
// 高阶函数
let doubled = [1,2,3].map { $0 * 2 }
let evens = [1,2,3,4].filter { $0 % 2 == 0 }
let total = [1,2,3].reduce(0, +)
// 基本函数(fun 关键字)
fun greet(name: String): String {
return "Hello, $name"
}
// 单表达式函数(等号语法,自动推断返回类型)
fun greet2(name: String) = "Hello, $name"
// Kotlin 使用命名参数(没有参数标签区分)
fun move(startX: Int, startY: Int, endX: Int, endY: Int) {}
move(startX = 0, startY = 0, endX = 10, endY = 10)
// 默认参数(与 Swift 相同)
fun log(message: String, level: String = "INFO") {}
// 可变参数(vararg)
fun sum(vararg numbers: Int): Int = numbers.sum()
// 解构声明(对应 Swift 多返回值元组)
fun minMax(arr: IntArray): Pair<Int, Int> =
Pair(arr.min(), arr.max())
val (min, max) = minMax(intArrayOf(1, 2, 3, 4, 5))
// 扩展函数(Extension Functions)
fun String.trimmed(): String = this.trim()
" hello ".trimmed() // "hello"
// 扩展属性
val String.wordCount: Int get() = this.split(" ").size
// 高阶函数(it 是单参数 lambda 的默认名)
val doubled = listOf(1, 2, 3).map { it * 2 }
val evens = listOf(1, 2, 3, 4).filter { it % 2 == 0 }
val total = listOf(1, 2, 3).reduce { acc, i -> acc + i }
// 作用域函数(Kotlin 独有,极其常用)
// apply: 配置对象后返回对象本身
val intent = Intent().apply {
putExtra("id", userId)
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
// with: 对对象执行多个操作
with(textView) {
text = "Hello"
textSize = 16f
}
// let: 非空时执行,返回 lambda 结果
val len = name?.let { it.length } ?: 0
// also: 执行副作用后返回原对象
val user = createUser().also { println("Created: ${it.name}") }
3.4 类、data class 与 sealed class
// 类
class Person {
let name: String
var age: Int
init(name: String, age: Int) {
self.name = name; self.age = age
}
func introduce() -> String {
"I'm \(name), \(age) years old"
}
}
// 结构体(值类型,自动 memberwise init)
struct Point {
var x: Double
var y: Double
}
// 继承
class Employee: Person {
let jobTitle: String
init(name: String, age: Int, title: String) {
self.jobTitle = title
super.init(name: name, age: age)
}
override func introduce() -> String {
"\(super.introduce()), \(jobTitle)"
}
}
// enum with associated values
enum Shape {
case circle(radius: Double)
case rectangle(width: Double, height: Double)
}
// Protocol
protocol Drawable {
func draw()
}
// 单例
class NetworkManager {
static let shared = NetworkManager()
private init() {}
}
// 类(主构造函数在类头部)
class Person(
val name: String, // val = 不可变
var age: Int // var = 可变
) {
fun introduce(): String = "I'm $name, $age years old"
}
// data class(对应 Swift struct,自动生成 equals/hashCode/copy/toString)
data class Point(val x: Double, val y: Double)
val p1 = Point(1.0, 2.0)
val p2 = p1.copy(y = 3.0) // 对应 Swift withCopy
// 继承(Kotlin 类默认 final,需要 open 才可继承)
open class Employee(
name: String,
age: Int,
val jobTitle: String
) : Person(name, age) {
override fun introduce() = "${super.introduce()}, $jobTitle"
}
// sealed class(比 Swift enum 更强大,子类可以有不同属性)
sealed class Shape {
data class Circle(val radius: Double) : Shape()
data class Rectangle(val width: Double, val height: Double) : Shape()
object Unknown : Shape()
}
fun area(s: Shape): Double = when (s) {
is Shape.Circle -> Math.PI * s.radius * s.radius
is Shape.Rectangle -> s.width * s.height
is Shape.Unknown -> 0.0
// 无需 else!编译器检查所有子类
}
// interface(对应 Swift protocol,支持默认实现)
interface Drawable {
fun draw()
fun debugDraw() { println("debug draw") } // 默认实现
}
// object 单例(对应 Swift static let shared)
object NetworkManager {
fun get(url: String) { /* ... */ }
}
NetworkManager.get("https://api.example.com")
// companion object(类级别的静态成员)
class MyClass {
companion object {
const val TAG = "MyClass"
fun create() = MyClass()
}
}
val tag = MyClass.TAG
3.5 协程基础预览
Kotlin 协程是 Android 异步编程的标准方案,对应 Swift 的 async/await + GCD。详细内容在第 11 章。
// async 函数
func fetchUser(id: String) async throws -> User {
let url = URL(string: "https://api.example.com/users/\(id)")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode(User.self, from: data)
}
// 调用
Task {
do {
let user = try await fetchUser(id: "123")
await MainActor.run { updateUI(user) }
} catch {
print(error)
}
}
// suspend 函数(对应 Swift async func)
suspend fun fetchUser(id: String): User {
return withContext(Dispatchers.IO) { // 切换到 IO 线程
apiService.getUser(id) // 网络请求
}
}
// 在 ViewModel 中调用
class UserViewModel : ViewModel() {
fun loadUser(id: String) {
viewModelScope.launch { // 对应 Swift Task {}
try {
val user = fetchUser(id) // 挂起等待
// 自动回到主线程更新 UI
_uiState.value = UiState.Success(user)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message)
}
}
}
}
1. 没有结构体值类型:Kotlin 只有类(引用类型),data class 提供不可变性特性
2. 没有 guard 语句:用 ?: return 替代
3. object 关键字:Kotlin 原生单例,优雅简洁
4. 作用域函数:let/run/with/apply/also 是 Kotlin 独有的
5. 扩展函数:可在不修改源码的情况下为任何类添加方法
6. when 表达式:比 Swift switch 更强大,可以作为表达式使用
Android 应用架构
Google 官方推荐使用 MVVM + Clean Architecture 架构,配合 Jetpack 组件库。这与 iOS 开发社区推崇的 MVVM + Clean Architecture 思路完全一致,区别在于具体工具和 API。
4.1 推荐架构概览
Android 官方 Architecture Guidelines 定义三层架构:
UI Layer(展示层)→ Domain Layer(业务层,可选)→ Data Layer(数据层)
依赖方向:UI → Domain → Data(单向依赖)
┌─────────────────────────────────────────┐
│ UI Layer(展示层) │
│ Fragment/Activity + ViewModel │
│ UI State (UiState sealed class) │
│ View Binding / Jetpack Compose │
├─────────────────────────────────────────┤
│ Domain Layer(业务逻辑层) │
│ UseCase(每个业务操作一个 UseCase) │
│ Domain Model(纯 Kotlin 数据类) │
│ Repository Interface(接口定义) │
├─────────────────────────────────────────┤
│ Data Layer(数据层) │
│ Repository Implementation │
│ Local DataSource (Room Database) │
│ Remote DataSource (Retrofit API) │
│ DataStore (Preferences) │
└─────────────────────────────────────────┘
4.2 Application 类
Application 类是 Android 应用的全局入口,对应 iOS 的 AppDelegate。它在应用启动时创建,生命周期与应用相同。
// 对应 iOS AppDelegate
// 注意:必须在 AndroidManifest.xml 的 application 标签中声明
// android:name=".MyApplication"
@HiltAndroidApp // 使用 Hilt 时必须加此注解,初始化 DI 框架
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// 应用启动时的初始化代码
// 对应 iOS AppDelegate.application(_:didFinishLaunchingWithOptions:)
// 初始化日志库
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}
// 初始化崩溃收集
// FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(!BuildConfig.DEBUG)
// 注意:不要在这里做耗时操作,会影响启动速度
}
override fun onLowMemory() {
super.onLowMemory()
// 系统内存不足时的回调
// 对应 iOS AppDelegate.applicationDidReceiveMemoryWarning
// 清理缓存等操作
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
// 更细粒度的内存压力回调
when (level) {
TRIM_MEMORY_UI_HIDDEN -> {
// 应用切到后台,释放 UI 相关缓存
}
TRIM_MEMORY_RUNNING_LOW -> {
// 系统内存不足
}
}
}
}
4.3 完整 MVVM 架构示例
以一个"用户列表"功能为例,展示完整的 MVVM + Clean Architecture 实现。
4.3.1 Data Layer — 数据层
// 网络响应模型(对应 iOS Codable struct)
data class UserDto(
@SerializedName("id") val id: Int, // @SerializedName 对应 iOS CodingKeys
@SerializedName("name") val name: String,
@SerializedName("email") val email: String,
@SerializedName("avatar_url") val avatarUrl: String?
)
// Domain 模型(纯业务模型,不含网络/数据库细节)
data class User(
val id: Int,
val name: String,
val email: String,
val avatarUrl: String?
)
// DTO → Domain 映射扩展函数
fun UserDto.toDomain() = User(
id = id,
name = name,
email = email,
avatarUrl = avatarUrl
)
// Room Entity(数据库表模型)— 详见第12章
@Entity(tableName = "users")
data class UserEntity(
@PrimaryKey val id: Int,
val name: String,
val email: String,
val avatarUrl: String?
)
fun UserEntity.toDomain() = User(id, name, email, avatarUrl)
fun User.toEntity() = UserEntity(id, name, email, avatarUrl)
// Repository 接口(Domain Layer 定义,对应 iOS 的 Protocol)
interface UserRepository {
fun getUsers(): Flow<List<User>> // 响应式数据流
suspend fun getUserById(id: Int): User? // 单次查询
suspend fun refreshUsers() // 刷新远程数据
}
// Repository 实现(Data Layer 实现)
class UserRepositoryImpl @Inject constructor(
private val remoteDataSource: UserRemoteDataSource, // 网络数据源
private val localDataSource: UserLocalDataSource, // 本地数据源
private val ioDispatcher: CoroutineDispatcher // IO 线程调度器
) : UserRepository {
// 先返回本地缓存,然后在后台刷新
override fun getUsers(): Flow<List<User>> {
return localDataSource.getAllUsers() // Room Flow,自动感知数据库变化
.map { entities -> entities.map { it.toDomain() } }
}
override suspend fun getUserById(id: Int): User? {
return withContext(ioDispatcher) {
localDataSource.getUserById(id)?.toDomain()
}
}
override suspend fun refreshUsers() {
withContext(ioDispatcher) {
try {
// 从网络获取数据
val remoteUsers = remoteDataSource.fetchUsers()
// 保存到本地数据库(作为缓存)
localDataSource.insertUsers(remoteUsers.map { it.toEntity() })
} catch (e: Exception) {
// 网络失败时继续使用本地缓存(离线优先策略)
throw e
}
}
}
}
4.3.2 Domain Layer — 业务逻辑层
// UseCase(对应 iOS 中的 Service 或 Interactor)
// 每个 UseCase 只做一件事
class GetUsersUseCase @Inject constructor(
private val userRepository: UserRepository
) {
// operator fun invoke 让 UseCase 可以像函数一样调用
operator fun invoke(): Flow<List<User>> {
return userRepository.getUsers()
.map { users ->
// 业务规则:过滤无效用户,按名字排序
users.filter { it.name.isNotBlank() }
.sortedBy { it.name }
}
}
}
class RefreshUsersUseCase @Inject constructor(
private val userRepository: UserRepository
) {
suspend operator fun invoke() = userRepository.refreshUsers()
}
4.3.3 UI Layer — 展示层
// UI 状态(sealed class,对应 iOS Result 或自定义状态枚举)
sealed class UserListUiState {
object Loading : UserListUiState()
data class Success(val users: List<User>) : UserListUiState()
data class Error(val message: String) : UserListUiState()
object Empty : UserListUiState()
}
// ViewModel(对应 iOS ViewModel in MVVM)
@HiltViewModel // Hilt 管理 ViewModel 的依赖注入
class UserListViewModel @Inject constructor(
private val getUsersUseCase: GetUsersUseCase,
private val refreshUsersUseCase: RefreshUsersUseCase
) : ViewModel() {
// StateFlow:持续发射当前状态的流(对应 iOS @Published)
private val _uiState = MutableStateFlow<UserListUiState>(UserListUiState.Loading)
val uiState: StateFlow<UserListUiState> = _uiState.asStateFlow()
// 搜索关键字
private val _searchQuery = MutableStateFlow("")
init {
// 初始化时加载数据(对应 iOS viewDidLoad 中的数据加载)
loadUsers()
}
private fun loadUsers() {
viewModelScope.launch { // viewModelScope 绑定 ViewModel 生命周期
getUsersUseCase()
.combine(_searchQuery) { users, query ->
// 根据搜索关键字过滤
if (query.isBlank()) users
else users.filter { it.name.contains(query, ignoreCase = true) }
}
.catch { e ->
_uiState.value = UserListUiState.Error(e.message ?: "Unknown error")
}
.collect { users ->
_uiState.value = if (users.isEmpty()) {
UserListUiState.Empty
} else {
UserListUiState.Success(users)
}
}
}
}
fun refresh() {
viewModelScope.launch {
_uiState.value = UserListUiState.Loading
try {
refreshUsersUseCase()
} catch (e: Exception) {
_uiState.value = UserListUiState.Error(e.message ?: "Refresh failed")
}
}
}
fun search(query: String) {
_searchQuery.value = query // 触发 combine 重新计算
}
}
@AndroidEntryPoint // Hilt 注入标记(Fragment 必须加)
class UserListFragment : Fragment(R.layout.fragment_user_list) {
// viewModels() 是 Jetpack 提供的委托属性(对应 iOS 的 @StateObject)
private val viewModel: UserListViewModel by viewModels()
// View Binding(对应 iOS IBOutlet,自动生成,无需手动 findViewById)
private var _binding: FragmentUserListBinding? = null
private val binding get() = _binding!! // 非空保证
private lateinit var adapter: UserListAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentUserListBinding.bind(view)
setupRecyclerView()
setupSearch()
observeViewModel() // 对应 iOS combine 的 sink/assign
}
private fun setupRecyclerView() {
adapter = UserListAdapter { user ->
// 点击事件:跳转到详情(对应 iOS performSegue)
val action = UserListFragmentDirections
.actionUserListToUserDetail(userId = user.id)
findNavController().navigate(action)
}
binding.recyclerView.apply {
this.adapter = this@UserListFragment.adapter
layoutManager = LinearLayoutManager(context)
addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
}
// 下拉刷新(对应 iOS UIRefreshControl)
binding.swipeRefresh.setOnRefreshListener {
viewModel.refresh()
}
}
private fun setupSearch() {
binding.searchInput.addTextChangedListener { text ->
viewModel.search(text?.toString() ?: "")
}
}
// 观察 ViewModel 状态(对应 iOS 的 Combine sink)
private fun observeViewModel() {
// lifecycleScope.launch + repeatOnLifecycle 是观察 Flow 的标准模式
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
binding.swipeRefresh.isRefreshing = false
when (state) {
is UserListUiState.Loading -> {
binding.progressBar.isVisible = true
binding.recyclerView.isVisible = false
binding.emptyView.isVisible = false
}
is UserListUiState.Success -> {
binding.progressBar.isVisible = false
binding.recyclerView.isVisible = true
binding.emptyView.isVisible = false
adapter.submitList(state.users) // DiffUtil 自动计算差异
}
is UserListUiState.Error -> {
binding.progressBar.isVisible = false
showErrorSnackbar(state.message)
}
is UserListUiState.Empty -> {
binding.progressBar.isVisible = false
binding.recyclerView.isVisible = false
binding.emptyView.isVisible = true
}
}
}
}
}
}
private fun showErrorSnackbar(message: String) {
Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG)
.setAction("Retry") { viewModel.refresh() }
.show()
}
// 释放 binding 引用,防止内存泄漏(Fragment 的 View 可能比 Fragment 先销毁)
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
4.4 Jetpack 组件概览
| Jetpack 组件 | 功能 | iOS 对应 |
|---|---|---|
| ViewModel | 存储和管理 UI 相关数据,跨配置变更存活 | ViewModel (MVVM) |
| LiveData | 可感知生命周期的可观察数据持有者 | @Published + Combine |
| Room | SQLite 的 ORM 抽象层 | Core Data |
| Navigation | Fragment 间导航的框架 | UINavigationController + Storyboard Segue |
| WorkManager | 可靠的后台任务调度 | BGTaskScheduler |
| DataStore | 异步、事务性的键值对存储 | UserDefaults (异步版) |
| Paging 3 | 分页加载库 | 手动实现 / DiffableDataSource |
| CameraX | 相机 API 抽象层 | AVFoundation / AVCaptureSession |
| Compose | 声明式 UI 框架 | SwiftUI |
| Hilt | 基于 Dagger 的依赖注入框架 | Swinject / 手动 DI |
Activity 与生命周期
Activity 是 Android 中最重要的组件,相当于 iOS 的 UIViewController。理解 Activity 生命周期是 Android 开发的基础。
5.1 Activity 生命周期完整图解
• 屏幕旋转/配置变更 → Activity 销毁重建(onCreate 重新调用),ViewModel 存活!
• 按 Home 键 → onPause → onStop(Activity 仍在内存中)
• 按返回键 → onPause → onStop → onDestroy
• 系统内存不足 → onStop 后可能被杀死(onSaveInstanceState 保存状态)
5.2 生命周期方法对比
| Android Activity | iOS UIViewController | 说明 |
|---|---|---|
| onCreate() | viewDidLoad() | 初始化,View 创建完毕 |
| onStart() | viewWillAppear() | 即将对用户可见 |
| onResume() | viewDidAppear() | 完全可见,获得焦点 |
| onPause() | viewWillDisappear() | 失去焦点,保存数据 |
| onStop() | viewDidDisappear() | 完全不可见 |
| onDestroy() | deinit | 销毁,释放资源 |
| onRestart() | 无直接对应 | 从 Stop 状态恢复时调用 |
| onSaveInstanceState() | 无直接对应(State Restoration) | 在系统杀死前保存状态 |
| onRestoreInstanceState() | 无直接对应 | 从保存状态恢复 |
5.3 Activity 完整代码示例
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
// View Binding(对应 iOS IBOutlet,自动生成绑定代码)
private lateinit var binding: ActivityMainBinding
// ViewModel(通过 Hilt 注入)
private val viewModel: MainViewModel by viewModels()
// 用于接收 Activity 结果的 Launcher(新版 API,替代 startActivityForResult)
private val pickImageLauncher = registerForActivityResult(
ActivityResultContracts.GetContent()
) { uri ->
// 处理图片选择结果(对应 iOS UIImagePickerController delegate)
uri?.let { viewModel.onImageSelected(it) }
}
// 权限请求 Launcher
private val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
pickImageLauncher.launch("image/*")
} else {
showPermissionDeniedDialog()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 设置 View Binding(对应 iOS 的 loadView/viewDidLoad)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 处理 savedInstanceState(对应 iOS State Restoration)
if (savedInstanceState != null) {
val savedText = savedInstanceState.getString(KEY_TEXT, "")
binding.textInput.setText(savedText)
}
// 设置 ActionBar(对应 iOS UINavigationBar)
setSupportActionBar(binding.toolbar)
supportActionBar?.title = "My App"
// 初始化 NavController(Navigation Component)
val navController = findNavController(R.id.nav_host_fragment)
setupActionBarWithNavController(navController)
// 观察 ViewModel 数据
observeViewModel()
// 设置点击事件(对应 iOS IBAction)
binding.fab.setOnClickListener {
requestCameraPermission()
}
}
override fun onStart() {
super.onStart()
// Activity 对用户可见,可以开始更新 UI
// 对应 iOS viewWillAppear
}
override fun onResume() {
super.onResume()
// Activity 获得焦点,可以开始动画、注册广播接收器
// 对应 iOS viewDidAppear
}
override fun onPause() {
super.onPause()
// Activity 失去焦点,保存草稿数据、暂停视频播放
// 对应 iOS viewWillDisappear
// 注意:这里的代码要快速执行!
}
override fun onStop() {
super.onStop()
// Activity 完全不可见,可以执行较耗时的操作
// 对应 iOS viewDidDisappear
}
override fun onDestroy() {
super.onDestroy()
// Activity 销毁,清理资源
// 注意:ViewModel 中的 viewModelScope 协程会自动取消
}
// 保存状态(屏幕旋转/内存不足时调用)
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
// 保存 EditText 内容等临时状态
outState.putString(KEY_TEXT, binding.textInput.text.toString())
}
private fun observeViewModel() {
viewModel.uiState.observe(this) { state ->
// observe() 感知 Activity 生命周期,自动在 DESTROYED 时取消订阅
when (state) {
is MainUiState.Loading -> binding.progressBar.isVisible = true
is MainUiState.Success -> {
binding.progressBar.isVisible = false
// 更新 UI
}
is MainUiState.Error -> {
binding.progressBar.isVisible = false
Toast.makeText(this, state.message, Toast.LENGTH_SHORT).show()
}
}
}
}
private fun requestCameraPermission() {
when {
ContextCompat.checkSelfPermission(
this, Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED -> {
// 已有权限
pickImageLauncher.launch("image/*")
}
shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
// 显示权限说明对话框
showPermissionRationaleDialog()
}
else -> {
// 请求权限
requestPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
}
// 处理系统返回按键
override fun onSupportNavigateUp(): Boolean {
val navController = findNavController(R.id.nav_host_fragment)
return navController.navigateUp() || super.onSupportNavigateUp()
}
companion object {
private const val KEY_TEXT = "key_text"
}
}
5.4 Intent 与页面跳转
Intent 是 Android 的页面跳转和数据传递机制,类似于 iOS 的 Segue + 数据传递,但更加灵活,还能跨应用通信。
// 跳转到下一个 VC 并传值
let detailVC = DetailViewController()
detailVC.userId = "123"
detailVC.userName = "Alice"
navigationController?.pushViewController(detailVC, animated: true)
// 模态展示
let sheet = BottomSheetVC()
present(sheet, animated: true)
// 通过 Segue 传值
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
if segue.identifier == "showDetail" {
if let dest = segue.destination as? DetailVC {
dest.userId = selectedUserId
}
}
}
// 从子 VC 获取结果(completion 回调)
let picker = ImagePickerVC()
picker.onImagePicked = { image in
self.imageView.image = image
}
present(picker, animated: true)
// Intent 跳转(对应 iOS pushViewController)
val intent = Intent(this, DetailActivity::class.java).apply {
putExtra("user_id", "123") // 传递 String
putExtra("user_name", "Alice") // 传递 String
putExtra("user_age", 25) // 传递 Int
// 传递 Parcelable 对象(对应 iOS Codable)
putExtra("user", user) // User 需实现 Parcelable
}
startActivity(intent)
// 在 DetailActivity 中接收数据
class DetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val userId = intent.getStringExtra("user_id")
val userName = intent.getStringExtra("user_name") ?: "Unknown"
val userAge = intent.getIntExtra("user_age", 0)
val user = intent.getParcelableExtra<User>("user") // 旧 API
// API 33+ 推荐:
val user2 = intent.getParcelableExtra("user", User::class.java)
}
}
// 获取 Activity 结果(新版 API,对应 iOS completion 回调)
private val launcher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == RESULT_OK) {
val data = result.data
val selectedId = data?.getStringExtra("selected_id")
// 处理返回结果
}
}
// 启动并等待结果
val intent = Intent(this, SelectActivity::class.java)
launcher.launch(intent)
// 在 SelectActivity 中返回结果
// 对应 iOS 的 dismiss(animated:) + completion
fun returnResult(selectedId: String) {
val resultIntent = Intent().apply {
putExtra("selected_id", selectedId)
}
setResult(RESULT_OK, resultIntent)
finish() // 关闭当前 Activity
}
// 清除返回栈(对应 iOS pop to root)
val intent = Intent(this, HomeActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
}
startActivity(intent)
5.5 Parcelable — 跨 Activity 传递对象
Parcelable 是 Android 高性能序列化接口,用于在 Activity/Fragment 间传递复杂对象。对应 iOS 的 Codable(但用于本地传递而非网络)。
// 方法1:使用 @Parcelize 注解(推荐,kotlin-parcelize 插件自动生成)
// 在 build.gradle 中添加:plugins { id("kotlin-parcelize") }
@Parcelize
data class User(
val id: String,
val name: String,
val email: String,
val age: Int
) : Parcelable // 自动生成 Parcelable 实现,无需手动写
// 使用
val intent = Intent(this, DetailActivity::class.java)
intent.putExtra("user", user) // User 实现了 Parcelable
startActivity(intent)
// 接收
val user = intent.getParcelableExtra("user", User::class.java) // API 33+
// 方法2:手动实现 Parcelable(了解即可,现在用 @Parcelize 更多)
data class Point(val x: Int, val y: Int) : Parcelable {
constructor(parcel: Parcel) : this(
parcel.readInt(),
parcel.readInt()
)
override fun writeToParcel(parcel: Parcel, flags: Int) {
parcel.writeInt(x)
parcel.writeInt(y)
}
override fun describeContents() = 0
companion object CREATOR : Parcelable.Creator<Point> {
override fun createFromParcel(parcel: Parcel) = Point(parcel)
override fun newArray(size: Int) = arrayOfNulls<Point>(size)
}
}
iOS push → Android startActivity(intent) 或 Navigation Component navigate()
iOS present → Android startActivity(intent)(Android 没有真正的模态,通过 theme 实现类似效果)
iOS dismiss → Android finish()
iOS popToRoot → Android FLAG_ACTIVITY_CLEAR_TOP 或 Navigation Component popBackStack(rootId)
推荐:使用 Navigation Component(第9章)替代直接用 Intent,更优雅地管理导航
Fragment 详解
Fragment 是 Android 中可复用的 UI 片段,类似于 iOS 中的子 UIViewController。它必须寄宿在 Activity 中,有自己的生命周期和布局。现代 Android 开发推荐以 Fragment 为主要的 UI 单元,Activity 只作为容器。
Google 官方推荐:一个 Activity + 多个 Fragment 的架构(Single Activity Architecture)。Activity 作为 NavHostFragment 的容器,所有界面都用 Fragment 实现,通过 Navigation Component 管理导航。这与 iOS 的 UINavigationController + UIViewController 体系高度类似。
6.1 Fragment 生命周期
6.2 Fragment 基础实现
@AndroidEntryPoint
class HomeFragment : Fragment() { // 可以传入布局 ID:Fragment(R.layout.fragment_home)
private var _binding: FragmentHomeBinding? = null
private val binding get() = _binding!! // 安全访问 binding
// Fragment 级别的 ViewModel(范围在 Fragment 内)
private val viewModel: HomeViewModel by viewModels()
// 共享 ViewModel(范围在宿主 Activity,用于 Fragment 间通信)
private val sharedViewModel: SharedViewModel by activityViewModels()
// 创建 View
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentHomeBinding.inflate(inflater, container, false)
return binding.root
}
// View 创建完毕,在这里操作 View(对应 iOS viewDidLoad)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupUI()
observeViewModel()
}
private fun setupUI() {
binding.button.setOnClickListener {
// 导航到其他 Fragment(使用 Navigation Component)
val action = HomeFragmentDirections.actionHomeToDetail(itemId = 123)
findNavController().navigate(action)
}
// 设置 Toolbar
(requireActivity() as AppCompatActivity).supportActionBar?.title = "Home"
}
private fun observeViewModel() {
// 使用 viewLifecycleOwner(不是 this!)来观察 LiveData
// 原因:Fragment 的生命周期比其 View 长,用 this 可能导致内存泄漏
viewModel.data.observe(viewLifecycleOwner) { data ->
binding.textView.text = data
}
// 观察 Flow(推荐方式)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
updateUI(state)
}
}
}
}
// ⚠️ 关键:必须在 onDestroyView 中释放 binding,否则内存泄漏
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
// 布局文件
// res/layout/fragment_home.xml 中定义 View
6.3 Fragment 事务
Fragment 事务(Transaction)是添加、移除、替换 Fragment 的操作,类似于 iOS 的 addChild/removeFromParent。
// 在 Activity 中管理 Fragment
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
// 初次添加 Fragment(savedInstanceState 为 null 时,避免重复添加)
if (savedInstanceState == null) {
supportFragmentManager.commit {
// add:添加 Fragment(不替换已有的)
add(R.id.fragment_container, HomeFragment(), "home")
// replace:替换容器中的 Fragment
// replace(R.id.fragment_container, HomeFragment())
}
}
}
fun showDetailFragment(itemId: Int) {
supportFragmentManager.commit {
// 替换 Fragment
replace(R.id.fragment_container, DetailFragment.newInstance(itemId))
// 加入返回栈(按返回键时弹出,类似 iOS popViewController)
addToBackStack("detail")
// 设置过渡动画
setCustomAnimations(
R.anim.slide_in_right, // 进入动画
R.anim.slide_out_left, // 退出动画
R.anim.slide_in_left, // 弹出时的进入动画(press back)
R.anim.slide_out_right // 弹出时的退出动画
)
}
}
// 检查并弹出返回栈
fun popFragment() {
if (supportFragmentManager.backStackEntryCount > 0) {
supportFragmentManager.popBackStack() // 对应 iOS popViewController
} else {
finish()
}
}
}
// Fragment 通过 companion object 传递参数(最佳实践)
class DetailFragment : Fragment() {
companion object {
private const val ARG_ITEM_ID = "item_id"
// 工厂方法(对应 iOS init(nibName:bundle:))
fun newInstance(itemId: Int): DetailFragment {
return DetailFragment().apply {
arguments = Bundle().apply {
putInt(ARG_ITEM_ID, itemId)
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 从 arguments 取出参数(对应 iOS 的属性赋值)
val itemId = arguments?.getInt(ARG_ITEM_ID) ?: 0
}
}
6.4 Fragment 间通信
Fragment 间通信应避免直接引用,推荐使用共享 ViewModel 或 Fragment Result API。
// 方式1:共享 ViewModel(推荐,对应 iOS 的共享数据源)
// 两个 Fragment 共享同一个 ViewModel 实例(宿主 Activity 级别)
class SharedViewModel : ViewModel() {
private val _selectedItem = MutableLiveData<Item>()
val selectedItem: LiveData<Item> = _selectedItem
fun selectItem(item: Item) {
_selectedItem.value = item
}
}
// Fragment A:发送数据
class ListFragment : Fragment() {
// activityViewModels() 获取 Activity 级别的 ViewModel
private val sharedViewModel: SharedViewModel by activityViewModels()
fun onItemClicked(item: Item) {
sharedViewModel.selectItem(item) // 通知其他 Fragment
}
}
// Fragment B:接收数据
class DetailFragment : Fragment() {
private val sharedViewModel: SharedViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
sharedViewModel.selectedItem.observe(viewLifecycleOwner) { item ->
// 更新 UI 显示选中的 item
binding.titleText.text = item.name
}
}
}
// 方式2:Fragment Result API(用于一次性结果传递,类似 iOS completion)
// 发送结果的 Fragment(对应 iOS dismiss + completion)
class SenderFragment : Fragment() {
fun sendResult() {
val result = Bundle().apply {
putString("key_result", "Hello from Sender!")
putInt("key_count", 42)
}
// 设置结果,"requestKey" 是双方约定的标识符
setFragmentResult("myRequest", result)
findNavController().popBackStack() // 返回
}
}
// 接收结果的 Fragment
class ReceiverFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 注册结果监听器(在 onCreate 中,不在 onViewCreated)
setFragmentResultListener("myRequest") { requestKey, bundle ->
val result = bundle.getString("key_result")
val count = bundle.getInt("key_count")
println("Received: $result, count: $count")
}
}
}
// 方式3:通过 Interface 回调(对应 iOS Delegate Pattern)
// 定义接口
interface OnItemSelectedListener {
fun onItemSelected(item: Item)
}
class ListFragment : Fragment() {
// 在 onAttach 中获取宿主(Activity 或父 Fragment)
private var listener: OnItemSelectedListener? = null
override fun onAttach(context: Context) {
super.onAttach(context)
// 宿主必须实现接口(否则抛异常)
listener = context as? OnItemSelectedListener
?: throw ClassCastException("$context must implement OnItemSelectedListener")
}
fun onItemClicked(item: Item) {
listener?.onItemSelected(item) // 通知宿主
}
override fun onDetach() {
super.onDetach()
listener = null // 释放引用,防止内存泄漏
}
}
6.5 DialogFragment
DialogFragment 是 Fragment 的子类,用于显示对话框,对应 iOS 的 UIAlertController 或自定义模态视图。
// 自定义 DialogFragment(对应 iOS 自定义 UIAlertController 或底部弹窗)
class ConfirmDialogFragment : DialogFragment() {
// 使用 Fragment Result API 传递结果
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val title = arguments?.getString(ARG_TITLE) ?: "Confirm"
val message = arguments?.getString(ARG_MESSAGE) ?: ""
return AlertDialog.Builder(requireContext())
.setTitle(title)
.setMessage(message)
.setPositiveButton("OK") { _, _ ->
// 确认按钮(对应 iOS UIAlertAction(.default))
setFragmentResult(REQUEST_KEY, Bundle().apply {
putBoolean("confirmed", true)
})
}
.setNegativeButton("Cancel") { _, _ ->
setFragmentResult(REQUEST_KEY, Bundle().apply {
putBoolean("confirmed", false)
})
}
.create()
}
companion object {
const val REQUEST_KEY = "confirm_dialog"
private const val ARG_TITLE = "title"
private const val ARG_MESSAGE = "message"
fun newInstance(title: String, message: String) =
ConfirmDialogFragment().apply {
arguments = Bundle().apply {
putString(ARG_TITLE, title)
putString(ARG_MESSAGE, message)
}
}
}
}
// 底部弹窗(BottomSheetDialogFragment,对应 iOS UISheetPresentationController)
class OptionsBottomSheet : BottomSheetDialogFragment() {
private var _binding: BottomSheetOptionsBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = BottomSheetOptionsBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 设置 BottomSheet 行为(高度、拖动等)
val bottomSheetBehavior = BottomSheetBehavior.from(binding.root.parent as View)
bottomSheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
binding.optionShare.setOnClickListener {
// 处理分享
dismiss()
}
binding.optionDelete.setOnClickListener {
// 处理删除
dismiss()
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
// 使用方式
class MyFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 监听对话框结果
setFragmentResultListener(ConfirmDialogFragment.REQUEST_KEY) { _, bundle ->
val confirmed = bundle.getBoolean("confirmed")
if (confirmed) {
viewModel.deleteItem()
}
}
binding.deleteButton.setOnClickListener {
// 显示确认对话框(对应 iOS present(alertController, animated: true))
ConfirmDialogFragment.newInstance(
title = "Delete Item",
message = "Are you sure you want to delete this item?"
).show(childFragmentManager, "confirm_dialog")
}
binding.moreButton.setOnClickListener {
// 显示底部弹窗
OptionsBottomSheet().show(childFragmentManager, "options")
}
}
}
UI 布局系统
Android 的 UI 布局系统基于 View 树,通过 XML 声明式定义布局,对应 iOS 的 AutoLayout + XIB/Storyboard。ConstraintLayout 是最接近 iOS Auto Layout 的布局方式。
7.1 View 树结构
Android UI = View 树(每个 View 有父 ViewGroup 和子 View)
iOS UI = View 层级(每个 UIView 有 superview 和 subviews)
两者在概念上完全相同,区别在于布局 API 不同。
7.2 ConstraintLayout(对比 Auto Layout)
ConstraintLayout 是 Android 最推荐的布局,与 iOS Auto Layout 思想完全一致:通过约束定义 View 之间的相对位置关系。
| Auto Layout 概念 | ConstraintLayout 对应 |
|---|---|
| leading/trailing constraint | app:layout_constraintStart/End_toStartOf/toEndOf |
| top/bottom constraint | app:layout_constraintTop/Bottom_toTopOf/toBottomOf |
| centerX/centerY constraint | layout_constraintStart_toStartOf="parent" + End_toEndOf="parent" |
| equal width/height constraint | layout_constraintWidth_percent / constraintDimensionRatio |
| UIStackView (horizontal) | Chain (horizontal) |
| UIStackView (vertical) | Chain (vertical) |
| UILayoutGuide | Guideline / Barrier |
| priority | layout_constraintHorizontal_bias / weight |
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<!-- 标题文本,固定在顶部(对应 iOS topLayoutGuide 约束) -->
<TextView
android:id="@+id/title_text"
android:layout_width="0dp" <!-- 0dp = match_constraint(宽度由约束决定)-->
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintTop_toTopOf="parent" <!-- 对应 iOS: top = superview.top -->
app:layout_constraintStart_toStartOf="parent" <!-- 对应 iOS: leading = superview.leading -->
app:layout_constraintEnd_toEndOf="parent" <!-- 对应 iOS: trailing = superview.trailing -->
tools:text="My App" /> <!-- tools: 只在编辑器中显示,不影响运行时 -->
<!-- 头像图片,在标题下方,水平居中(对应 iOS centerX constraint) -->
<ImageView
android:id="@+id/avatar_image"
android:layout_width="80dp"
android:layout_height="80dp"
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@id/title_text" <!-- 在标题下方 -->
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" /> <!-- 水平居中 -->
<!-- 用户名,在头像右侧 -->
<TextView
android:id="@+id/name_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
app:layout_constraintStart_toEndOf="@id/avatar_image" <!-- 在头像右侧 -->
app:layout_constraintTop_toTopOf="@id/avatar_image" <!-- 与头像顶部对齐 -->
app:layout_constraintEnd_toEndOf="parent" />
<!-- Guideline:垂直参考线,位于父布局 30% 处(对应 iOS UILayoutGuide) -->
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline_v"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.3" /> <!-- 30% 位置 -->
<!-- Barrier:动态参考线,跟随多个 View 中最大的 -->
<androidx.constraintlayout.widget.Barrier
android:id="@+id/text_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom" <!-- 在所有引用 View 的最下方 -->
app:constraint_referenced_ids="name_text,avatar_image" />
<!-- 按钮,在 Barrier 下方,水平方向 Chain -->
<Button
android:id="@+id/btn_follow"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="Follow"
app:layout_constraintTop_toBottomOf="@id/text_barrier"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/btn_message" <!-- Chain 中的约束 -->
app:layout_constraintHorizontal_chainStyle="spread" /> <!-- Chain 样式:spread均匀分布 -->
<Button
android:id="@+id/btn_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="Message"
app:layout_constraintTop_toTopOf="@id/btn_follow"
app:layout_constraintStart_toEndOf="@id/btn_follow"
app:layout_constraintEnd_toEndOf="parent" />
<!-- 列表,填充剩余空间 -->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler_view"
android:layout_width="0dp"
android:layout_height="0dp" <!-- 高度也由约束决定 -->
android:layout_marginTop="16dp"
app:layout_constraintTop_toBottomOf="@id/btn_follow"
app:layout_constraintBottom_toBottomOf="parent" <!-- 延伸到底部 -->
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
7.3 其他常用布局
<!-- LinearLayout:线性排列(类似 iOS UIStackView) -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" <!-- vertical 或 horizontal -->
android:divider="@drawable/divider"
android:showDividers="middle">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1" <!-- weight:分配剩余空间的比例(对应 iOS 的 distribution .fill) -->
android:text="Item 1" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="2" <!-- 占剩余空间的 2/3 -->
android:text="Item 2" />
</LinearLayout>
<!-- FrameLayout:帧布局(子 View 叠加,类似 iOS ZStack/overlapping subviews) -->
<FrameLayout
android:layout_width="match_parent"
android:layout_height="200dp">
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
android:src="@drawable/background" />
<!-- 遮罩层叠加在图片上 -->
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#80000000" />
<!-- 文字在最上层居中 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" <!-- gravity:子 View 在 FrameLayout 中的位置 -->
android:text="Overlay Text"
android:textColor="@color/white" />
</FrameLayout>
<!-- include:复用布局(对应 iOS 的 embed view controller 或自定义 UIView) -->
<include layout="@layout/item_header" android:id="@+id/header" />
<!-- merge:减少布局嵌套层级(配合 include 使用,去掉 include 根节点) -->
<!-- 在 item_header.xml 中,如果根节点用 merge 而非 LinearLayout,可减少一层 -->
<!-- ViewStub:按需加载的懒加载布局(对应 iOS 的 isHidden + 按需创建) -->
<ViewStub
android:id="@+id/stub_error"
android:inflatedId="@+id/error_layout"
android:layout="@layout/layout_error" <!-- 实际布局文件 -->
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<!-- 代码中按需加载 ViewStub -->
<!-- binding.stubError.inflate() // 首次加载,inflate 后 ViewStub 被替换为实际 View -->
7.4 View Binding
View Binding 是 Android 官方推荐的 View 引用方式,自动为每个 XML 布局文件生成绑定类,完全替代 findViewById,对应 iOS 的 IBOutlet。
class ProfileViewController: UIViewController {
@IBOutlet weak var nameLabel: UILabel!
@IBOutlet weak var avatarImage: UIImageView!
@IBOutlet weak var followButton: UIButton!
override func viewDidLoad() {
super.viewDidLoad()
nameLabel.text = "Alice"
avatarImage.image = UIImage(named: "avatar")
followButton.setTitle("Follow", for: .normal)
}
}
// Activity 中使用 View Binding
class ProfileActivity : AppCompatActivity() {
// ActivityProfileBinding 由 activity_profile.xml 自动生成
// 命名规则:文件名驼峰化 + Binding
private lateinit var binding: ActivityProfileBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// inflate 并设置 contentView
binding = ActivityProfileBinding.inflate(layoutInflater)
setContentView(binding.root)
// 直接通过 binding 访问 View(对应 IBOutlet)
// XML 中的 android:id="@+id/name_label" 对应 binding.nameLabel
binding.nameLabel.text = "Alice"
binding.avatarImage.setImageResource(R.drawable.avatar)
binding.followButton.text = "Follow"
}
}
// Fragment 中使用 View Binding(注意内存管理)
class ProfileFragment : Fragment() {
private var _binding: FragmentProfileBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentProfileBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.nameLabel.text = "Alice"
}
// 必须在 onDestroyView 中置 null,防止内存泄漏
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
7.5 资源系统
<!-- res/values/strings.xml(对应 iOS Localizable.strings)-->
<resources>
<string name="app_name">My App</string>
<string name="welcome_message">Welcome, %1$s!</string> <!-- 格式化字符串 -->
<string name="items_count">%d items</string>
<!-- 字符串数组 -->
<string-array name="categories">
<item>Technology</item>
<item>Science</item>
<item>Arts</item>
</string-array>
</resources>
<!-- res/values/colors.xml(对应 iOS Color Assets)-->
<resources>
<color name="primary">#6200EE</color>
<color name="primary_dark">#3700B3</color>
<color name="accent">#03DAC5</color>
<color name="background">#FFFFFF</color>
<color name="surface">#FFFFFF</color>
<color name="on_primary">#FFFFFF</color>
<color name="text_primary">#212121</color>
<color name="text_secondary">#757575</color>
</resources>
<!-- res/values/dimens.xml(对应 iOS Layout Constraints 常量)-->
<resources>
<dimen name="spacing_xs">4dp</dimen>
<dimen name="spacing_sm">8dp</dimen>
<dimen name="spacing_md">16dp</dimen>
<dimen name="spacing_lg">24dp</dimen>
<dimen name="spacing_xl">32dp</dimen>
<dimen name="text_size_body">14sp</dimen>
<dimen name="text_size_title">20sp</dimen>
<dimen name="corner_radius">8dp</dimen>
</resources>
<!-- res/values/themes.xml(对应 iOS UIAppearance 主题)-->
<resources>
<style name="Theme.MyApp" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<item name="colorPrimary">@color/primary</item>
<item name="colorPrimaryVariant">@color/primary_dark</item>
<item name="colorSecondary">@color/accent</item>
<item name="android:statusBarColor">@color/primary_dark</item>
</style>
<!-- 暗色主题(对应 iOS Dark Mode)-->
<style name="Theme.MyApp.Dark" parent="Theme.MaterialComponents.DayNight">
<item name="colorPrimary">@color/primary_dark</item>
</style>
</resources>
// 访问字符串资源(对应 iOS NSLocalizedString)
val appName = getString(R.string.app_name)
val welcome = getString(R.string.welcome_message, "Alice") // 格式化参数
// 访问颜色(对应 iOS UIColor(named:))
val primaryColor = ContextCompat.getColor(context, R.color.primary)
// 访问尺寸
val spacing = resources.getDimensionPixelSize(R.dimen.spacing_md)
// 访问图片(对应 iOS UIImage(named:))
val drawable = ContextCompat.getDrawable(context, R.drawable.ic_user)
imageView.setImageResource(R.drawable.ic_user)
// 适配暗色模式
val isNightMode = resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
// dp 转 px(对应 iOS 的 point * scale)
fun Int.dpToPx(context: Context): Int {
return (this * context.resources.displayMetrics.density).toInt()
}
val padding = 16.dpToPx(context)
常用 View 控件
本章介绍 Android 最常用的控件,重点讲解 RecyclerView(对应 UITableView/UICollectionView),以及 BottomNavigationView、Toolbar 等 Material Design 组件。
8.1 基础控件对比
| iOS 控件 | Android 对应 | 说明 |
|---|---|---|
| UILabel | TextView | 文本显示 |
| UITextField | EditText | 单行文本输入 |
| UITextView | EditText (multiline) | 多行文本输入/显示 |
| UIButton | Button / MaterialButton | 按钮 |
| UIImageView | ImageView | 图片显示 |
| UISwitch | Switch / SwitchCompat | 开关 |
| UISlider | Slider (Material) / SeekBar | 滑动条 |
| UIProgressView | ProgressBar (horizontal) | 进度条 |
| UIActivityIndicatorView | CircularProgressIndicator | 加载指示器 |
| UITableView | RecyclerView (Linear) | 垂直滚动列表 |
| UICollectionView | RecyclerView (Grid/Staggered) | 网格列表 |
| UIScrollView | ScrollView / NestedScrollView | 可滚动容器 |
| UITabBar | BottomNavigationView | 底部标签栏 |
| UINavigationBar | Toolbar / ActionBar | 顶部导航栏 |
| UIAlertController | AlertDialog / Snackbar | 弹窗/提示 |
| WKWebView | WebView | 网页视图 |
| MKMapView | MapView (Google Maps SDK) | 地图视图 |
8.2 RecyclerView 完整实现
RecyclerView 是 Android 中最核心的控件,对应 iOS 的 UITableView 和 UICollectionView。它通过 Adapter + ViewHolder 模式实现高效的列表渲染。
RecyclerView 把 UITableView 的职责进行了更细粒度的分离:
• LayoutManager = UITableView.style(线性、网格等布局策略)
• Adapter = UITableViewDataSource(数据源和 Cell 创建)
• ViewHolder = 对应 UITableViewCell 的视图复用机制
• ItemDecoration = separatorStyle(间距、分割线)
• DiffUtil = DiffableDataSource(计算差异并高效更新)
8.2.1 数据模型
// 文章数据模型
data class Article(
val id: Int,
val title: String,
val summary: String,
val imageUrl: String?,
val author: String,
val publishedAt: String,
val isBookmarked: Boolean = false
)
8.2.2 Item 布局
<?xml version="1.0" encoding="utf-8"?>
<!-- CardView 包裹(对应 iOS UITableViewCell 带 content view)-->
<com.google.android.material.card.MaterialCardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:cardCornerRadius="8dp"
app:cardElevation="2dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="12dp">
<ImageView
android:id="@+id/article_image"
android:layout_width="80dp"
android:layout_height="80dp"
android:scaleType="centerCrop"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/article_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:maxLines="2"
android:ellipsize="end"
android:textSize="16sp"
android:textStyle="bold"
android:textColor="?attr/colorOnSurface"
app:layout_constraintStart_toEndOf="@id/article_image"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/article_image" />
<TextView
android:id="@+id/article_summary"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="4dp"
android:maxLines="2"
android:ellipsize="end"
android:textSize="13sp"
android:textColor="?attr/colorOnSurfaceVariant"
app:layout_constraintStart_toEndOf="@id/article_image"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/article_title" />
<TextView
android:id="@+id/article_author"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="12sp"
android:textColor="?attr/colorPrimary"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@id/bookmark_button"
app:layout_constraintTop_toBottomOf="@id/article_image" />
<ImageButton
android:id="@+id/bookmark_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:src="@drawable/ic_bookmark_border"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
8.2.3 Adapter 与 ViewHolder
// ListAdapter 是 RecyclerView.Adapter 的增强版
// 自动使用 DiffUtil 计算差异(对应 iOS DiffableDataSource)
class ArticleAdapter(
private val onItemClick: (Article) -> Unit,
private val onBookmarkClick: (Article) -> Unit
) : ListAdapter<Article, ArticleAdapter.ArticleViewHolder>(ArticleDiffCallback()) {
// DiffUtil.ItemCallback 定义如何判断两个 item 是否相同
// 对应 iOS DiffableDataSource 的 hashValue 和 == 判断
class ArticleDiffCallback : DiffUtil.ItemCallback<Article>() {
// 判断是否是同一个 item(通常用 ID 判断)
override fun areItemsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem.id == newItem.id
}
// 判断内容是否相同(内容相同则不重绘)
override fun areContentsTheSame(oldItem: Article, newItem: Article): Boolean {
return oldItem == newItem // data class 自动生成 equals()
}
// 可选:计算局部更新(避免整个 item 重绘)
override fun getChangePayload(oldItem: Article, newItem: Article): Any? {
return if (oldItem.isBookmarked != newItem.isBookmarked) {
"bookmark_changed"
} else null
}
}
// ViewHolder:持有每个列表项的 View 引用
// 对应 iOS UITableViewCell(View 复用机制)
class ArticleViewHolder(
private val binding: ItemArticleBinding
) : RecyclerView.ViewHolder(binding.root) {
// bind:将数据绑定到 View(对应 iOS UITableViewCell.configure(with:))
fun bind(article: Article, onItemClick: (Article) -> Unit, onBookmarkClick: (Article) -> Unit) {
binding.articleTitle.text = article.title
binding.articleSummary.text = article.summary
binding.articleAuthor.text = article.author
// 使用 Glide 加载图片(对应 iOS Kingfisher/SDWebImage)
Glide.with(binding.root.context)
.load(article.imageUrl)
.placeholder(R.drawable.placeholder_article)
.error(R.drawable.error_image)
.centerCrop()
.into(binding.articleImage)
// 书签状态
binding.bookmarkButton.setImageResource(
if (article.isBookmarked) R.drawable.ic_bookmark else R.drawable.ic_bookmark_border
)
// 点击事件(在 ViewHolder 设置,不在 onBindViewHolder)
binding.root.setOnClickListener { onItemClick(article) }
binding.bookmarkButton.setOnClickListener { onBookmarkClick(article) }
}
// 局部更新(只更新书签状态,不重绘整个 item)
fun updateBookmark(isBookmarked: Boolean) {
binding.bookmarkButton.setImageResource(
if (isBookmarked) R.drawable.ic_bookmark else R.drawable.ic_bookmark_border
)
}
}
// 创建 ViewHolder(对应 iOS tableView(_:cellForRowAt:) 的 dequeueReusableCell)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ArticleViewHolder {
val binding = ItemArticleBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
return ArticleViewHolder(binding)
}
// 绑定数据到 ViewHolder(对应 iOS configureCell 逻辑)
override fun onBindViewHolder(holder: ArticleViewHolder, position: Int) {
holder.bind(getItem(position), onItemClick, onBookmarkClick)
}
// 局部更新(使用 payload 避免整 item 重绘)
override fun onBindViewHolder(
holder: ArticleViewHolder,
position: Int,
payloads: List<Any>
) {
if (payloads.isNotEmpty() && payloads[0] == "bookmark_changed") {
holder.updateBookmark(getItem(position).isBookmarked)
} else {
super.onBindViewHolder(holder, position, payloads)
}
}
}
8.2.4 多类型 Item
// 多类型列表(对应 iOS UICollectionViewDifferableDataSource 的多 Section)
sealed class FeedItem {
data class Header(val title: String, val subtitle: String) : FeedItem()
data class ArticleItem(val article: Article) : FeedItem()
data class BannerItem(val imageUrl: String, val link: String) : FeedItem()
object LoadingMore : FeedItem()
}
class FeedAdapter(
private val onArticleClick: (Article) -> Unit
) : ListAdapter<FeedItem, RecyclerView.ViewHolder>(FeedDiffCallback()) {
companion object {
const val TYPE_HEADER = 0
const val TYPE_ARTICLE = 1
const val TYPE_BANNER = 2
const val TYPE_LOADING = 3
}
// 根据数据类型返回 ViewType(对应 iOS numberOfSections/cellForRow 的类型判断)
override fun getItemViewType(position: Int): Int = when (getItem(position)) {
is FeedItem.Header -> TYPE_HEADER
is FeedItem.ArticleItem -> TYPE_ARTICLE
is FeedItem.BannerItem -> TYPE_BANNER
is FeedItem.LoadingMore -> TYPE_LOADING
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return when (viewType) {
TYPE_HEADER -> HeaderViewHolder(
ItemHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
TYPE_ARTICLE -> ArticleAdapter.ArticleViewHolder(
ItemArticleBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
TYPE_BANNER -> BannerViewHolder(
ItemBannerBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
else -> LoadingViewHolder(
ItemLoadingBinding.inflate(LayoutInflater.from(parent.context), parent, false)
)
}
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (val item = getItem(position)) {
is FeedItem.Header -> (holder as HeaderViewHolder).bind(item)
is FeedItem.ArticleItem -> (holder as ArticleAdapter.ArticleViewHolder).bind(
item.article, onArticleClick, {}
)
is FeedItem.BannerItem -> (holder as BannerViewHolder).bind(item)
is FeedItem.LoadingMore -> { /* 无需绑定 */ }
}
}
class FeedDiffCallback : DiffUtil.ItemCallback<FeedItem>() {
override fun areItemsTheSame(old: FeedItem, new: FeedItem): Boolean = when {
old is FeedItem.Header && new is FeedItem.Header -> old.title == new.title
old is FeedItem.ArticleItem && new is FeedItem.ArticleItem -> old.article.id == new.article.id
old is FeedItem.BannerItem && new is FeedItem.BannerItem -> old.imageUrl == new.imageUrl
old is FeedItem.LoadingMore && new is FeedItem.LoadingMore -> true
else -> false
}
override fun areContentsTheSame(old: FeedItem, new: FeedItem) = old == new
}
}
8.2.5 在 Fragment 中使用 RecyclerView
@AndroidEntryPoint
class ArticleListFragment : Fragment() {
private var _binding: FragmentArticleListBinding? = null
private val binding get() = _binding!!
private val viewModel: ArticleViewModel by viewModels()
private lateinit var adapter: ArticleAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = FragmentArticleListBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
observeViewModel()
}
private fun setupRecyclerView() {
adapter = ArticleAdapter(
onItemClick = { article ->
findNavController().navigate(
ArticleListFragmentDirections.actionToDetail(articleId = article.id)
)
},
onBookmarkClick = { article ->
viewModel.toggleBookmark(article)
}
)
binding.recyclerView.apply {
this.adapter = this@ArticleListFragment.adapter
// LinearLayoutManager:垂直列表(对应 UITableView)
layoutManager = LinearLayoutManager(requireContext())
// GridLayoutManager:网格(对应 UICollectionView flow layout)
// layoutManager = GridLayoutManager(requireContext(), 2)
// 添加 item 间距装饰(对应 iOS minimumLineSpacing)
addItemDecoration(
MaterialDividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL)
)
// 设置 item 动画(新增/删除/移动动画)
itemAnimator = DefaultItemAnimator()
// 预加载(Prefetch)优化滚动性能
setHasFixedSize(true) // 如果列表大小不变(item 数量变化但总高度固定)
}
// 下拉刷新
binding.swipeRefreshLayout.setOnRefreshListener {
viewModel.refresh()
}
}
private fun observeViewModel() {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.articles.collect { articles ->
// submitList 触发 DiffUtil 计算,只更新变化的 item
adapter.submitList(articles)
binding.swipeRefreshLayout.isRefreshing = false
binding.emptyView.isVisible = articles.isEmpty()
}
}
}
}
override fun onDestroyView() {
super.onDestroyView()
// 清除 RecyclerView 的 adapter 引用,防止内存泄漏
binding.recyclerView.adapter = null
_binding = null
}
}
8.3 BottomNavigationView(对比 UITabBarController)
<!-- activity_main.xml -->
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 内容区域,NavHostFragment 容器 -->
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<!-- 底部导航栏(对应 iOS UITabBar)-->
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_nav"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
app:menu="@menu/bottom_nav_menu" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<!-- res/menu/bottom_nav_menu.xml(对应 iOS UITabBarItem)-->
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/nav_home"
android:icon="@drawable/ic_home"
android:title="@string/tab_home" />
<item
android:id="@+id/nav_search"
android:icon="@drawable/ic_search"
android:title="@string/tab_search" />
<item
android:id="@+id/nav_bookmarks"
android:icon="@drawable/ic_bookmark"
android:title="@string/tab_bookmarks" />
<item
android:id="@+id/nav_profile"
android:icon="@drawable/ic_person"
android:title="@string/tab_profile" />
</menu>
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
// 获取 NavController(Navigation Component)
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
// 将 BottomNavigationView 与 NavController 绑定
// 对应 iOS UITabBarController 的自动 VC 切换
binding.bottomNav.setupWithNavController(navController)
// 不在顶级目标时隐藏底部导航
val topLevelDestinations = setOf(
R.id.nav_home, R.id.nav_search, R.id.nav_bookmarks, R.id.nav_profile
)
navController.addOnDestinationChangedListener { _, destination, _ ->
binding.bottomNav.isVisible = destination.id in topLevelDestinations
}
}
}
Navigation Component(对比 iOS Navigation)
Navigation Component 是 Jetpack 提供的导航框架,通过可视化的导航图(NavGraph)管理 Fragment 之间的跳转。对应 iOS 的 UINavigationController + Storyboard Segue,但功能更加强大和灵活。
| iOS Navigation | Android Navigation Component |
|---|---|
| UINavigationController | NavController + NavHostFragment |
| Storyboard + Segue | NavGraph (nav_graph.xml) |
| performSegue(withIdentifier:) | navController.navigate(action) |
| prepare(for:sender:) | Safe Args (编译时类型安全的参数传递) |
| navigationController?.popViewController | navController.popBackStack() |
| navigationController?.popToRootViewController | navController.popBackStack(rootId, false) |
| Universal Links | Deep Link |
9.1 NavGraph 配置
<?xml version="1.0" encoding="utf-8"?>
<navigation
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/nav_graph"
app:startDestination="@id/homeFragment"> <!-- 对应 iOS Initial ViewController -->
<!-- Home Fragment(对应 iOS 根 VC)-->
<fragment
android:id="@+id/homeFragment"
android:name="com.example.app.ui.home.HomeFragment"
android:label="Home"
tools:layout="@layout/fragment_home">
<!-- Action:从 Home 导航到 Detail(对应 iOS Segue)-->
<action
android:id="@+id/action_home_to_detail"
app:destination="@id/detailFragment"
app:enterAnim="@anim/slide_in_right"
app:exitAnim="@anim/slide_out_left"
app:popEnterAnim="@anim/slide_in_left"
app:popExitAnim="@anim/slide_out_right" />
<!-- Action:从 Home 导航到 Search -->
<action
android:id="@+id/action_home_to_search"
app:destination="@id/searchFragment" />
</fragment>
<!-- Detail Fragment -->
<fragment
android:id="@+id/detailFragment"
android:name="com.example.app.ui.detail.DetailFragment"
android:label="Detail"
tools:layout="@layout/fragment_detail">
<!-- 接收参数(Safe Args)-->
<argument
android:name="articleId"
app:argType="integer" <!-- 类型安全!对应 iOS 的强类型参数 -->
android:defaultValue="-1" />
<argument
android:name="articleTitle"
app:argType="string"
app:nullable="true"
android:defaultValue="@null" />
</fragment>
<!-- Search Fragment -->
<fragment
android:id="@+id/searchFragment"
android:name="com.example.app.ui.search.SearchFragment"
android:label="Search"
tools:layout="@layout/fragment_search">
<!-- Deep Link(对应 iOS Universal Links)-->
<deepLink
android:id="@+id/deepLink_search"
app:uri="https://example.com/search?query={query}" />
<deepLink
app:uri="myapp://search/{query}" /> <!-- 自定义 Scheme -->
</fragment>
<!-- 嵌套导航图(对应 iOS 嵌套 NavigationController)-->
<navigation
android:id="@+id/profile_nav_graph"
app:startDestination="@id/profileFragment">
<fragment android:id="@+id/profileFragment"
android:name="com.example.app.ui.profile.ProfileFragment" />
<fragment android:id="@+id/editProfileFragment"
android:name="com.example.app.ui.profile.EditProfileFragment" />
</navigation>
</navigation>
9.2 Safe Args 参数传递
Safe Args 是 Navigation Component 的 Gradle 插件,自动生成类型安全的导航代码,避免手动 putExtra/getExtra 的字符串 key 错误。
class HomeFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
adapter.setOnItemClickListener { article ->
// Safe Args 自动生成 HomeFragmentDirections 类
// 对应 iOS: performSegue(withIdentifier: "showDetail", sender: article)
val action = HomeFragmentDirections.actionHomeToDetail(
articleId = article.id, // 类型安全,编译时检查
articleTitle = article.title // 不会有 key 拼写错误
)
findNavController().navigate(action)
}
// 带 NavOptions 的导航(类比 iOS 各种 push 选项)
binding.settingsButton.setOnClickListener {
findNavController().navigate(
R.id.action_home_to_settings,
null,
NavOptions.Builder()
.setLaunchSingleTop(true) // 避免重复创建(对应 iOS checkSingleTop)
.setPopUpTo(R.id.homeFragment, false) // 清除返回栈到 Home
.build()
)
}
}
}
// DetailFragment:接收 Safe Args 参数
class DetailFragment : Fragment() {
// Safe Args 生成 DetailFragmentArgs,比 intent.getExtra 更安全
private val args: DetailFragmentArgs by navArgs()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 直接通过 args 访问参数(类型安全)
val articleId = args.articleId
val articleTitle = args.articleTitle ?: "Untitled"
binding.titleText.text = articleTitle
viewModel.loadArticle(articleId)
// 设置返回按钮(对应 iOS navigationItem.backButton)
binding.toolbar.setNavigationOnClickListener {
findNavController().popBackStack() // 对应 iOS navigationController?.popViewController
}
}
}
9.3 Deep Link
// AndroidManifest.xml 中已通过 nav_graph 自动处理 intent-filter
// 无需手动编写(对应 iOS Info.plist URL Schemes 或 Associated Domains)
// 在 Activity 中处理 Deep Link(Navigation Component 自动处理)
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
val navController = findNavController(R.id.nav_host_fragment)
// 处理 Deep Link Intent(Navigation Component 自动解析)
val navDeepLinkBuilder = NavDeepLinkBuilder(this)
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.detailFragment)
.setArguments(Bundle().apply {
putInt("articleId", 123)
})
// 创建 PendingIntent(用于通知点击)
val pendingIntent = navDeepLinkBuilder.createPendingIntent()
}
}
// 发送带 Deep Link 的通知(对应 iOS UNUserNotificationCenter)
fun sendNotificationWithDeepLink(context: Context, articleId: Int, title: String) {
val args = Bundle().apply { putInt("articleId", articleId) }
val pendingIntent = NavDeepLinkBuilder(context)
.setComponentName(MainActivity::class.java)
.setGraph(R.navigation.nav_graph)
.setDestination(R.id.detailFragment)
.setArguments(args)
.createPendingIntent()
val notification = NotificationCompat.Builder(context, "channel_id")
.setContentTitle("New Article")
.setContentText(title)
.setSmallIcon(R.drawable.ic_notification)
.setContentIntent(pendingIntent)
.setAutoCancel(true)
.build()
NotificationManagerCompat.from(context).notify(articleId, notification)
}
9.4 Bottom Navigation + Navigation Component 完整集成
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_graph"
app:startDestination="@id/homeFragment">
<!-- 四个底部 Tab 对应的 Fragment -->
<fragment android:id="@+id/homeFragment"
android:name="com.example.app.ui.home.HomeFragment"
android:label="Home">
<action android:id="@+id/action_home_to_detail"
app:destination="@id/detailFragment"
app:popUpTo="@id/homeFragment" />
</fragment>
<fragment android:id="@+id/searchFragment"
android:name="com.example.app.ui.search.SearchFragment"
android:label="Search" />
<fragment android:id="@+id/bookmarksFragment"
android:name="com.example.app.ui.bookmarks.BookmarksFragment"
android:label="Bookmarks" />
<fragment android:id="@+id/profileFragment"
android:name="com.example.app.ui.profile.ProfileFragment"
android:label="Profile">
<action android:id="@+id/action_profile_to_edit"
app:destination="@id/editProfileFragment" />
</fragment>
<!-- 不是 Tab 的 Fragment -->
<fragment android:id="@+id/detailFragment"
android:name="com.example.app.ui.detail.DetailFragment">
<argument android:name="articleId" app:argType="integer" />
</fragment>
<fragment android:id="@+id/editProfileFragment"
android:name="com.example.app.ui.profile.EditProfileFragment" />
</navigation>
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
// BottomNavigationView 与 NavController 绑定
// Menu item ID 必须与 NavGraph destination ID 一致!
binding.bottomNav.setupWithNavController(navController)
// 顶级目标集合(Tab 对应的目标,这些目标不显示返回按钮)
val appBarConfiguration = AppBarConfiguration(
setOf(
R.id.homeFragment,
R.id.searchFragment,
R.id.bookmarksFragment,
R.id.profileFragment
)
)
setupActionBarWithNavController(navController, appBarConfiguration)
// 监听导航目标变化
navController.addOnDestinationChangedListener { _, destination, _ ->
// 非顶级目标时隐藏底部导航(沉浸式体验)
binding.bottomNav.isVisible = destination.id in setOf(
R.id.homeFragment, R.id.searchFragment,
R.id.bookmarksFragment, R.id.profileFragment
)
}
}
// 支持系统返回按钮和 ActionBar 返回按钮
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp() || super.onSupportNavigateUp()
}
}
ViewModel & LiveData
ViewModel 是 Android MVVM 架构的核心,解决了 iOS 开发中常见的"ViewController 过胖"问题。配合 LiveData/StateFlow,实现完整的响应式数据流。
10.1 ViewModel 生命周期
Android ViewModel 在 屏幕旋转/配置变更 时会存活,而 Activity/Fragment 会销毁重建。这解决了 iOS 中因系统强制 VC 销毁导致数据丢失的问题。
iOS 中你可能用过在 AppDelegate 或 SceneDelegate 中存储状态来应对这个问题——Android 的 ViewModel 给了一个优雅的解决方案。
10.2 LiveData vs Combine/RxSwift
| 概念 | iOS | Android |
|---|---|---|
| 可观察属性 | @Published var name = "" | MutableLiveData<String>() 或 MutableStateFlow("") |
| 订阅/观察 | $name.sink { } / AnyCancellable | liveData.observe(owner) { } |
| 生命周期感知 | 需手动管理 cancellables | 自动感知(传入 LifecycleOwner) |
| 主线程更新 | @MainActor 或 receive(on: .main) | LiveData 自动在主线程,Flow 需 flowWithLifecycle |
| 转换 | .map { } / .combineLatest() | Transformations.map { } / combine() |
| 冷/热流 | Publisher(冷) / PassthroughSubject(热) | Flow(冷) / StateFlow/SharedFlow(热) |
10.3 LiveData 实战
@HiltViewModel
class UserViewModel @Inject constructor(
private val userRepository: UserRepository
) : ViewModel() {
// MutableLiveData:内部可修改(对应 iOS @Published private(set) var)
private val _users = MutableLiveData<List<User>>()
// LiveData:外部只读(对应 iOS AnyPublisher)
val users: LiveData<List<User>> = _users
// 使用 liveData builder(对应 iOS Publisher 的 tryMap)
val currentUser: LiveData<User?> = liveData {
val user = userRepository.getCurrentUser()
emit(user)
}
// Transformations.map(对应 iOS .map { })
val userCount: LiveData<String> = Transformations.map(_users) { users ->
"${users.size} users"
}
// Transformations.switchMap:根据值变化切换数据源
private val _userId = MutableLiveData<Int>()
val userDetail: LiveData<User?> = Transformations.switchMap(_userId) { id ->
userRepository.getUserById(id) // 返回 LiveData
}
// MediatorLiveData:合并多个 LiveData(对应 iOS combineLatest)
val combinedState = MediatorLiveData<Pair<List<User>, Boolean>>().apply {
var users: List<User> = emptyList()
var isLoading = false
addSource(_users) { newUsers ->
users = newUsers
value = Pair(users, isLoading)
}
addSource(_isLoading) { loading ->
isLoading = loading
value = Pair(users, isLoading)
}
}
private val _isLoading = MutableLiveData(false)
init {
loadUsers()
}
fun loadUsers() {
viewModelScope.launch {
_isLoading.value = true
try {
val userList = userRepository.fetchUsers()
_users.value = userList // 更新 LiveData(自动在主线程)
} catch (e: Exception) {
// 错误处理
} finally {
_isLoading.value = false
}
}
}
fun loadUserById(id: Int) {
_userId.value = id // 触发 switchMap
}
// ViewModel 销毁时调用(对应 iOS deinit)
override fun onCleared() {
super.onCleared()
// 清理资源(viewModelScope 协程会自动取消)
}
}
10.4 StateFlow vs LiveData(现代推荐)
StateFlow 是 Kotlin Flow 的一种,比 LiveData 更现代,与协程集成更好。Google 推荐在新项目中使用 StateFlow/SharedFlow 替代 LiveData。
class ArticleViewModel: ObservableObject {
@Published var articles: [Article] = []
@Published var isLoading: Bool = false
@Published var errorMessage: String? = nil
private var cancellables = Set<AnyCancellable>()
func loadArticles() {
isLoading = true
apiService.fetchArticles()
.receive(on: DispatchQueue.main)
.sink(
receiveCompletion: { [weak self] completion in
self?.isLoading = false
if case .failure(let error) = completion {
self?.errorMessage = error.localizedDescription
}
},
receiveValue: { [weak self] articles in
self?.articles = articles
}
)
.store(in: &cancellables)
}
}
@HiltViewModel
class ArticleViewModel @Inject constructor(
private val getArticlesUseCase: GetArticlesUseCase
) : ViewModel() {
// StateFlow:当前状态(对应 @Published,热流,始终有值)
private val _uiState = MutableStateFlow<ArticleUiState>(ArticleUiState.Loading)
val uiState: StateFlow<ArticleUiState> = _uiState.asStateFlow()
// SharedFlow:一次性事件(对应 PassthroughSubject,用于导航/Snackbar)
private val _events = MutableSharedFlow<ArticleEvent>()
val events: SharedFlow<ArticleEvent> = _events.asSharedFlow()
init {
loadArticles()
}
fun loadArticles() {
viewModelScope.launch {
_uiState.value = ArticleUiState.Loading
getArticlesUseCase()
.catch { e ->
_uiState.value = ArticleUiState.Error(e.message ?: "Error")
}
.collect { articles ->
_uiState.value = ArticleUiState.Success(articles)
}
}
}
fun onArticleClicked(articleId: Int) {
viewModelScope.launch {
// 发射一次性事件(对应 iOS passthrough subject)
_events.emit(ArticleEvent.NavigateToDetail(articleId))
}
}
}
// Fragment 中观察
class ArticleFragment : Fragment() {
private val viewModel: ArticleViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
// 观察状态
launch {
viewModel.uiState.collect { state ->
when (state) {
is ArticleUiState.Loading -> showLoading()
is ArticleUiState.Success -> showArticles(state.articles)
is ArticleUiState.Error -> showError(state.message)
}
}
}
// 观察一次性事件
launch {
viewModel.events.collect { event ->
when (event) {
is ArticleEvent.NavigateToDetail -> {
findNavController().navigate(
ArticleFragmentDirections.actionToDetail(event.articleId)
)
}
}
}
}
}
}
}
}
10.5 双向数据绑定(Two-Way Data Binding)
<?xml version="1.0" encoding="utf-8"?>
<!-- Data Binding 布局(根标签是 layout)-->
<layout xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<!-- 声明 ViewModel 变量 -->
<variable
name="viewModel"
type="com.example.app.ui.form.FormViewModel" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<!-- @= 双向绑定:EditText 内容变化时自动更新 ViewModel -->
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={viewModel.userName}" <!-- 对应 iOS @Binding -->
android:hint="@string/hint_name" />
<!-- @ 单向绑定:只显示 ViewModel 数据 -->
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{`Hello, ` + viewModel.userName}" />
<!-- 绑定点击事件 -->
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Submit"
android:onClick="@{() -> viewModel.onSubmit()}" />
</LinearLayout>
</layout>
class FormViewModel : ViewModel() {
// ObservableField 或 MutableStateFlow 都可以配合 Data Binding
val userName = MutableLiveData("")
val email = MutableLiveData("")
val password = MutableLiveData("")
// 计算属性:基于多个字段(对应 iOS Combine.combineLatest)
val isFormValid: LiveData<Boolean> = MediatorLiveData<Boolean>().apply {
val validate = {
value = !userName.value.isNullOrBlank() &&
email.value?.contains("@") == true &&
(password.value?.length ?: 0) >= 6
}
addSource(userName) { validate() }
addSource(email) { validate() }
addSource(password) { validate() }
}
fun onSubmit() {
viewModelScope.launch {
// 提交表单逻辑
}
}
}
// Fragment 使用 Data Binding
class FormFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = FragmentFormBinding.inflate(inflater, container, false)
binding.lifecycleOwner = viewLifecycleOwner // 让 LiveData 感知生命周期
binding.viewModel = viewModel // 绑定 ViewModel
return binding.root
}
}
LiveData:适合传统 View 系统,与 lifecycleOwner 集成简单,代码简洁
StateFlow:适合 Kotlin 协程 + Flow 全套,功能更强(支持 combine/zip/flatMap),推荐新项目使用
SharedFlow:适合一次性事件(导航、Snackbar),不存储历史值
总结:新项目推荐 StateFlow + SharedFlow,老项目可以 LiveData 和 StateFlow 混用
协程 (Coroutines) 与 Flow
Kotlin 协程是 Android 异步编程的标准解决方案,对应 Swift 的 async/await + GCD。协程的核心思想是:用同步的方式写异步代码,避免回调地狱。
| 概念 | Swift/iOS | Kotlin/Android |
|---|---|---|
| 异步函数 | async func fetchData() | suspend fun fetchData() |
| 调用异步函数 | await fetchData() | fetchData() (suspend 函数内直接调用) |
| 启动异步任务 | Task { ... } | launch { ... } |
| 并发任务 | async let / TaskGroup | async { ... } + Deferred.await() |
| 主线程 | @MainActor / DispatchQueue.main | Dispatchers.Main |
| 后台线程 | DispatchQueue.global() | Dispatchers.IO / Dispatchers.Default |
| 取消 | Task.cancel() | Job.cancel() / scope 取消时自动取消 |
| 超时 | withTimeout() | withTimeout { } / withTimeoutOrNull { } |
| 数据流 | AsyncStream / Combine Publisher | Flow / StateFlow / SharedFlow |
11.1 协程基础
// suspend 函数:可以被挂起(不阻塞线程)和恢复
// 对应 Swift async func
suspend fun fetchUser(id: String): User {
// delay 不会阻塞线程,只是挂起协程(对应 Swift Task.sleep)
delay(100)
return apiService.getUser(id) // 网络请求(底层是挂起操作)
}
// CoroutineScope 定义协程的生命周期
class MyClass {
// 自定义 Scope,需要手动取消
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
fun start() {
// launch:启动协程,不关心返回值(对应 Swift Task { })
scope.launch {
try {
val user = fetchUser("123") // 挂起等待,不阻塞主线程
updateUI(user) // 自动在 Main 线程执行
} catch (e: Exception) {
showError(e.message)
}
}
}
fun cleanup() {
scope.cancel() // 取消所有在此 scope 中的协程
}
}
// Dispatchers(线程调度,对应 iOS DispatchQueue)
suspend fun processData() {
// Dispatchers.Main:主线程,用于 UI 操作
withContext(Dispatchers.Main) {
textView.text = "Loading..."
}
// Dispatchers.IO:IO 线程池,用于网络请求、文件读写(64个线程)
val data = withContext(Dispatchers.IO) {
database.query("SELECT * FROM users") // 数据库查询
}
// Dispatchers.Default:计算线程池,用于 CPU 密集型操作(CPU 核心数个线程)
val result = withContext(Dispatchers.Default) {
data.sortedBy { it.name } // 大量计算
}
withContext(Dispatchers.Main) {
showData(result) // 切回主线程更新 UI
}
}
11.2 在 ViewModel 中使用协程
@HiltViewModel
class DataViewModel @Inject constructor(
private val repository: DataRepository
) : ViewModel() {
private val _state = MutableStateFlow<UiState>(UiState.Loading)
val state: StateFlow<UiState> = _state.asStateFlow()
init {
loadData()
}
fun loadData() {
// viewModelScope:绑定到 ViewModel 生命周期的 Scope
// ViewModel 销毁时(onCleared),scope 自动取消所有协程
viewModelScope.launch {
_state.value = UiState.Loading
try {
val data = repository.fetchData() // suspend 函数,挂起等待
_state.value = UiState.Success(data)
} catch (e: Exception) {
_state.value = UiState.Error(e.message ?: "Unknown error")
}
}
}
// async/await:并发执行多个异步任务
fun loadParallelData() {
viewModelScope.launch {
_state.value = UiState.Loading
try {
// async 启动并发任务(对应 Swift async let)
val userDeferred = async { repository.fetchUser() }
val articlesDeferred = async { repository.fetchArticles() }
val notificationsDeferred = async { repository.fetchNotifications() }
// await 等待所有任务完成(对应 Swift await withTaskGroup)
val user = userDeferred.await()
val articles = articlesDeferred.await()
val notifications = notificationsDeferred.await()
_state.value = UiState.Success(
DashboardData(user, articles, notifications)
)
} catch (e: Exception) {
_state.value = UiState.Error(e.message ?: "Error")
}
}
}
// withTimeout:超时控制
fun loadWithTimeout() {
viewModelScope.launch {
try {
val data = withTimeout(5000L) { // 5 秒超时
repository.fetchData()
}
_state.value = UiState.Success(data)
} catch (e: TimeoutCancellationException) {
_state.value = UiState.Error("Request timeout")
}
}
}
}
11.3 Flow 详解
Flow 是 Kotlin 的数据流 API,对应 iOS 的 Combine Publisher 或 AsyncStream。它支持冷启动、背压处理和丰富的操作符。
Flow 有三种常见形态,选错会导致数据丢失或重复处理,必须搞清楚:
- Flow(冷流):类似 iOS Publisher。没有订阅者时不产生数据;每个新订阅者都从头开始获取数据。适合"一次性请求",如网络请求、数据库单次查询。
- StateFlow(热流,有状态):类似 iOS
CurrentValueSubject。始终持有一个当前值,新订阅者立即收到最新值,多个订阅者共享同一个数据流。适合表示 UI 状态(加载中/成功/失败)。 - SharedFlow(热流,无初始值):类似 iOS
PassthroughSubject。不持有当前值,新订阅者不会收到历史数据。适合表示"一次性事件",如弹出 Toast、页面导航。
简单记忆:UI 状态用 StateFlow,一次性事件用 SharedFlow,数据管道用 Flow。
// 创建 Flow(对应 iOS Combine 的 Publisher)
fun userFlow(): Flow<User> = flow {
emit(localDatabase.getUser()) // 先发射本地数据
val remote = apiService.getUser() // 再获取远程数据
emit(remote) // 再发射远程数据
}
// Room 自动返回 Flow(数据库变化时自动发射新值)
@Dao
interface UserDao {
@Query("SELECT * FROM users")
fun getAllUsers(): Flow<List<User>> // 数据库变化 → Flow 自动发射
}
// Flow 操作符(与 iOS Combine 高度相似)
fun processUserFlow() {
userRepository.getUsers()
.filter { users -> users.isNotEmpty() } // 过滤
.map { users -> users.map { it.name } } // 转换
.onEach { names -> println("Got $names") } // 副作用(对应 Combine .handleEvents)
.catch { e -> emit(emptyList()) } // 错误处理(对应 .catch)
.flowOn(Dispatchers.IO) // 指定上游运行的线程(对应 .subscribe(on:))
.collect { names -> updateUI(names) } // 收集(对应 .sink)
}
// StateFlow:热流,有当前状态(对应 iOS CurrentValueSubject)
val stateFlow = MutableStateFlow(0) // 初始值 0
stateFlow.value = 1 // 更新值(对应 subject.send(1))
val current = stateFlow.value // 获取当前值(对应 subject.value)
// SharedFlow:热流,无初始状态(对应 iOS PassthroughSubject)
val sharedFlow = MutableSharedFlow<String>()
viewModelScope.launch {
sharedFlow.emit("Hello") // 发射值(对应 subject.send())
}
// 合并多个 Flow(对应 iOS combineLatest)
fun combinedFlow(): Flow<CombinedData> {
return combine(
userRepository.getUsers(),
articleRepository.getArticles()
) { users, articles ->
CombinedData(users, articles)
}
}
// zip:一一对应合并(对应 iOS .zip)
val zipped: Flow<Pair<Int, String>> = flowOf(1, 2, 3)
.zip(flowOf("a", "b", "c")) { n, s -> Pair(n, s) }
// flatMapLatest:取消之前的,只处理最新的(对应 iOS .switchToLatest())
fun searchFlow(queryFlow: Flow<String>): Flow<List<Result>> {
return queryFlow
.debounce(300) // 防抖,等待 300ms 无输入后再搜索(对应 iOS debounce)
.filter { it.length >= 2 }
.flatMapLatest { query ->
// 每次 query 变化时,取消上一个搜索
repository.search(query)
}
}
// collect(对应 iOS .sink,需要在协程中调用)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiFlow.collect { data ->
adapter.submitList(data)
}
}
}
11.4 错误处理
// Result 封装(对应 Swift Result<T, Error>)
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
object Loading : Result<Nothing>()
}
// 在 Repository 层封装 Result
suspend fun fetchUserSafe(id: String): Result<User> {
return try {
val user = apiService.getUser(id)
Result.Success(user)
} catch (e: IOException) {
Result.Error(e) // 网络错误
} catch (e: HttpException) {
Result.Error(e) // HTTP 错误
}
}
// Flow 中处理错误
fun safeUserFlow(id: String): Flow<Result<User>> = flow {
emit(Result.Loading)
try {
val user = apiService.getUser(id)
emit(Result.Success(user))
} catch (e: Exception) {
emit(Result.Error(e))
}
}
// SupervisorJob:子协程失败不影响兄弟协程(类比 iOS 的 TaskGroup 错误处理)
val supervisorScope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
supervisorScope.launch {
val job1 = launch {
// 这个协程失败...
throw Exception("Job 1 failed")
}
val job2 = launch {
// ...不会影响这个协程继续执行
delay(1000)
println("Job 2 completed")
}
}
// coroutineExceptionHandler:捕获未处理的异常
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
println("Unhandled exception: ${throwable.message}")
}
viewModelScope.launch(exceptionHandler) {
// 未捕获的异常会传到 exceptionHandler
throw Exception("Uncaught!")
}
Swift async/await ≈ Kotlin suspend + coroutines(非常相似)
DispatchQueue.main = Dispatchers.Main(主线程)
DispatchQueue.global(qos: .background) = Dispatchers.IO(IO 线程)
Task { } = viewModelScope.launch { }(启动协程)
async let = async { }.await()(并发任务)
AsyncStream = Flow(异步数据流)
PassthroughSubject = SharedFlow(事件流)
CurrentValueSubject = StateFlow(状态流)
Room 数据库(对比 Core Data)
Room 是 Android 官方的数据库 ORM 库,对应 iOS 的 Core Data。相比 Core Data,Room 更加简洁直观,使用注解驱动,与 Kotlin 协程和 Flow 无缝集成。
| 概念 | Core Data | Room |
|---|---|---|
| 数据库 | NSPersistentContainer | @Database + RoomDatabase |
| 数据模型 | NSManagedObject + .xcdatamodeld | @Entity data class |
| 数据访问 | NSFetchRequest + NSPredicate | @Dao + @Query (SQL) |
| 查询语言 | NSPredicate(自定义语法) | 标准 SQL |
| 关系 | NSRelationship(可视化配置) | 外键 + @Relation 注解 |
| 迁移 | NSMigratePersistentStoresAutomatically | Migration 类(手动 SQL) |
| 响应式 | NSFetchedResultsController | Flow<List<T>> |
| 事务 | context.save() | @Transaction 注解 |
12.1 Room 三大组件
12.1.1 @Entity — 数据表模型
// @Entity 对应 Core Data 的 NSManagedObject + .xcdatamodeld 配置
@Entity(
tableName = "articles",
indices = [
Index(value = ["url"], unique = true), // URL 唯一索引
Index(value = ["category_id"]) // category_id 普通索引
]
)
data class ArticleEntity(
@PrimaryKey(autoGenerate = true) // 对应 Core Data 的 objectID(自增主键)
val id: Int = 0,
@ColumnInfo(name = "title") // 列名(默认与字段名相同)
val title: String,
@ColumnInfo(name = "summary")
val summary: String,
@ColumnInfo(name = "url")
val url: String,
@ColumnInfo(name = "image_url")
val imageUrl: String?, // 可空字段
@ColumnInfo(name = "author")
val author: String,
@ColumnInfo(name = "published_at")
val publishedAt: Long, // 时间戳(Long 比 Date 更适合 Room)
@ColumnInfo(name = "category_id")
val categoryId: Int,
@ColumnInfo(name = "is_bookmarked", defaultValue = "0")
val isBookmarked: Boolean = false,
@ColumnInfo(name = "read_count", defaultValue = "0")
val readCount: Int = 0
)
// 嵌套对象(@Embedded,对应 Core Data 的 transformable 属性)
data class Address(
val street: String,
val city: String,
val country: String
)
@Entity(tableName = "users")
data class UserEntity(
@PrimaryKey val id: Int,
val name: String,
@Embedded val address: Address? // address.street, address.city 成为列
)
// @TypeConverter:存储复杂类型(对应 Core Data 的 Transformable)
class Converters {
@TypeConverter
fun fromTimestamp(value: Long?): Date? = value?.let { Date(it) }
@TypeConverter
fun dateToTimestamp(date: Date?): Long? = date?.time
@TypeConverter
fun fromStringList(value: String): List<String> =
Gson().fromJson(value, object : TypeToken<List<String>>() {}.type)
@TypeConverter
fun toStringList(list: List<String>): String = Gson().toJson(list)
}
12.1.2 @Dao — 数据访问对象
// @Dao 对应 Core Data 的 NSFetchRequest + NSManagedObjectContext
@Dao
interface ArticleDao {
// @Query:原生 SQL 查询(比 Core Data NSPredicate 更直观)
@Query("SELECT * FROM articles ORDER BY published_at DESC")
fun getAllArticles(): Flow<List<ArticleEntity>> // Flow 自动感知数据库变化
@Query("SELECT * FROM articles WHERE id = :id")
suspend fun getArticleById(id: Int): ArticleEntity?
// 条件查询(对应 Core Data NSPredicate)
@Query("SELECT * FROM articles WHERE category_id = :categoryId ORDER BY published_at DESC")
fun getArticlesByCategory(categoryId: Int): Flow<List<ArticleEntity>>
// 搜索(LIKE 查询)
@Query("""
SELECT * FROM articles
WHERE title LIKE '%' || :query || '%'
OR summary LIKE '%' || :query || '%'
ORDER BY published_at DESC
""")
fun searchArticles(query: String): Flow<List<ArticleEntity>>
// 书签查询
@Query("SELECT * FROM articles WHERE is_bookmarked = 1 ORDER BY published_at DESC")
fun getBookmarkedArticles(): Flow<List<ArticleEntity>>
// 聚合查询
@Query("SELECT COUNT(*) FROM articles WHERE category_id = :categoryId")
suspend fun getArticleCountByCategory(categoryId: Int): Int
// 多表关联查询(JOIN)
@Query("""
SELECT a.*, c.name as category_name
FROM articles a
INNER JOIN categories c ON a.category_id = c.id
WHERE a.id = :articleId
""")
suspend fun getArticleWithCategory(articleId: Int): ArticleWithCategory?
// @Insert(对应 Core Data context.insert + save)
@Insert(onConflict = OnConflictStrategy.REPLACE) // 冲突时替换(对应 upsert)
suspend fun insertArticle(article: ArticleEntity): Long // 返回插入的行 ID
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertArticles(articles: List<ArticleEntity>)
// @Update
@Update
suspend fun updateArticle(article: ArticleEntity)
// 局部更新(只更新书签状态,比 @Update 更高效)
@Query("UPDATE articles SET is_bookmarked = :isBookmarked WHERE id = :id")
suspend fun updateBookmarkStatus(id: Int, isBookmarked: Boolean)
// @Delete
@Delete
suspend fun deleteArticle(article: ArticleEntity)
@Query("DELETE FROM articles WHERE id = :id")
suspend fun deleteArticleById(id: Int)
@Query("DELETE FROM articles WHERE published_at < :timestamp")
suspend fun deleteOldArticles(timestamp: Long): Int // 返回删除的行数
@Query("DELETE FROM articles")
suspend fun deleteAllArticles()
// @Transaction:多个操作的原子事务(对应 Core Data batch save)
@Transaction
suspend fun refreshArticles(newArticles: List<ArticleEntity>) {
deleteAllArticles()
insertArticles(newArticles)
}
}
12.1.3 @Database — 数据库定义
// @Database 对应 Core Data 的 NSPersistentContainer
@Database(
entities = [
ArticleEntity::class,
UserEntity::class,
CategoryEntity::class
],
version = 3, // 数据库版本号(每次 schema 变更递增)
exportSchema = true // 导出 schema JSON 文件(用于版本控制)
)
@TypeConverters(Converters::class) // 注册 TypeConverter
abstract class AppDatabase : RoomDatabase() {
// 抽象 DAO 访问器(Room 自动实现)
abstract fun articleDao(): ArticleDao
abstract fun userDao(): UserDao
abstract fun categoryDao(): CategoryDao
companion object {
@Volatile // 线程可见性
private var INSTANCE: AppDatabase? = null
// 单例获取(对应 Core Data 的 NSPersistentContainer.shared)
fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"app_database" // 数据库文件名
)
.addMigrations(MIGRATION_1_2, MIGRATION_2_3) // 数据库迁移
.fallbackToDestructiveMigration() // 迁移失败时清空重建(谨慎使用!)
.build()
INSTANCE = instance
instance
}
}
// 数据库迁移(从版本1迁移到版本2)
// 对应 Core Data 的 Lightweight Migration 或自定义 NSMappingModel
val MIGRATION_1_2 = object : Migration(1, 2) {
override fun migrate(database: SupportSQLiteDatabase) {
// 执行 SQL 变更(添加新列)
database.execSQL(
"ALTER TABLE articles ADD COLUMN read_count INTEGER NOT NULL DEFAULT 0"
)
}
}
// 迁移:版本2到版本3(添加新表)
val MIGRATION_2_3 = object : Migration(2, 3) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("""
CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
name TEXT NOT NULL,
color TEXT NOT NULL DEFAULT '#000000'
)
""".trimIndent())
}
}
}
}
// 通过 Hilt 提供 Database 实例(推荐)
@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {
@Provides
@Singleton
fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
return AppDatabase.getDatabase(context)
}
@Provides
fun provideArticleDao(db: AppDatabase): ArticleDao = db.articleDao()
@Provides
fun provideUserDao(db: AppDatabase): UserDao = db.userDao()
}
12.2 Room + Repository 完整示例
// Repository 实现(离线优先策略)
class ArticleRepositoryImpl @Inject constructor(
private val articleDao: ArticleDao,
private val apiService: ArticleApiService,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : ArticleRepository {
// 返回本地数据库的 Flow(数据库变化时自动更新)
// 对应 Core Data NSFetchedResultsController
override fun getArticles(): Flow<List<Article>> {
return articleDao.getAllArticles()
.map { entities -> entities.map { it.toDomain() } }
.flowOn(ioDispatcher)
}
// 刷新:从网络获取,写入本地数据库
override suspend fun refreshArticles() {
withContext(ioDispatcher) {
val remoteArticles = apiService.getArticles()
// 事务操作:清空旧数据,插入新数据
articleDao.refreshArticles(remoteArticles.map { it.toEntity() })
}
}
// 切换书签
override suspend fun toggleBookmark(articleId: Int) {
withContext(ioDispatcher) {
val article = articleDao.getArticleById(articleId) ?: return@withContext
articleDao.updateBookmarkStatus(articleId, !article.isBookmarked)
}
}
// 搜索
override fun search(query: String): Flow<List<Article>> {
return articleDao.searchArticles(query)
.map { entities -> entities.map { it.toDomain() } }
.flowOn(ioDispatcher)
}
}
Room 优点:标准 SQL 查询更直观,编译时检查 SQL 语法,与 Flow 无缝集成,学习曲线低
Core Data 优点:可视化 .xcdatamodeld 编辑器,支持 Fault(懒加载),批量操作更成熟
Room 最佳实践:在子线程执行(Room 默认禁止主线程查询),配合 Hilt 注入,使用 Flow 而不是 suspend + LiveData
网络请求 — Retrofit + OkHttp
Retrofit 是 Android 最流行的 HTTP 客户端库,对应 iOS 的 Alamofire。OkHttp 是底层 HTTP 客户端(类比 URLSession),Retrofit 建立在 OkHttp 之上,提供更高级的 API 定义方式。
| 功能 | iOS (URLSession/Alamofire) | Android (OkHttp/Retrofit) |
|---|---|---|
| 底层 HTTP 客户端 | URLSession | OkHttp |
| 高级 HTTP 库 | Alamofire | Retrofit |
| API 定义 | Router enum / Endpoint struct | @GET/@POST 注解 interface |
| JSON 解析 | Codable (JSONDecoder) | Gson / Moshi / Kotlinx.serialization |
| 拦截器 | RequestAdapter + RequestRetrier | OkHttp Interceptor |
| 认证 | URLCredential / Interceptor | Authenticator / Header Interceptor |
| 日志 | EventMonitor | HttpLoggingInterceptor |
| 文件上传 | uploadTask / multipart | @Multipart + @Part |
| 取消 | task.cancel() | Call.cancel() / 协程取消 |
13.1 数据模型(Gson 解析)
// Swift Codable(自动解析 JSON)
struct Article: Codable {
let id: Int
let title: String
let summary: String
let imageUrl: String?
let publishedAt: Date
enum CodingKeys: String, CodingKey {
case id, title, summary
case imageUrl = "image_url"
case publishedAt = "published_at"
}
}
// 列表响应
struct ArticleListResponse: Codable {
let data: [Article]
let total: Int
let page: Int
}
// 解码
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let articles = try decoder.decode(ArticleListResponse.self, from: data)
// Kotlin + Gson(对应 Swift Codable)
data class ArticleDto(
@SerializedName("id") val id: Int,
@SerializedName("title") val title: String,
@SerializedName("summary") val summary: String,
@SerializedName("image_url") val imageUrl: String?,
@SerializedName("published_at") val publishedAt: String
)
// 列表响应包装(对应 iOS 的 Codable struct)
data class ArticleListResponse(
@SerializedName("data") val data: List<ArticleDto>,
@SerializedName("total") val total: Int,
@SerializedName("page") val page: Int
)
// 通用 API 响应包装
data class ApiResponse<T>(
@SerializedName("code") val code: Int,
@SerializedName("message") val message: String,
@SerializedName("data") val data: T?
) {
val isSuccess: Boolean get() = code == 200
}
// Kotlin Serialization(对应 Codable,需要添加 kotlinx-serialization-json 依赖)
@Serializable
data class Article(
val id: Int,
val title: String,
@SerialName("image_url") val imageUrl: String? = null
)
13.2 Retrofit API 定义
// Retrofit 接口定义(对应 iOS Alamofire 的 Router 或 TargetType)
// Retrofit 会自动生成实现类
interface ArticleApiService {
// @GET:GET 请求(对应 iOS .get method)
@GET("articles")
suspend fun getArticles(
@Query("page") page: Int = 1, // 查询参数 ?page=1
@Query("limit") limit: Int = 20, // ?limit=20
@Query("category") category: String? = null
): Response<ArticleListResponse> // 包含 HTTP 状态码的响应
// @GET with @Path:路径参数(对应 iOS URL path 插值)
@GET("articles/{id}")
suspend fun getArticleById(@Path("id") id: Int): ArticleDto
// @POST with @Body:提交 JSON 请求体(对应 iOS httpBody)
@POST("articles")
suspend fun createArticle(@Body request: CreateArticleRequest): ArticleDto
// @PUT:更新
@PUT("articles/{id}")
suspend fun updateArticle(
@Path("id") id: Int,
@Body request: UpdateArticleRequest
): ArticleDto
// @DELETE
@DELETE("articles/{id}")
suspend fun deleteArticle(@Path("id") id: Int): Unit
// @PATCH:部分更新
@PATCH("articles/{id}/bookmark")
suspend fun toggleBookmark(
@Path("id") id: Int,
@Body request: BookmarkRequest
): ArticleDto
// @Header:自定义请求头
@GET("articles/premium")
suspend fun getPremiumArticles(
@Header("X-Subscription-Token") token: String
): List<ArticleDto>
// @Headers:多个固定请求头
@Headers(
"Content-Type: application/json",
"Accept-Language: zh-CN"
)
@GET("articles/featured")
suspend fun getFeaturedArticles(): List<ArticleDto>
// @Multipart:文件上传(对应 iOS multipart form)
@Multipart
@POST("articles/upload-image")
suspend fun uploadImage(
@Part image: MultipartBody.Part, // 文件
@Part("article_id") articleId: RequestBody // 附加字段
): ImageUploadResponse
// @FormUrlEncoded:表单数据(对应 iOS application/x-www-form-urlencoded)
@FormUrlEncoded
@POST("auth/login")
suspend fun login(
@Field("email") email: String,
@Field("password") password: String
): LoginResponse
// @QueryMap:动态查询参数
@GET("articles/search")
suspend fun searchArticles(
@QueryMap filters: Map<String, String>
): List<ArticleDto>
// 返回 Call 而不是 suspend(用于取消等场景)
@GET("articles")
fun getArticlesCall(): Call<ArticleListResponse>
}
13.3 OkHttp 拦截器
OkHttp 的拦截器(Interceptor)是其最强大的特性之一,对应 iOS Alamofire 的 RequestAdapter。它基于责任链模式:每个拦截器处理请求后,决定是否将请求传递给链中的下一个拦截器。
拦截器有两种类型:
- Application Interceptor(应用拦截器):在 OkHttp 核心逻辑之前/之后执行,看到的是原始请求,每次调用只触发一次,适合做认证头注入、日志记录
- Network Interceptor(网络拦截器):在网络层执行,能看到重定向和重试请求,适合监控实际网络流量
// 认证拦截器(对应 iOS Alamofire RequestAdapter)
class AuthInterceptor @Inject constructor(
private val tokenProvider: TokenProvider
) : Interceptor {
override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
val originalRequest = chain.request()
// 为每个请求添加 Authorization 头
val token = tokenProvider.getAccessToken()
val authenticatedRequest = originalRequest.newBuilder()
.header("Authorization", "Bearer $token")
.header("Accept", "application/json")
.header("App-Version", BuildConfig.VERSION_NAME)
.build()
return chain.proceed(authenticatedRequest)
}
}
// Token 刷新拦截器(对应 iOS Alamofire RequestRetrier)
class TokenRefreshInterceptor @Inject constructor(
private val tokenProvider: TokenProvider,
private val authRepository: AuthRepository
) : Authenticator { // Authenticator 专门处理 401 响应
override fun authenticate(route: Route?, response: okhttp3.Response): Request? {
// 避免无限重试
if (responseCount(response) >= 3) return null
val newToken = runBlocking {
try {
authRepository.refreshToken()
} catch (e: Exception) {
null
}
} ?: return null // 刷新失败,返回 null 表示不重试
// 用新 Token 重试请求
return response.request.newBuilder()
.header("Authorization", "Bearer $newToken")
.build()
}
private fun responseCount(response: okhttp3.Response): Int {
var count = 1
var prior = response.priorResponse
while (prior != null) {
count++
prior = prior.priorResponse
}
return count
}
}
// 日志拦截器(对应 iOS NetworkActivityLogger)
// 已内置于 OkHttp,直接使用即可
val loggingInterceptor = HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY // 打印请求体和响应体
} else {
HttpLoggingInterceptor.Level.NONE // 生产环境不打印
}
}
// 缓存拦截器(对应 iOS URLCache)
class CacheInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
val request = chain.request()
val response = chain.proceed(request)
// 设置缓存控制头
return response.newBuilder()
.header("Cache-Control", "public, max-age=60") // 缓存 60 秒
.build()
}
}
13.4 完整网络层配置
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(
authInterceptor: AuthInterceptor,
tokenRefreshInterceptor: TokenRefreshInterceptor
): OkHttpClient {
return OkHttpClient.Builder()
// 拦截器(按顺序执行)
.addInterceptor(authInterceptor) // 添加认证头
.addInterceptor(HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY
else HttpLoggingInterceptor.Level.NONE
})
.authenticator(tokenRefreshInterceptor) // 处理 401 Token 刷新
// 超时配置(对应 iOS URLSession timeoutIntervalForRequest)
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
// 缓存(对应 iOS URLCache)
.cache(Cache(File("cache"), 10 * 1024 * 1024)) // 10MB 磁盘缓存
// TLS 配置(对应 iOS App Transport Security)
// .sslSocketFactory(sslSocketFactory, trustManager)
.build()
}
@Provides
@Singleton
fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(
GsonBuilder()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
.create()
))
// 也可以使用 Moshi 或 Kotlinx.serialization
// .addConverterFactory(MoshiConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideArticleApiService(retrofit: Retrofit): ArticleApiService {
return retrofit.create(ArticleApiService::class.java)
}
}
// Repository 使用
class ArticleRemoteDataSource @Inject constructor(
private val apiService: ArticleApiService
) {
// 封装 API 调用,处理 HTTP 错误
suspend fun fetchArticles(page: Int): List<ArticleDto> {
val response = apiService.getArticles(page = page)
if (response.isSuccessful) {
return response.body()?.data ?: emptyList()
} else {
throw HttpException(response) // 将 HTTP 错误转为异常
}
}
// 通用的 API 调用扩展函数
private suspend fun <T> safeApiCall(
apiCall: suspend () -> Response<T>
): T {
try {
val response = apiCall()
if (response.isSuccessful) {
return response.body() ?: throw Exception("Empty response body")
}
val errorBody = response.errorBody()?.string()
throw HttpException(response)
} catch (e: IOException) {
throw NetworkException("No internet connection", e)
} catch (e: HttpException) {
throw when (e.code()) {
401 -> UnauthorizedException()
403 -> ForbiddenException()
404 -> NotFoundException()
500 -> ServerException()
else -> e
}
}
}
}
Jetpack Compose(对比 SwiftUI)
Jetpack Compose 是 Android 的声明式 UI 框架,与 SwiftUI 在设计理念上高度一致。如果你已经熟悉 SwiftUI,学习 Compose 会非常自然。两者都基于"状态驱动 UI"的范式。
| 概念 | SwiftUI | Jetpack Compose |
|---|---|---|
| 基础单元 | struct View | @Composable fun |
| 本地状态 | @State var count = 0 | var count by remember { mutableStateOf(0) } |
| 双向绑定 | @Binding var text: String | var text by remember { mutableStateOf("") } + onValueChange |
| ViewModel 状态 | @StateObject / @ObservedObject | viewModel.uiState.collectAsState() |
| 竖向布局 | VStack | Column |
| 横向布局 | HStack | Row |
| 层叠布局 | ZStack | Box |
| 懒加载列表 | List / LazyVStack | LazyColumn / LazyRow |
| 修饰符 | .padding() .background() | Modifier.padding().background() |
| 主题 | @Environment(\.colorScheme) | MaterialTheme.colorScheme |
| 导航 | NavigationStack + .navigationDestination | NavController + Compose Navigation |
| 动画 | withAnimation { } / .animation() | animate*AsState / AnimatedVisibility |
| UI 刷新机制 | View 重新计算 | Recomposition(重组) |
14.1 基础 Composable
// SwiftUI View
struct ContentView: View {
@State private var count = 0
@State private var name = ""
var body: some View {
VStack(spacing: 16) {
Text("Count: \(count)")
.font(.title)
.foregroundColor(.blue)
.padding()
.background(Color.gray.opacity(0.2))
.cornerRadius(8)
TextField("Enter name", text: $name)
.textFieldStyle(.roundedBorder)
.padding(.horizontal)
Button("Increment") {
count += 1
}
.buttonStyle(.borderedProminent)
Spacer()
}
.padding()
}
}
// 自定义 View
struct ProfileCard: View {
let user: User
let onTap: () -> Void
var body: some View {
HStack {
AsyncImage(url: URL(string: user.avatarUrl ?? ""))
.frame(width: 50, height: 50)
.clipShape(Circle())
VStack(alignment: .leading) {
Text(user.name).font(.headline)
Text(user.email).font(.subheadline)
.foregroundColor(.secondary)
}
Spacer()
}
.padding()
.background(Color(.secondarySystemBackground))
.cornerRadius(12)
.onTapGesture(perform: onTap)
}
}
// Jetpack Compose(对应 SwiftUI View)
@Composable
fun ContentScreen() {
// remember 保存状态(对应 @State)
var count by remember { mutableStateOf(0) }
var name by remember { mutableStateOf("") }
// Column 对应 VStack
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Text(对应 SwiftUI Text)
Text(
text = "Count: $count",
style = MaterialTheme.typography.titleLarge, // 对应 .font(.title)
color = MaterialTheme.colorScheme.primary,
modifier = Modifier
.padding(16.dp)
.background(
MaterialTheme.colorScheme.surfaceVariant,
RoundedCornerShape(8.dp)
)
)
// TextField(对应 SwiftUI TextField)
OutlinedTextField(
value = name,
onValueChange = { name = it }, // 对应 $name(双向绑定)
label = { Text("Enter name") },
modifier = Modifier.fillMaxWidth()
)
// Button(对应 SwiftUI Button)
Button(
onClick = { count++ },
modifier = Modifier.fillMaxWidth()
) {
Text("Increment")
}
Spacer(modifier = Modifier.weight(1f)) // 对应 Spacer()
}
}
// 自定义 Composable(对应 SwiftUI 自定义 View)
@Composable
fun ProfileCard(
user: User,
onTap: () -> Unit,
modifier: Modifier = Modifier // 约定:接受外部 Modifier
) {
Row(
modifier = modifier
.fillMaxWidth()
.clickable(onClick = onTap) // 对应 .onTapGesture
.background(
MaterialTheme.colorScheme.surfaceVariant,
RoundedCornerShape(12.dp)
)
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
// 圆形头像(使用 Coil 异步加载)
AsyncImage(
model = user.avatarUrl,
contentDescription = user.name,
modifier = Modifier
.size(50.dp)
.clip(CircleShape), // 对应 .clipShape(Circle())
contentScale = ContentScale.Crop
)
// Column 对应 VStack(alignment: .leading)
Column(
modifier = Modifier
.weight(1f)
.padding(start = 12.dp)
) {
Text(
text = user.name,
style = MaterialTheme.typography.titleMedium // 对应 .font(.headline)
)
Text(
text = user.email,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant // 对应 .foregroundColor(.secondary)
)
}
}
}
14.2 LazyColumn(对比 SwiftUI List)
// SwiftUI List(懒加载)
struct ArticleListView: View {
@StateObject var viewModel = ArticleViewModel()
var body: some View {
List {
ForEach(viewModel.articles) { article in
ArticleRow(article: article)
.onTapGesture {
viewModel.selectArticle(article)
}
}
.onDelete { offsets in
viewModel.deleteArticles(at: offsets)
}
}
.refreshable {
await viewModel.refresh()
}
.listStyle(.plain)
}
}
struct ArticleRow: View {
let article: Article
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(article.title).font(.headline)
Text(article.summary).font(.subheadline)
.lineLimit(2)
.foregroundColor(.secondary)
}
.padding(.vertical, 4)
}
}
// Jetpack Compose LazyColumn(对应 SwiftUI List)
@Composable
fun ArticleListScreen(
viewModel: ArticleViewModel = hiltViewModel() // Hilt 注入 ViewModel
) {
// 收集 StateFlow 状态(对应 @StateObject)
val uiState by viewModel.uiState.collectAsState()
when (val state = uiState) {
is ArticleUiState.Loading -> {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator() // 对应 SwiftUI ProgressView
}
}
is ArticleUiState.Success -> {
// 下拉刷新(对应 .refreshable)
val pullRefreshState = rememberPullRefreshState(
refreshing = state.isRefreshing,
onRefresh = { viewModel.refresh() }
)
Box(modifier = Modifier.pullRefresh(pullRefreshState)) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// 固定 Header
item {
Text(
"Latest Articles",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(bottom = 8.dp)
)
}
// 列表项(对应 ForEach)
items(
items = state.articles,
key = { article -> article.id } // 对应 .id(\.id),提升性能
) { article ->
ArticleRow(
article = article,
onClick = { viewModel.selectArticle(article) }
)
}
// 加载更多
if (state.isLoadingMore) {
item {
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
CircularProgressIndicator(modifier = Modifier.padding(16.dp))
}
}
}
}
PullRefreshIndicator(
state.isRefreshing,
pullRefreshState,
modifier = Modifier.align(Alignment.TopCenter)
)
}
}
is ArticleUiState.Error -> {
// 错误状态
}
}
}
// Composable Item
@Composable
fun ArticleRow(
article: Article,
onClick: () -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) {
Column(modifier = Modifier.padding(12.dp)) {
Text(
text = article.title,
style = MaterialTheme.typography.titleMedium,
maxLines = 2,
overflow = TextOverflow.Ellipsis // 对应 .lineLimit(2)
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = article.summary,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}
}
14.3 状态管理与 ViewModel
// 状态提升(State Hoisting)— 对应 SwiftUI 的 @Binding
// 将状态和事件提升到父 Composable,子 Composable 只接收参数
@Composable
fun SearchBar(
query: String, // 对应 @Binding var query
onQueryChange: (String) -> Unit, // 事件回调(对应 action 参数)
onSearch: () -> Unit,
modifier: Modifier = Modifier
) {
OutlinedTextField(
value = query,
onValueChange = onQueryChange,
placeholder = { Text("Search articles...") },
leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) },
trailingIcon = {
if (query.isNotEmpty()) {
IconButton(onClick = { onQueryChange("") }) {
Icon(Icons.Default.Clear, contentDescription = "Clear")
}
}
},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(onSearch = { onSearch() }),
singleLine = true,
modifier = modifier.fillMaxWidth()
)
}
// 父 Composable 持有状态
@Composable
fun SearchScreen(viewModel: SearchViewModel = hiltViewModel()) {
var query by remember { mutableStateOf("") }
Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
// 将状态和回调传给 SearchBar
SearchBar(
query = query,
onQueryChange = { newQuery ->
query = newQuery
viewModel.search(newQuery) // 触发 ViewModel 搜索
},
onSearch = { viewModel.search(query) }
)
val results by viewModel.searchResults.collectAsState()
LazyColumn {
items(results) { article ->
ArticleRow(article = article, onClick = { /* 导航 */ })
}
}
}
}
// remember 和 rememberSaveable(对应 @State + Scene Storage)
@Composable
fun CounterScreen() {
// remember:跨重组保存状态,配置变更(旋转屏幕)时丢失
var count by remember { mutableStateOf(0) }
// rememberSaveable:配置变更时也能保存(对应 @SceneStorage 或 @AppStorage)
var savedCount by rememberSaveable { mutableStateOf(0) }
Column {
Text("Count: $count")
Text("Saved Count: $savedCount")
Button(onClick = { count++; savedCount++ }) {
Text("Increment")
}
}
}
14.4 动画
// animate*AsState:属性动画(对应 SwiftUI .animation())
@Composable
fun AnimatedCounter(count: Int) {
// 数值变化时自动动画(对应 SwiftUI withAnimation)
val animatedCount by animateIntAsState(
targetValue = count,
animationSpec = tween(durationMillis = 300) // 对应 .animation(.easeInOut(duration:))
)
Text(text = "$animatedCount", style = MaterialTheme.typography.displayLarge)
}
// AnimatedVisibility:显示/隐藏动画(对应 SwiftUI .transition())
@Composable
fun AnimatedContent(show: Boolean) {
AnimatedVisibility(
visible = show,
enter = fadeIn() + slideInVertically(), // 进入动画
exit = fadeOut() + slideOutVertically() // 退出动画
) {
Text("Hello! I'm animated")
}
}
// Crossfade:内容切换动画(对应 SwiftUI .transition with id)
@Composable
fun TabContent(selectedTab: Int) {
Crossfade(targetState = selectedTab) { tab ->
when (tab) {
0 -> HomeContent()
1 -> SearchContent()
2 -> ProfileContent()
}
}
}
// 自定义动画
@Composable
fun PulsingButton(onClick: () -> Unit) {
val infiniteTransition = rememberInfiniteTransition()
val scale by infiniteTransition.animateFloat(
initialValue = 1f,
targetValue = 1.1f,
animationSpec = infiniteRepeatable(
animation = tween(500),
repeatMode = RepeatMode.Reverse
)
)
Button(
onClick = onClick,
modifier = Modifier.scale(scale)
) {
Text("Pulse!")
}
}
14.5 与 View 系统互操作
// 在 Fragment/Activity 中嵌入 Compose(对应 SwiftUI UIViewControllerRepresentable 反向)
class HomeFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// ComposeView 作为传统 View 和 Compose 的桥梁
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
)
setContent {
// 设置 Material 主题
MaterialTheme(
colorScheme = darkColorScheme()
) {
// Compose UI
HomeScreen()
}
}
}
}
}
// 在 Compose 中嵌入传统 View(AndroidView,对应 SwiftUI UIViewRepresentable)
@Composable
fun MapViewComposable(location: LatLng) {
AndroidView(
factory = { context ->
// 创建传统 View(如 Google MapView)
MapView(context).apply {
onCreate(null)
getMapAsync { map ->
map.addMarker(MarkerOptions().position(location))
map.moveCamera(CameraUpdateFactory.newLatLng(location))
}
}
},
update = { mapView ->
// 当 Compose 状态变化时更新 View
mapView.getMapAsync { map ->
map.moveCamera(CameraUpdateFactory.newLatLng(location))
}
}
)
}
最相似:布局模型(Column/Row/Box vs VStack/HStack/ZStack),状态管理(remember/mutableStateOf vs @State),Modifier 链式调用
主要差异:
• Compose 用 @Composable 函数,SwiftUI 用 struct View
• Compose 用 LazyColumn/LazyRow,SwiftUI 用 List/LazyVGrid
• Compose 的 Modifier 是链式的,SwiftUI 的 modifier 也是
• Compose 用 hiltViewModel() 注入,SwiftUI 用 @StateObject
• Compose 的 Recomposition 更精细,需注意不要在 Composable 中进行重操作
依赖注入 — Hilt
Hilt 是 Google 官方推荐的 Android 依赖注入框架,基于 Dagger 构建。对应 iOS 的 Swinject 或手动 DI。Hilt 大幅减少了 Dagger 的样板代码,与 Jetpack 组件(ViewModel、WorkManager 等)无缝集成。
没有 DI 时,每个类都要自己创建依赖,导致:代码耦合、难以测试(无法 mock)、依赖链管理混乱。
Hilt 自动管理对象的创建和生命周期,让每个类只声明"我需要什么",而不关心"如何获得"。
15.1 Hilt 核心注解
| Hilt 注解 | 作用 | iOS 类比 |
|---|---|---|
| @HiltAndroidApp | 标记 Application,触发 Hilt 代码生成 | 无(Swinject Container 初始化) |
| @AndroidEntryPoint | 标记 Activity/Fragment,启用 Hilt 注入 | 无 |
| @HiltViewModel | 标记 ViewModel,由 Hilt 管理依赖 | 无 |
| @Inject | 标记需要注入的构造函数或字段 | Swinject @Injected |
| @Module | 定义如何提供依赖(当无法用 @Inject 时) | Swinject Container.register |
| @InstallIn | 指定 Module 的组件范围 | 无 |
| @Provides | 在 Module 中定义依赖提供方法 | 无 |
| @Binds | 绑定接口到实现类 | 无 |
| @Singleton | 整个应用只有一个实例 | container.register(.singleton) |
| @ViewModelScoped | 与 ViewModel 同生命周期 | 无 |
15.2 完整 Hilt 示例
// Step 1: Application 类(必须)
@HiltAndroidApp // 触发 Hilt 代码生成,是所有注入的入口
class MyApplication : Application()
// Step 2: Activity/Fragment 标记
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
// Hilt 自动注入(字段注入,对应 @Injected)
@Inject lateinit var analyticsService: AnalyticsService
// ViewModel 通过 viewModels() 委托自动获得
private val viewModel: MainViewModel by viewModels()
}
// Step 3: ViewModel 标记
@HiltViewModel
class MainViewModel @Inject constructor(
// 构造函数注入(推荐方式,对应手动 DI 的 init 参数)
private val userRepository: UserRepository,
private val articleRepository: ArticleRepository
) : ViewModel()
// Step 4: Repository 构造函数注入
class UserRepositoryImpl @Inject constructor(
private val userDao: UserDao,
private val apiService: UserApiService,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher
) : UserRepository // 实现接口
// Step 5: Module - 配置无法直接 @Inject 的依赖(第三方库、接口等)
@Module
@InstallIn(SingletonComponent::class) // 应用级别单例(整个 App 只有一个)
object AppModule {
// @Provides:提供 Retrofit(第三方库,无法修改源码添加 @Inject)
@Provides
@Singleton
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
@Provides
@Singleton
fun provideApiService(retrofit: Retrofit): UserApiService {
return retrofit.create(UserApiService::class.java)
}
// 提供 Dispatcher(让 Dispatcher 可以被 mock,方便测试)
@Provides
@IoDispatcher
fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
@Provides
@MainDispatcher
fun provideMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
}
// @Binds:绑定接口到实现类(比 @Provides 更高效,不生成实例化代码)
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
// 将 UserRepositoryImpl 绑定到 UserRepository 接口
// 当其他地方注入 UserRepository 时,实际提供 UserRepositoryImpl
@Binds
@Singleton
abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository
@Binds
@Singleton
abstract fun bindArticleRepository(impl: ArticleRepositoryImpl): ArticleRepository
}
// 自定义 Qualifier(区分同类型但用途不同的依赖)
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class IoDispatcher
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class MainDispatcher
// Activity 作用域(每个 Activity 创建新实例)
@Module
@InstallIn(ActivityComponent::class) // Activity 级别
object ActivityModule {
@Provides
@ActivityScoped
fun provideNavigationHelper(activity: Activity): NavigationHelper {
return NavigationHelper(activity)
}
}
// 测试时的 Module 替换(非常方便!对比 iOS 需要手动 mock)
@HiltAndroidTest
class UserViewModelTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
// 替换真实 Module
@BindValue
@JvmField
val userRepository: UserRepository = FakeUserRepository()
@Test
fun testLoadUsers() { /* 使用 FakeUserRepository */ }
}
数据存储全解
Android 提供多种数据存储方案,适用于不同场景。理解何时用哪种方案是 Android 开发的重要技能。
| 存储方式 | 适用场景 | iOS 对应 | 大小限制 |
|---|---|---|---|
| SharedPreferences | 简单键值对(用户设置) | UserDefaults | 小量数据 |
| DataStore (Preferences) | 异步键值对(推荐替代 SharedPreferences) | UserDefaults (异步) | 小量数据 |
| DataStore (Proto) | 类型安全的结构化数据 | Codable + UserDefaults | 中量数据 |
| Room Database | 结构化关系型数据 | Core Data / SQLite | 无限制 |
| Internal Storage | 私有文件(下载文件、缓存) | Documents / Cache 目录 | 设备存储 |
| External Storage | 公共文件(图片、文档) | Photos / Files | 设备存储 |
| EncryptedSharedPreferences | 加密的敏感数据 | Keychain | 小量数据 |
16.1 SharedPreferences(对比 UserDefaults)
// UserDefaults 读写
let defaults = UserDefaults.standard
defaults.set("Alice", forKey: "username")
defaults.set(true, forKey: "isLoggedIn")
defaults.set(42, forKey: "score")
let username = defaults.string(forKey: "username") ?? ""
let isLoggedIn = defaults.bool(forKey: "isLoggedIn")
// @AppStorage(SwiftUI)
@AppStorage("username") var username = ""
@AppStorage("isDarkMode") var isDarkMode = false
// 封装
@propertyWrapper
struct UserDefault<T> {
let key: String
let defaultValue: T
var wrappedValue: T {
get { UserDefaults.standard.object(forKey: key) as? T ?? defaultValue }
set { UserDefaults.standard.set(newValue, forKey: key) }
}
}
// SharedPreferences(同步,主线程读写,不推荐在主线程写入大量数据)
val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
// 写入(edit() 必须 commit 或 apply)
prefs.edit()
.putString("username", "Alice")
.putBoolean("is_logged_in", true)
.putInt("score", 42)
.apply() // 异步写入(对应 UserDefaults.synchronize,推荐)
// .commit() // 同步写入,阻塞线程(不推荐)
// 读取
val username = prefs.getString("username", "") ?: ""
val isLoggedIn = prefs.getBoolean("is_logged_in", false)
val score = prefs.getInt("score", 0)
// 删除
prefs.edit().remove("username").apply()
prefs.edit().clear().apply() // 清除所有
// 监听变化(对应 UserDefaults 的 KVO 观察)
prefs.registerOnSharedPreferenceChangeListener { _, key ->
when (key) {
"username" -> updateUI()
}
}
// 封装为类型安全的工具类
class AppPreferences @Inject constructor(
@ApplicationContext context: Context
) {
private val prefs = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
var username: String
get() = prefs.getString(KEY_USERNAME, "") ?: ""
set(value) = prefs.edit().putString(KEY_USERNAME, value).apply()
var isLoggedIn: Boolean
get() = prefs.getBoolean(KEY_IS_LOGGED_IN, false)
set(value) = prefs.edit().putBoolean(KEY_IS_LOGGED_IN, value).apply()
companion object {
private const val KEY_USERNAME = "username"
private const val KEY_IS_LOGGED_IN = "is_logged_in"
}
}
16.2 DataStore(现代替代 SharedPreferences)
SharedPreferences 是 Android 老牌的键值对存储,但它有三个致命缺陷:同步读写可能阻塞主线程、不保证类型安全、不支持事务(多个写操作可能数据不一致)。DataStore 是 Jetpack 提供的现代替代品,用 Kotlin 协程和 Flow 解决了这些问题。
1. 协程安全:所有读写都是异步的,通过 Flow 返回数据,不会阻塞主线程
2. 类型安全:使用强类型 Key(stringPreferencesKey、intPreferencesKey 等),避免拼写错误
3. 一致性保证:基于事务机制,不会出现 SharedPreferences 的并发写入问题
// DataStore(对应 UserDefaults,但是异步且线程安全)
// 1. 创建 DataStore 实例(通常在 top-level 或 Hilt Module 中)
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
// 2. 定义 Keys(类型安全)
object PreferencesKeys {
val USERNAME = stringPreferencesKey("username")
val IS_DARK_MODE = booleanPreferencesKey("is_dark_mode")
val FONT_SIZE = intPreferencesKey("font_size")
val LAST_SYNC = longPreferencesKey("last_sync")
}
// 3. DataStore Repository 封装
class UserPreferencesRepository @Inject constructor(
private val dataStore: DataStore<Preferences>
) {
// 读取(Flow,响应式)
val username: Flow<String> = dataStore.data
.catch { e ->
if (e is IOException) emit(emptyPreferences())
else throw e
}
.map { preferences ->
preferences[PreferencesKeys.USERNAME] ?: ""
}
val isDarkMode: Flow<Boolean> = dataStore.data
.map { it[PreferencesKeys.IS_DARK_MODE] ?: false }
// 读取多个值
data class UserPreferences(val username: String, val isDarkMode: Boolean)
val userPreferences: Flow<UserPreferences> = dataStore.data
.map { preferences ->
UserPreferences(
username = preferences[PreferencesKeys.USERNAME] ?: "",
isDarkMode = preferences[PreferencesKeys.IS_DARK_MODE] ?: false
)
}
// 写入(suspend 函数,必须在协程中调用)
suspend fun setUsername(username: String) {
dataStore.edit { preferences ->
preferences[PreferencesKeys.USERNAME] = username
}
}
suspend fun setDarkMode(enabled: Boolean) {
dataStore.edit { preferences ->
preferences[PreferencesKeys.IS_DARK_MODE] = enabled
}
}
// 清除
suspend fun clearPreferences() {
dataStore.edit { it.clear() }
}
}
// 4. 在 ViewModel 中使用
@HiltViewModel
class SettingsViewModel @Inject constructor(
private val preferencesRepository: UserPreferencesRepository
) : ViewModel() {
val isDarkMode = preferencesRepository.isDarkMode
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = false
)
fun toggleDarkMode() {
viewModelScope.launch {
preferencesRepository.setDarkMode(!isDarkMode.value)
}
}
}
// 5. 加密存储(对应 iOS Keychain)
// EncryptedSharedPreferences(敏感数据)
val masterKey = MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build()
val encryptedPrefs = EncryptedSharedPreferences.create(
context,
"secret_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
// 使用方式与 SharedPreferences 相同,但数据是加密存储的
encryptedPrefs.edit().putString("auth_token", "Bearer xxxxx").apply()
val token = encryptedPrefs.getString("auth_token", null)
16.3 文件存储
Android 文件存储分为两类:内部存储(应用私有,其他应用无法访问,卸载时自动清除)和外部存储(公共区域,用户可以访问,如相册、下载文件夹)。
从 Android 10(API 29)开始,Google 引入了分区存储(Scoped Storage),大幅限制了应用对外部存储的访问权限:
- 应用不再能随意访问 SD 卡上的任意目录
- 访问其他应用的文件需要通过
MediaStoreAPI 或Storage Access Framework - 使用
file://URI 分享文件会崩溃,必须使用 FileProvider 生成content://URI
对应 iOS 的沙盒机制,Android 的 Scoped Storage 让权限管理更安全,但也需要调整文件操作的写法。
// 内部存储(对应 iOS Documents 目录,应用私有,卸载时删除)
class FileStorageManager(private val context: Context) {
// 内部存储文件路径
fun getInternalFile(filename: String): File {
return File(context.filesDir, filename)
// 对应 iOS: FileManager.default.urls(for: .documentDirectory, ...).first
}
// 缓存文件(系统可以在存储不足时清理)
fun getCacheFile(filename: String): File {
return File(context.cacheDir, filename)
// 对应 iOS: FileManager.default.urls(for: .cachesDirectory, ...)
}
// 写入文件
fun writeTextFile(filename: String, content: String) {
File(context.filesDir, filename).writeText(content)
}
fun readTextFile(filename: String): String? {
val file = File(context.filesDir, filename)
return if (file.exists()) file.readText() else null
}
// 写入 JSON(序列化对象)
inline fun <reified T> writeJsonFile(filename: String, data: T) {
val file = File(context.filesDir, filename)
file.writeText(Gson().toJson(data))
}
inline fun <reified T> readJsonFile(filename: String): T? {
val file = File(context.filesDir, filename)
if (!file.exists()) return null
return try {
Gson().fromJson(file.readText(), T::class.java)
} catch (e: Exception) {
null
}
}
// 删除文件
fun deleteFile(filename: String): Boolean {
return File(context.filesDir, filename).delete()
}
}
// 外部存储(公共媒体,需要权限,API 29+ 使用 MediaStore)
// API 29+ 推荐使用 MediaStore,无需 WRITE_EXTERNAL_STORAGE 权限
fun saveImageToGallery(context: Context, bitmap: Bitmap, filename: String) {
val contentValues = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, filename)
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/MyApp") // 保存位置
}
val uri = context.contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues
) ?: return
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 95, outputStream)
}
}
权限系统(对比 iOS 权限)
Android 的权限系统比 iOS 更复杂,分为"普通权限"(安装时自动授予)和"危险权限"(运行时请求)。对应 iOS 的 Privacy - XXX Usage Description。
| 权限类型 | iOS | Android |
|---|---|---|
| 相机 | NSCameraUsageDescription | android.permission.CAMERA |
| 麦克风 | NSMicrophoneUsageDescription | android.permission.RECORD_AUDIO |
| 定位(精确) | NSLocationWhenInUseUsageDescription | ACCESS_FINE_LOCATION |
| 定位(粗略) | 无区分 | ACCESS_COARSE_LOCATION |
| 照片读取 | NSPhotoLibraryUsageDescription | READ_MEDIA_IMAGES (API 33+) |
| 联系人 | NSContactsUsageDescription | READ_CONTACTS / WRITE_CONTACTS |
| 通知 | UNUserNotificationCenter.requestAuthorization | POST_NOTIFICATIONS (API 33+) |
| 网络 | 自动(无需声明) | INTERNET(普通权限,自动授予) |
| 蓝牙 | NSBluetoothAlwaysUsageDescription | BLUETOOTH_CONNECT (API 31+) |
17.1 运行时权限请求(新版 API)
// 相机权限请求
AVCaptureDevice.requestAccess(for: .video) { granted in
DispatchQueue.main.async {
if granted {
self.openCamera()
} else {
self.showPermissionAlert()
}
}
}
// 定位权限
locationManager.requestWhenInUseAuthorization()
// 通知权限
UNUserNotificationCenter.current().requestAuthorization(
options: [.alert, .badge, .sound]
) { granted, error in
print("Notification permission: \(granted)")
}
// 检查权限状态
let status = AVCaptureDevice.authorizationStatus(for: .video)
switch status {
case .authorized: openCamera()
case .notDetermined: requestPermission()
case .denied, .restricted: showSettings()
@unknown default: break
}
// 新版 ActivityResultLauncher API(推荐,替代 onRequestPermissionsResult)
@AndroidEntryPoint
class CameraFragment : Fragment() {
// 单个权限
private val cameraPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
openCamera()
} else {
showPermissionDeniedMessage()
}
}
// 多个权限
private val multiplePermissionsLauncher = registerForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { permissions ->
val cameraGranted = permissions[Manifest.permission.CAMERA] ?: false
val audioGranted = permissions[Manifest.permission.RECORD_AUDIO] ?: false
if (cameraGranted && audioGranted) {
startVideoRecording()
}
}
private fun requestCameraPermission() {
when {
// 已有权限
ContextCompat.checkSelfPermission(
requireContext(), Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED -> {
openCamera()
}
// 需要显示权限说明(用户之前拒绝过)
shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> {
showPermissionRationaleDialog(
message = "相机权限用于拍摄照片和扫描二维码",
onConfirm = { cameraPermissionLauncher.launch(Manifest.permission.CAMERA) }
)
}
// 直接请求权限
else -> {
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
}
}
}
private fun showPermissionRationaleDialog(message: String, onConfirm: () -> Unit) {
AlertDialog.Builder(requireContext())
.setTitle("需要相机权限")
.setMessage(message)
.setPositiveButton("授权") { _, _ -> onConfirm() }
.setNegativeButton("取消", null)
.setNeutralButton("前往设置") { _, _ ->
// 跳转到应用设置页(对应 iOS UIApplication.openURL(settingsUrl))
startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", requireContext().packageName, null)
})
}
.show()
}
}
// 使用 Accompanist 权限库(Compose 中更优雅)
// implementation("com.google.accompanist:accompanist-permissions:0.33.2-alpha")
@Composable
fun CameraPermissionScreen() {
val cameraPermissionState = rememberPermissionState(
Manifest.permission.CAMERA
)
when {
cameraPermissionState.status.isGranted -> {
CameraPreview()
}
cameraPermissionState.status.shouldShowRationale -> {
Column {
Text("需要相机权限来拍摄照片")
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
Text("授权")
}
}
}
else -> {
Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
Text("请求相机权限")
}
}
}
}
进阶 — 自定义 View(对比 UIView)
自定义 View 是 Android 高级开发的重要技能,对应 iOS 的自定义 UIView + Core Graphics。当内置控件无法满足需求时,通过重写 onMeasure/onLayout/onDraw 实现完全自定义的控件。
| 概念 | iOS UIView | Android View |
|---|---|---|
| 尺寸计算 | intrinsicContentSize / systemLayoutSizeFitting | onMeasure() |
| 布局子 View | layoutSubviews() | onLayout() |
| 绘制 | draw(_ rect: CGRect) + UIGraphicsContext | onDraw(canvas: Canvas) + Paint |
| 触摸事件 | touchesBegan/Moved/Ended | onTouchEvent(MotionEvent) |
| 手势识别 | UIGestureRecognizer | GestureDetector |
| 属性动画 | UIView.animate / CAAnimation | ObjectAnimator / ValueAnimator |
| 自定义属性 | @IBInspectable | declare-styleable + obtainStyledAttributes |
18.1 完整自定义 View 示例
// 自定义圆形进度条 View(对应 iOS 自定义 UIView + Core Graphics)
class CircleProgressView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null, // 支持 XML 属性
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
// 自定义属性(对应 iOS @IBInspectable)
var progress: Float = 0f
set(value) {
field = value.coerceIn(0f, 1f) // 限制在 [0, 1] 范围内
invalidate() // 触发重绘(对应 iOS setNeedsDisplay())
}
var progressColor: Int = Color.BLUE
var trackColor: Int = Color.LTGRAY
var strokeWidth: Float = 20f
var centerText: String = ""
// Paint 对象(对应 iOS UIColor / CGContext 绘制属性)
private val trackPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE // 只绘制描边(对应 iOS CGContext strokePath)
strokeCap = Paint.Cap.ROUND // 圆头
}
private val progressPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
strokeCap = Paint.Cap.ROUND
}
private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
textAlign = Paint.Align.CENTER
isFakeBoldText = true
}
// 矩形区域(避免在 onDraw 中创建对象,提升性能)
private val oval = RectF()
// 读取 XML 自定义属性(对应 iOS @IBInspectable 的 Interface Builder 设置)
init {
context.obtainStyledAttributes(attrs, R.styleable.CircleProgressView).apply {
progress = getFloat(R.styleable.CircleProgressView_progress, 0f)
progressColor = getColor(R.styleable.CircleProgressView_progressColor, Color.BLUE)
trackColor = getColor(R.styleable.CircleProgressView_trackColor, Color.LTGRAY)
strokeWidth = getDimension(R.styleable.CircleProgressView_strokeWidth, 20f)
centerText = getString(R.styleable.CircleProgressView_centerText) ?: ""
recycle() // 必须 recycle,释放 TypedArray
}
}
// 测量尺寸(对应 iOS intrinsicContentSize)
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// MeasureSpec 解析
val desiredSize = (strokeWidth * 10).toInt() // 默认尺寸
val width = resolveSize(desiredSize, widthMeasureSpec)
val height = resolveSize(desiredSize, heightMeasureSpec)
// 保持正方形
val size = minOf(width, height)
setMeasuredDimension(size, size) // 必须调用此方法设置尺寸
}
// 绘制(对应 iOS draw(_ rect:))
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val size = minOf(width, height).toFloat()
val padding = strokeWidth / 2 + 4f // 留出描边的空间
// 设置绘制区域
oval.set(padding, padding, size - padding, size - padding)
// 绘制背景轨道(对应 iOS CGContext arc)
trackPaint.apply {
color = trackColor
this.strokeWidth = this@CircleProgressView.strokeWidth
}
canvas.drawArc(oval, 0f, 360f, false, trackPaint)
// 绘制进度弧(-90f 从顶部开始)
progressPaint.apply {
color = progressColor
this.strokeWidth = this@CircleProgressView.strokeWidth
// 渐变色(对应 iOS CAGradientLayer)
shader = SweepGradient(
size / 2, size / 2,
intArrayOf(progressColor, progressColor and 0x00FFFFFF or (0x80 shl 24)),
floatArrayOf(0f, 1f)
)
}
canvas.drawArc(oval, -90f, 360f * progress, false, progressPaint)
// 绘制中心文字(对应 iOS Core Text)
if (centerText.isNotEmpty()) {
textPaint.apply {
color = progressColor
textSize = size * 0.2f
}
val textY = size / 2 - (textPaint.descent() + textPaint.ascent()) / 2
canvas.drawText(centerText, size / 2, textY, textPaint)
}
}
// 属性动画(对应 iOS UIView.animate)
fun animateToProgress(targetProgress: Float, duration: Long = 1000L) {
ObjectAnimator.ofFloat(this, "progress", progress, targetProgress).apply {
this.duration = duration
interpolator = DecelerateInterpolator()
start()
}
}
}
// res/values/attrs.xml — 自定义属性声明
/*
<declare-styleable name="CircleProgressView">
<attr name="progress" format="float" />
<attr name="progressColor" format="color" />
<attr name="trackColor" format="color" />
<attr name="strokeWidth" format="dimension" />
<attr name="centerText" format="string" />
</declare-styleable>
*/
18.2 触摸事件与手势
Android 的触摸事件处理是一个事件分发树,当用户触摸屏幕时,事件从 Activity → ViewGroup → View 逐层向下传递。理解这个机制,能帮你解决自定义 View 的滑动冲突(比如 ScrollView 里嵌套 RecyclerView 时的手势竞争问题)。
每个 View/ViewGroup 都有三个关键方法:
dispatchTouchEvent():事件分发总入口,负责决定是自己处理还是往下传onInterceptTouchEvent()(仅 ViewGroup 有):是否拦截事件,返回true则子 View 不会收到后续事件onTouchEvent():实际处理触摸事件,返回true表示消费了这个事件
在日常开发中,你通常不需要手动实现这些,而是使用 GestureDetector(手势检测器)来识别常见手势:点击、双击、长按、滑动、抛掷(fling)——对应 iOS 的各种 UIGestureRecognizer。
class InteractiveView(context: Context, attrs: AttributeSet? = null) : View(context, attrs) {
// GestureDetector(对应 iOS UIGestureRecognizer)
private val gestureDetector = GestureDetectorCompat(context, object : GestureDetector.SimpleOnGestureListener() {
// 单击(对应 UITapGestureRecognizer)
override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
onClick(e.x, e.y)
return true
}
// 双击(对应 UITapGestureRecognizer(numberOfTapsRequired: 2))
override fun onDoubleTap(e: MotionEvent): Boolean {
onDoubleTap(e.x, e.y)
return true
}
// 长按(对应 UILongPressGestureRecognizer)
override fun onLongPress(e: MotionEvent) {
onLongPress(e.x, e.y)
}
// 滑动(对应 UISwipeGestureRecognizer)
override fun onFling(
e1: MotionEvent?,
e2: MotionEvent,
velocityX: Float,
velocityY: Float
): Boolean {
if (velocityX > 500) onSwipeRight()
else if (velocityX < -500) onSwipeLeft()
return true
}
// 拖拽(对应 UIPanGestureRecognizer)
override fun onScroll(
e1: MotionEvent?,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
): Boolean {
onDrag(-distanceX, -distanceY)
return true
}
})
// ScaleGestureDetector:缩放手势(对应 UIPinchGestureRecognizer)
private var scaleFactor = 1f
private val scaleGestureDetector = ScaleGestureDetector(context, object : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScale(detector: ScaleGestureDetector): Boolean {
scaleFactor *= detector.scaleFactor
scaleFactor = scaleFactor.coerceIn(0.1f, 10f)
invalidate()
return true
}
})
// RotationGestureDetector:旋转手势(对应 UIRotationGestureRecognizer)
// 分发触摸事件
override fun onTouchEvent(event: MotionEvent): Boolean {
var handled = scaleGestureDetector.onTouchEvent(event) // 优先处理缩放
handled = gestureDetector.onTouchEvent(event) || handled
return handled || super.onTouchEvent(event)
}
private fun onClick(x: Float, y: Float) { /* 处理点击 */ }
private fun onDoubleTap(x: Float, y: Float) { /* 处理双击 */ }
private fun onLongPress(x: Float, y: Float) { /* 处理长按 */ }
private fun onSwipeRight() { /* 处理右滑 */ }
private fun onSwipeLeft() { /* 处理左滑 */ }
private fun onDrag(dx: Float, dy: Float) { /* 处理拖拽 */ }
}
18.3 属性动画
Android 动画系统分为两大类,理解区别很重要:
- 视图动画(View Animation):XML 定义的老式动画(tween animation)。只改变 View 的视觉显示位置,实际的点击区域和 View 属性不变。基本已被淘汰,不要用。
- 属性动画(Property Animation):从 Android 3.0 开始引入,真实改变对象的属性值(包括点击区域)。现代 Android 动画的标准方案,对应 iOS 的
UIView.animate/CAAnimation。
属性动画的三种常用 API(由简到复杂):
- ViewPropertyAnimator:
view.animate().alpha(0f).setDuration(300).start()——最简洁,专为 View 设计,对应 iOSUIView.animate(withDuration:) - ObjectAnimator:可以动画 View 上的任意属性(包括自定义 View 的属性),需要有对应的 setter
- ValueAnimator:只产生数值变化,不自动更新 View,需要自己在
onAnimationUpdate中处理——灵活度最高
// ViewPropertyAnimator(对应 UIView.animate(withDuration:))
// 最简单的动画 API
view.animate()
.alpha(0f) // 对应 view.alpha = 0
.translationX(100f) // 对应 view.center.x += 100
.scaleX(1.5f) // 对应 view.transform = CGAffineTransform(scaleX:y:)
.rotation(45f) // 旋转 45 度
.setDuration(300) // 对应 duration: 0.3
.setInterpolator(DecelerateInterpolator()) // 对应 .easeOut
.withStartAction { /* 动画开始 */ }
.withEndAction { /* 动画结束(对应 completion:)*/ }
.start()
// ObjectAnimator(更灵活,可以动画任意属性)
ObjectAnimator.ofFloat(view, View.TRANSLATION_Y, 0f, -100f).apply {
duration = 400
interpolator = BounceInterpolator()
repeatCount = 0
addListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
// 动画结束回调
}
})
start()
}
// AnimatorSet:组合多个动画(对应 iOS AnimationGroup)
val animSet = AnimatorSet().apply {
playTogether( // 同时播放
ObjectAnimator.ofFloat(view, View.SCALE_X, 1f, 1.2f),
ObjectAnimator.ofFloat(view, View.SCALE_Y, 1f, 1.2f),
ObjectAnimator.ofFloat(view, View.ALPHA, 1f, 0.8f)
)
// 或 playSequentially() 顺序播放
duration = 300
}
animSet.start()
// ValueAnimator:自定义值动画
ValueAnimator.ofFloat(0f, 1f).apply {
duration = 1000
addUpdateListener { animation ->
val value = animation.animatedValue as Float
// 使用 value 更新任何东西
myView.progress = value
progressText.text = "${(value * 100).toInt()}%"
}
start()
}
性能优化
Android 性能优化与 iOS 有很多共通之处,都需要关注布局性能、内存管理、图片加载等。Android 有独特的碎片化问题和 Java/Kotlin 的 GC 特性需要特别注意。
| 优化方向 | iOS 工具 | Android 工具 |
|---|---|---|
| 布局性能 | View Debugger / Instruments | Layout Inspector / Profiler |
| 内存分析 | Memory Graph Debugger / Leaks | Memory Profiler / LeakCanary |
| CPU 性能 | Time Profiler | CPU Profiler / Systrace |
| 网络分析 | Network Profiler / Charles | Network Profiler / Charles / Flipper |
| 图片工具 | Image I/O Instruments | Profiler + Glide 内置统计 |
| 崩溃分析 | Crashlytics / Xcode Organizer | Crashlytics / Play Console |
19.1 布局优化
<!-- ❌ 错误:嵌套过深,性能差 -->
<LinearLayout>
<LinearLayout>
<LinearLayout>
<TextView />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<!-- ✅ 正确:用 ConstraintLayout 扁平化布局 -->
<ConstraintLayout>
<TextView app:layout_constraintTop_toTopOf="parent" />
</ConstraintLayout>
<!-- merge 标签:减少一层包装(用于 include 的根布局)-->
<!-- item_header.xml -->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 内容直接插入到 include 的父容器中,少一层 ViewGroup -->
<TextView android:id="@+id/header_title" />
<TextView android:id="@+id/header_subtitle" />
</merge>
<!-- ViewStub:懒加载,按需展开(对比 iOS 按需创建 View)-->
<ViewStub
android:id="@+id/stub_empty_view"
android:inflatedId="@+id/empty_view"
android:layout="@layout/layout_empty"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<!-- 在代码中按需加载 -->
<!-- binding.stubEmptyView.inflate() 展开 ViewStub(只执行一次)-->
19.2 内存管理与泄漏预防
// ❌ 常见内存泄漏 1:Fragment 持有 View 引用
class BadFragment : Fragment() {
// 错误:Fragment 销毁 View 后,binding 仍持有 View 引用
private lateinit var binding: FragmentBadBinding
// 正确做法已在第6章介绍:_binding = null 在 onDestroyView
}
// ❌ 常见内存泄漏 2:单例持有 Context
object BadSingleton {
// 错误:Activity Context 泄漏
var context: Context? = null
}
// ✅ 使用 Application Context
object GoodSingleton {
// Application Context 生命周期与 App 相同,不会泄漏
lateinit var appContext: Context
}
// ❌ 常见内存泄漏 3:Lambda 捕获 Activity/Fragment
class BadActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 错误:lambda 持有 this(Activity 引用)
handler.postDelayed({
updateUI() // this 被 lambda 捕获
}, 5000)
}
}
// ✅ 使用 WeakReference
class GoodActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val weakActivity = WeakReference(this)
handler.postDelayed({
weakActivity.get()?.updateUI() // 若 Activity 已销毁,不执行
}, 5000)
}
}
// ❌ 常见内存泄漏 4:未取消的协程
class BadViewModel : ViewModel() {
// 错误:使用 GlobalScope,不会随 ViewModel 销毁而取消
fun badLoad() {
GlobalScope.launch { /* 永远不会取消 */ }
}
}
// ✅ 使用 viewModelScope
class GoodViewModel : ViewModel() {
fun goodLoad() {
viewModelScope.launch { /* ViewModel 销毁时自动取消 */ }
}
}
// LeakCanary:自动检测内存泄漏(对应 iOS Instruments Leaks)
// 只需添加依赖即可,debug 构建自动启用
// debugImplementation("com.squareup.leakcanary:leakcanary-android:2.12")
// StrictMode:在 Debug 模式下检测主线程违规(磁盘/网络操作)
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog() // 打印日志
.penaltyDialog() // 弹出对话框(可见的违规)
.build()
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.build()
)
}
}
}
19.3 图片加载优化(Glide vs Coil)
图片加载是移动开发中最容易 OOM(内存溢出)的操作之一。一张 4000×3000 的照片,如果直接解码到内存,占用约 46MB(4000×3000×4字节/ARGB)!而一台中端手机的单个应用内存限制可能只有 256MB。
图片加载库(Glide / Coil / Kingfisher)做了以下关键优化:
- 按目标 View 尺寸降采样:ImageView 只有 100×100 dp,就只解码到这个尺寸,而不是加载原图
- 内存缓存 + 磁盘缓存:同一张图片不会重复下载和解码
- 生命周期感知:Activity/Fragment 销毁时自动取消正在进行的请求,防止内存泄漏
- 线程管理:网络请求和解码在后台线程,只在主线程设置到 ImageView
// Glide(对应 iOS Kingfisher / SDWebImage)
// 基本用法
Glide.with(context)
.load(imageUrl)
.placeholder(R.drawable.placeholder) // 占位图(对应 Kingfisher .placeholder)
.error(R.drawable.error_image) // 错误图
.centerCrop() // 对应 .scaledToFill()
.transition(DrawableTransitionOptions.withCrossFade()) // 淡入动画
.into(imageView)
// 自定义 Glide 配置(对应 Kingfisher KingfisherManager 全局配置)
@GlideModule
class MyGlideModule : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) {
// 内存缓存大小
builder.setMemoryCache(LruResourceCache(50 * 1024 * 1024L)) // 50MB
// 磁盘缓存
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 200 * 1024 * 1024L))
}
}
// Coil(更现代,Kotlin 原生,对应 Kingfisher)
// build.gradle: implementation("io.coil-kt:coil:2.5.0")
imageView.load(imageUrl) {
crossfade(true)
placeholder(R.drawable.placeholder)
error(R.drawable.error_image)
transformations(CircleCropTransformation(), BlurTransformation(context, radius = 8))
}
// 预加载(对应 Kingfisher prefetch)
val request = ImageRequest.Builder(context)
.data(imageUrl)
.target { /* 预加载,不显示 */ }
.build()
imageLoader.enqueue(request)
// RecyclerView 中的性能优化
class OptimizedAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
super.onViewRecycled(holder)
// ViewHolder 被回收时,取消 Glide 请求(对应 Kingfisher cancelImageDownloadTask)
Glide.with(holder.itemView.context).clear(holder.itemView)
}
}
19.4 RecyclerView 性能优化
RecyclerView 本身已经很高效,但不当使用会让它变慢。几个最常见的性能问题:
- 调用
notifyDataSetChanged():相当于告诉 RecyclerView "我不知道哪里变了,你重绘所有 item"——这会导致整个列表闪烁。应该用 DiffUtil(自动计算差异,只更新变化的 item),对应 iOS 的DiffableDataSource - 在
onBindViewHolder中创建对象:这个方法每次滚动都会调用,在里面new Paint()或new OnClickListener()会造成大量 GC 压力 - 不合理嵌套:垂直 RecyclerView 里嵌套水平 RecyclerView,如果不共享 RecycledViewPool,每次创建新的水平列表都是全量创建
RecyclerView 内部有一个三级缓存:Scrap(复用正在显示的 View)→ Cache(保存最近滑出的 View)→ RecycledViewPool(按 ViewType 保存可复用的 ViewHolder),理解这个缓存层次有助于调优复杂列表场景。
// RecyclerView 性能优化技巧
recyclerView.apply {
// 1. 固定大小(item 数量变化但 RecyclerView 大小不变)
setHasFixedSize(true)
// 2. 预取(Prefetch)— 滚动时提前加载
layoutManager = LinearLayoutManager(context).apply {
isItemPrefetchEnabled = true
initialPrefetchItemCount = 4
}
// 3. 回收池共享(多个 RecyclerView 共享 ViewHolder)
recycledViewPool = sharedRecycledViewPool
// 4. 嵌套 RecyclerView 的复用
recycledViewPool.setMaxRecycledViews(TYPE_ARTICLE, 20)
}
// 5. 使用 DiffUtil 而不是 notifyDataSetChanged()
// adapter.submitList(newList) — 自动计算差异,只更新变化的 item
// 6. 避免在 onBindViewHolder 中创建对象
// ❌ 错误
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val paint = Paint() // 每次绑定都创建新对象
val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ARGB_8888)
}
// ✅ 正确:在 ViewHolder 中复用
class OptimizedViewHolder(view: View) : RecyclerView.ViewHolder(view) {
private val paint = Paint() // 创建一次,复用
// ...
}
发布与签名
Android 应用发布流程与 iOS 有相似的概念,但操作细节差异较大。Android 使用 Keystore 进行签名,构建产物为 APK 或 AAB(Android App Bundle)。
| 发布环节 | iOS | Android |
|---|---|---|
| 签名文件 | Certificates + Provisioning Profiles | Keystore (.jks) |
| 构建产物 | .ipa | .apk (直接安装) / .aab (Google Play 推荐) |
| 代码混淆 | Swift 编译器优化 | ProGuard / R8 |
| 内测分发 | TestFlight | Internal Test / Firebase App Distribution |
| 版本管理 | Build Number + Version | versionCode + versionName |
| 审核时间 | 1-3 天 | 几小时 - 1 天 |
| 多渠道 | 不支持 | Build Variants + Product Flavors |
20.1 生成 Keystore
# 使用 keytool 生成 Keystore(对应 iOS 申请 Certificate)
# keytool 在 JDK 中,Android Studio 的 JDK 目录下也有
keytool -genkey -v \
-keystore myapp-release.jks \ # Keystore 文件名
-keyalg RSA \
-keysize 2048 \
-validity 10000 \ # 有效期(天),建议至少 25 年
-alias myapp-key \ # 别名
-storepass "store_password" \ # Keystore 密码
-keypass "key_password" # Key 密码
# 系统会询问:名字、组织、城市、省份、国家
# 填写后生成 myapp-release.jks 文件
# ⚠️ 重要:Keystore 必须安全保存!
# 如果丢失,将无法更新 Google Play 上的应用
# 查看 Keystore 信息
keytool -list -v -keystore myapp-release.jks
# Android Studio 也提供 GUI 方式:
# Build → Generate Signed Bundle/APK → Create New...
20.2 签名配置
android {
// 签名配置(对应 iOS Signing & Capabilities)
signingConfigs {
create("release") {
// 方式1:直接写(不推荐,安全风险)
// storeFile = file("myapp-release.jks")
// storePassword = "store_password"
// keyAlias = "myapp-key"
// keyPassword = "key_password"
// 方式2:从环境变量读取(推荐,CI/CD 时使用)
storeFile = file(System.getenv("KEYSTORE_PATH") ?: "myapp-release.jks")
storePassword = System.getenv("KEYSTORE_PASSWORD") ?: ""
keyAlias = System.getenv("KEY_ALIAS") ?: ""
keyPassword = System.getenv("KEY_PASSWORD") ?: ""
// 方式3:从 local.properties 读取(推荐本地开发)
// local.properties 加入 .gitignore,不提交到 git
}
}
buildTypes {
release {
signingConfig = signingConfigs.getByName("release") // 使用 release 签名
isMinifyEnabled = true // 开启代码压缩
isShrinkResources = true // 移除未使用的资源
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
debug {
// debug 自动使用 Android Studio 的 debug keystore
// debug keystore 位置:~/.android/debug.keystore
}
}
// Build Variants(对应 iOS Build Configurations + Schemes)
flavorDimensions += listOf("environment", "store")
productFlavors {
// 环境维度
create("dev") {
dimension = "environment"
applicationIdSuffix = ".dev"
versionNameSuffix = "-dev"
buildConfigField("String", "BASE_URL", "\"https://dev-api.example.com/\"")
buildConfigField("Boolean", "ENABLE_LOGGING", "true")
}
create("staging") {
dimension = "environment"
applicationIdSuffix = ".staging"
buildConfigField("String", "BASE_URL", "\"https://staging-api.example.com/\"")
buildConfigField("Boolean", "ENABLE_LOGGING", "true")
}
create("prod") {
dimension = "environment"
buildConfigField("String", "BASE_URL", "\"https://api.example.com/\"")
buildConfigField("Boolean", "ENABLE_LOGGING", "false")
}
// 渠道维度(多应用商店)
create("googlePlay") {
dimension = "store"
buildConfigField("String", "STORE_NAME", "\"google_play\"")
}
create("huawei") {
dimension = "store"
applicationIdSuffix = ".huawei"
buildConfigField("String", "STORE_NAME", "\"huawei_appgallery\"")
}
}
}
20.3 ProGuard/R8 混淆配置
iOS 编译后代码默认已经过编译器优化,而 Android 默认不开启混淆——Debug 包的 APK 里类名、方法名全都是原始名称,任何人都可以用工具反编译查看你的代码逻辑。发布 Release 版本时必须开启混淆。
R8 是 Android 的代码优化工具(ProGuard 的进化版),它做四件事:
- 代码缩减(Shrinking):移除未使用的类、方法、字段——Tree Shaking
- 资源缩减(Resource Shrinking):移除未使用的图片、字符串等资源
- 混淆(Obfuscation):将类名
UserRepository改为a.b.c,增加反编译难度 - 代码优化(Optimization):内联方法、移除冗余代码等
-keep 保留?混淆会把类名改掉,但以下场景依赖原始名称:
• Retrofit:通过注解和反射解析 API 接口,混淆后注解信息丢失
• 序列化/反序列化:Gson/Moshi 通过字段名映射 JSON,混淆后字段名变了,JSON 解析失败
• 数据模型类:用于网络传输或数据库的类,字段名不能改变
如果 Release 包功能正常而 Debug 包不正常,通常是少写了 -keep 规则。
# ProGuard/R8 混淆规则(对应 iOS 的 bitcode + 编译器优化)
# 保留 Retrofit 相关(混淆会破坏注解反射)
-keepattributes Signature
-keepattributes *Annotation*
-keep class retrofit2.** { *; }
-keepclasseswithmembers class * {
@retrofit2.http.* <methods>;
}
# 保留 Gson 解析的数据类(混淆会破坏字段名映射)
-keep class com.example.app.data.model.** { *; }
-keepclassmembers class com.example.app.data.model.** {
<fields>;
}
# 保留 Room 实体
-keep class com.example.app.data.local.entity.** { *; }
# 保留枚举
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# 保留 Parcelable(用于 Intent 数据传递)
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
# Hilt 相关
-keep class dagger.hilt.** { *; }
-keep @dagger.hilt.android.HiltAndroidApp class * { *; }
# 日志移除(Release 包不输出日志)
-assumenosideeffects class android.util.Log {
public static boolean isLoggable(java.lang.String, int);
public static int v(...);
public static int i(...);
public static int d(...);
public static int w(...);
public static int e(...);
}
20.4 构建 Release APK/AAB
# 构建 Release APK(对应 iOS Archive → Export .ipa)
./gradlew assembleProdGooglePlayRelease
# 构建 AAB(Android App Bundle,Google Play 推荐格式)
# AAB 比 APK 小 20%,Google Play 会根据设备生成最优 APK
./gradlew bundleProdGooglePlayRelease
# 输出路径:
# APK: app/build/outputs/apk/prod/googlePlay/release/app-prod-googlePlay-release.apk
# AAB: app/build/outputs/bundle/prodGooglePlayRelease/app-prod-googlePlay-release.aab
# 验证签名
apksigner verify --verbose app-release.apk
jarsigner -verify -verbose -certs app-release.apk
# 查看 APK 内容(类似 iOS .ipa 解包查看)
apkanalyzer manifest print app-release.apk
apkanalyzer dex packages app-release.apk
# 本地安装测试(对应 iOS Xcode 直接部署到设备)
adb install -r app-prod-googlePlay-release.apk
# CI/CD 示例(GitHub Actions)
# 通常在 CI 中配置环境变量,自动构建签名包
echo "KEYSTORE_PASSWORD=${{ secrets.KEYSTORE_PASSWORD }}" >> local.properties
./gradlew bundleProdGooglePlayRelease
20.5 Google Play 发布流程对比
| 步骤 | iOS App Store | Google Play |
|---|---|---|
| 注册 | Apple Developer Program ($99/年) | Google Play Console ($25 一次性) |
| 打包 | Xcode Archive → Export .ipa | ./gradlew bundleRelease → .aab |
| 上传 | Transporter / Xcode Organizer | Play Console 直接上传 |
| 测试轨道 | TestFlight (内测/公测) | Internal → Closed → Open → Production |
| 截图要求 | 多种设备尺寸 | 手机 + 平板(可选) |
| 审核 | App Review(1-3天) | 自动审核 + 人工审核 |
| 版本管理 | Phased Release(分阶段发布) | 阶段性发布(5% → 10% → 50% → 100%) |
| 应用内支付 | StoreKit / In-App Purchase | Google Play Billing Library |
第1周:Kotlin 语法(配合 Swift 对照,约2天)→ Android Studio 配置 → 创建第一个 Hello World 项目
第2周:Activity 生命周期 → ConstraintLayout + View Binding → 基本控件(TextView、Button、ImageView)
第3周:Fragment + Navigation Component → RecyclerView(重点!)→ BottomNavigationView
第4周:ViewModel + StateFlow → Room 数据库 → Retrofit 网络请求
第5-6周:Hilt 依赖注入 → Jetpack Compose(有 SwiftUI 基础会很快)→ 协程 + Flow 深入
第7-8周:项目实战 → 性能优化 → 发布 Google Play
关键心态:Android 和 iOS 的底层逻辑是相通的,大部分概念你已经掌握,主要是适应 API 差异和 Kotlin 语法。享受这个过程!