📱 Android 开发完全指南

iOS 开发者转型教程 · 20 章 · Kotlin + Jetpack
Chapter 01

iOS vs Android — 核心差异对比

作为拥有 10 年 iOS 开发经验的工程师,你已经掌握了移动开发的核心思维。Android 开发与 iOS 开发在理念上高度相似,但在工具链、API 设计和生态系统上有显著差异。本章系统梳理两者的对应关系,帮助你快速建立心智模型。

💡 关键认知

iOS 和 Android 开发的核心思想是相通的:组件化、生命周期管理、响应式 UI、异步编程。掌握了这些原理,Android 学习将是一次"换语法"的旅程,而非从零开始。

1.1 开发语言对比:Swift vs Kotlin

Swift 和 Kotlin 都是现代的、类型安全的编程语言,设计哲学高度相似。两者都强调空安全、函数式编程特性、类型推断和简洁语法。

特性SwiftKotlin
空安全Optional (T?)Nullable (T?)
变量/常量var / letvar / val
函数定义func name() {}fun name() {}
字符串插值\(variable)$variable / ${expr}
类扩展extension扩展函数 fun Type.name()
枚举enum + associated valuessealed class / enum class
结构体struct (值类型)data class (引用类型)
协议protocolinterface
闭包{ } / trailing closurelambda { } / 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

功能XcodeAndroid Studio
基础Apple 专有 IDE基于 IntelliJ IDEA(JetBrains)
模拟器iOS SimulatorAndroid Virtual Device (AVD)
界面设计Interface Builder / SwiftUI PreviewLayout Editor / Compose Preview
性能分析InstrumentsAndroid Profiler
调试LLDBADB + JDWP debugger
代码补全基础 AI 补全AI Assistant (Gemini)
快捷键Cmd+R 运行 / Cmd+B 构建Shift+F10 运行 / Ctrl+F9 构建
重构有限重构强大的 IntelliJ 重构
平台仅 macOSmacOS / Windows / Linux

1.3 UI 框架对比

框架类型iOSAndroid
命令式 UIUIKitView System (XML + View)
声明式 UISwiftUIJetpack Compose
布局文件XIB / StoryboardXML Layout
自动布局Auto Layout / NSLayoutConstraintConstraintLayout
列表UITableView / UICollectionViewRecyclerView
导航UINavigationControllerNavigation Component
标签栏UITabBarControllerBottomNavigationView
图片显示UIImageViewImageView
文本UILabel / UITextFieldTextView / EditText

1.4 应用生命周期对比

概念iOSAndroid
应用入口AppDelegate / @mainApplication 类 / AndroidManifest.xml
场景管理SceneDelegate (iOS 13+)Activity(每个界面独立)
视图控制器UIViewControllerActivity / Fragment
视图加载viewDidLoad()onCreate()
即将显示viewWillAppear()onStart() / onResume()
已经显示viewDidAppear()onResume()
即将消失viewWillDisappear()onPause()
已经消失viewDidDisappear()onStop()
销毁deinitonDestroy()

1.5 完整概念映射表

iOS 概念Android 对应说明
AppDelegateApplication全局应用入口,管理生命周期
UIViewControllerActivity界面容器,管理视图层级
UIViewController (子视图)Fragment可复用的 UI 片段,寄生在 Activity 中
XIB / StoryboardXML Layout声明式 UI 布局文件
Auto LayoutConstraintLayout基于约束的响应式布局系统
UITableViewRecyclerView (LinearLayoutManager)垂直滚动列表
UICollectionViewRecyclerView (GridLayoutManager)网格/灵活布局列表
UITableViewDelegate/DataSourceRecyclerView.Adapter列表数据源与事件
Delegate PatternInterface / Listener回调通信模式
NotificationCenterEventBus / LiveData / Flow跨组件事件传递
Combine / RxSwiftFlow / LiveData / RxJava响应式编程框架
Core DataRoom Database本地关系型数据库 ORM
URLSession / AlamofireOkHttp / Retrofit网络请求库
GCD / OperationQueueKotlin Coroutines异步并发处理
UserDefaultsSharedPreferences / DataStore轻量级键值对存储
Info.plistAndroidManifest.xml应用配置清单文件
NSBundleIntent / Bundle数据传递容器
Segue / present()Intent / Navigation Component页面导航跳转
CocoaPods / SPMGradle / Maven依赖管理系统
IBOutlet / IBActionView Binding / View.setOnClickListener视图绑定与事件
@State / @Binding (SwiftUI)remember / mutableStateOf (Compose)声明式 UI 状态管理
@ObservedObject (SwiftUI)ViewModel + collectAsStateViewModel 状态观察
SDWebImage / KingfisherGlide / Coil / Picasso异步图片加载库
Swinject / 手动 DIHilt / Dagger / Koin依赖注入框架
Swift ConcurrencyKotlin Coroutines + Flow结构化并发框架
TestFlightFirebase App Distribution / Internal Test内测分发平台
App StoreGoogle Play正式应用商店

1.6 平台分发对比

分发环节iOS (App Store)Android (Google Play)
签名工具Certificates + Provisioning ProfileKeystore (.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 一次性注册费
✅ iOS 开发者快速上手建议

1. 先把 Kotlin 语法过一遍(约 2 天),与 Swift 对比学习效率最高
2. 掌握 Activity + Fragment 生命周期(类比 UIViewController)
3. 学习 ConstraintLayout(与 Auto Layout 思想完全相同)
4. 掌握 RecyclerView(Android 中最常用的控件之一)
5. 理解 MVVM + ViewModel + LiveData/Flow 架构
6. 最后学 Jetpack Compose(有 SwiftUI 基础会很快)

Chapter 02

开发环境搭建

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) 及以上。

Bash 首次配置检查
# 检查 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 级别和硬件配置。

✅ 推荐 AVD 配置

设备:Pixel 7 或 Pixel 8
API Level:API 34 (Android 14) 或 API 35 (Android 15)
ABI:arm64-v8a(真机对应)或 x86_64(模拟器性能更好)
RAM:至少 2GB,推荐 4GB

Bash AVD 命令行操作
# 列出所有 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 项目有较大差异,但逻辑相似。理解目录结构是高效开发的前提。

目录结构 Android 项目目录树
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 app/src/main/AndroidManifest.xml
<?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 的结合体。它管理依赖、构建变体、签名等所有构建相关事务。

功能iOSAndroid
依赖管理CocoaPods / SPM / CarthageGradle (Maven Central / JitPack)
配置文件Podfile / Package.swiftbuild.gradle / build.gradle.kts
锁定文件Podfile.lockgradle.lockfile(可选)
构建变体Schemes + ConfigurationsBuild Types + Product Flavors
构建脚本语言Ruby (Podfile)Groovy / Kotlin DSL
Kotlin DSL app/build.gradle.kts(模块级)
// 插件声明(对应 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)
}
Kotlin DSL build.gradle.kts(项目级)
// 项目级 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。

TOML gradle/libs.versions.toml
[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" }
⚠ 注意:API Level vs iOS Deployment Target

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 查看最新分布数据。

Chapter 03

Kotlin 语言精要(为 Swift 开发者)

Kotlin 与 Swift 在设计哲学上高度相似,都是现代的、类型安全的、多范式编程语言。对于 Swift 开发者来说,学习 Kotlin 的核心工作量在于适应 JVM 的思维模型和 Kotlin 特有的语法糖。

3.1 变量与类型

Swift
// 常量
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
Kotlin
// 常量(不可变,对应 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 的空安全更加强制,不允许任何可空类型不经检查就使用。

Swift
// 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 {
    // ...
}
Kotlin
// 可空类型声明(在类型后加 ?)
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 函数

Swift
// 基本函数
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, +)
Kotlin
// 基本函数(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

Swift
// 类
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() {}
}
Kotlin
// 类(主构造函数在类头部)
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 章。

Swift async/await
// 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)
    }
}
Kotlin 协程
// 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)
            }
        }
    }
}
✅ Kotlin vs Swift 关键差异总结

1. 没有结构体值类型:Kotlin 只有类(引用类型),data class 提供不可变性特性
2. 没有 guard 语句:用 ?: return 替代
3. object 关键字:Kotlin 原生单例,优雅简洁
4. 作用域函数:let/run/with/apply/also 是 Kotlin 独有的
5. 扩展函数:可在不修改源码的情况下为任何类添加方法
6. when 表达式:比 Swift switch 更强大,可以作为表达式使用

Chapter 04

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(单向依赖)

架构图 MVVM + Clean Architecture 分层
┌─────────────────────────────────────────┐
│            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。它在应用启动时创建,生命周期与应用相同。

Kotlin MyApplication.kt
// 对应 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 — 数据层

Kotlin data/model/User.kt
// 网络响应模型(对应 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)
Kotlin data/repository/UserRepository.kt
// 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 — 业务逻辑层

Kotlin domain/usecase/GetUsersUseCase.kt
// 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 — 展示层

Kotlin ui/userlist/UserListViewModel.kt
// 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 重新计算
    }
}
Kotlin ui/userlist/UserListFragment.kt
@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
RoomSQLite 的 ORM 抽象层Core Data
NavigationFragment 间导航的框架UINavigationController + Storyboard Segue
WorkManager可靠的后台任务调度BGTaskScheduler
DataStore异步、事务性的键值对存储UserDefaults (异步版)
Paging 3分页加载库手动实现 / DiffableDataSource
CameraX相机 API 抽象层AVFoundation / AVCaptureSession
Compose声明式 UI 框架SwiftUI
Hilt基于 Dagger 的依赖注入框架Swinject / 手动 DI
Chapter 05

Activity 与生命周期

Activity 是 Android 中最重要的组件,相当于 iOS 的 UIViewController。理解 Activity 生命周期是 Android 开发的基础。

5.1 Activity 生命周期完整图解

【Activity 完整生命周期】对应 iOS UIViewController 生命周期
Activity 启动 / 首次创建
onCreate() — 对应 viewDidLoad()
初始化 View、绑定数据、设置 Adapter
onStart() — 对应 viewWillAppear()
Activity 对用户可见(但未获得焦点)
onResume() — 对应 viewDidAppear()
Activity 获得焦点,开始与用户交互
↕ 运行中(可能被部分遮挡)
onPause() — 对应 viewWillDisappear()
失去焦点(另一 Activity 在前台)保存草稿、停止动画
↕(可能返回 onResume)
onStop() — 对应 viewDidDisappear()
Activity 不再可见,释放资源、停止网络请求
↕(可能返回 onRestart → onStart)
onDestroy() — 对应 deinit
Activity 被销毁,释放所有资源
特殊状态:
• 屏幕旋转/配置变更 → Activity 销毁重建(onCreate 重新调用),ViewModel 存活!
• 按 Home 键 → onPause → onStop(Activity 仍在内存中)
• 按返回键 → onPause → onStop → onDestroy
• 系统内存不足 → onStop 后可能被杀死(onSaveInstanceState 保存状态)

5.2 生命周期方法对比

Android ActivityiOS UIViewController说明
onCreate()viewDidLoad()初始化,View 创建完毕
onStart()viewWillAppear()即将对用户可见
onResume()viewDidAppear()完全可见,获得焦点
onPause()viewWillDisappear()失去焦点,保存数据
onStop()viewDidDisappear()完全不可见
onDestroy()deinit销毁,释放资源
onRestart()无直接对应从 Stop 状态恢复时调用
onSaveInstanceState()无直接对应(State Restoration)在系统杀死前保存状态
onRestoreInstanceState()无直接对应从保存状态恢复

5.3 Activity 完整代码示例

Kotlin MainActivity.kt
@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 + 数据传递,但更加灵活,还能跨应用通信。

Swift (iOS)
// 跳转到下一个 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)
Kotlin (Android)
// 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(但用于本地传递而非网络)。

Kotlin User.kt — Parcelable 实现
// 方法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 vs Android 导航对比总结

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,更优雅地管理导航

Chapter 06

Fragment 详解

Fragment 是 Android 中可复用的 UI 片段,类似于 iOS 中的子 UIViewController。它必须寄宿在 Activity 中,有自己的生命周期和布局。现代 Android 开发推荐以 Fragment 为主要的 UI 单元,Activity 只作为容器。

💡 Fragment vs Activity 的现代实践

Google 官方推荐:一个 Activity + 多个 Fragment 的架构(Single Activity Architecture)。Activity 作为 NavHostFragment 的容器,所有界面都用 Fragment 实现,通过 Navigation Component 管理导航。这与 iOS 的 UINavigationController + UIViewController 体系高度类似。

6.1 Fragment 生命周期

【Fragment 完整生命周期】
Fragment 被添加到 Activity
onAttach() — Fragment 与 Activity 关联
onCreate() — 初始化,不涉及 View
onCreateView() — 创建/填充布局
onViewCreated() — View 创建完毕,可以操作 View
对应 iOS viewDidLoad()
onStart() / onResume()
↕ 运行
onPause() / onStop()
onDestroyView() — 销毁 View(Fragment 对象可能仍存活)
⚠️ 必须在此释放 _binding = null
onDestroy() / onDetach()

6.2 Fragment 基础实现

Kotlin HomeFragment.kt
@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。

Kotlin Fragment 事务操作
// 在 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。

Kotlin Fragment 间通信方式
// 方式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 或自定义模态视图。

Kotlin ConfirmDialogFragment.kt
// 自定义 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")
        }
    }
}
Chapter 07

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 constraintapp:layout_constraintStart/End_toStartOf/toEndOf
top/bottom constraintapp:layout_constraintTop/Bottom_toTopOf/toBottomOf
centerX/centerY constraintlayout_constraintStart_toStartOf="parent" + End_toEndOf="parent"
equal width/height constraintlayout_constraintWidth_percent / constraintDimensionRatio
UIStackView (horizontal)Chain (horizontal)
UIStackView (vertical)Chain (vertical)
UILayoutGuideGuideline / Barrier
prioritylayout_constraintHorizontal_bias / weight
XML res/layout/fragment_home.xml
<?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 其他常用布局

XML LinearLayout、RelativeLayout、FrameLayout
<!-- 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。

Swift (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)
    }
}
Kotlin (View Binding)
// 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 资源系统

XML res/values/strings.xml, colors.xml, dimens.xml
<!-- 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>
Kotlin 在代码中访问资源
// 访问字符串资源(对应 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)
Chapter 08

常用 View 控件

本章介绍 Android 最常用的控件,重点讲解 RecyclerView(对应 UITableView/UICollectionView),以及 BottomNavigationView、Toolbar 等 Material Design 组件。

8.1 基础控件对比

iOS 控件Android 对应说明
UILabelTextView文本显示
UITextFieldEditText单行文本输入
UITextViewEditText (multiline)多行文本输入/显示
UIButtonButton / MaterialButton按钮
UIImageViewImageView图片显示
UISwitchSwitch / SwitchCompat开关
UISliderSlider (Material) / SeekBar滑动条
UIProgressViewProgressBar (horizontal)进度条
UIActivityIndicatorViewCircularProgressIndicator加载指示器
UITableViewRecyclerView (Linear)垂直滚动列表
UICollectionViewRecyclerView (Grid/Staggered)网格列表
UIScrollViewScrollView / NestedScrollView可滚动容器
UITabBarBottomNavigationView底部标签栏
UINavigationBarToolbar / ActionBar顶部导航栏
UIAlertControllerAlertDialog / Snackbar弹窗/提示
WKWebViewWebView网页视图
MKMapViewMapView (Google Maps SDK)地图视图

8.2 RecyclerView 完整实现

RecyclerView 是 Android 中最核心的控件,对应 iOS 的 UITableView 和 UICollectionView。它通过 Adapter + ViewHolder 模式实现高效的列表渲染。

💡 RecyclerView vs UITableView

RecyclerView 把 UITableView 的职责进行了更细粒度的分离:
LayoutManager = UITableView.style(线性、网格等布局策略)
Adapter = UITableViewDataSource(数据源和 Cell 创建)
ViewHolder = 对应 UITableViewCell 的视图复用机制
ItemDecoration = separatorStyle(间距、分割线)
DiffUtil = DiffableDataSource(计算差异并高效更新)

8.2.1 数据模型

Kotlin Article.kt — 数据模型
// 文章数据模型
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 res/layout/item_article.xml
<?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

Kotlin ArticleAdapter.kt
// 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

Kotlin 多类型 RecyclerView Adapter
// 多类型列表(对应 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

Kotlin ArticleListFragment.kt
@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)

XML activity_main.xml + res/menu/bottom_nav_menu.xml
<!-- 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>
Kotlin MainActivity.kt — BottomNav + Navigation
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
        }
    }
}
Chapter 09

Navigation Component(对比 iOS Navigation)

Navigation Component 是 Jetpack 提供的导航框架,通过可视化的导航图(NavGraph)管理 Fragment 之间的跳转。对应 iOS 的 UINavigationController + Storyboard Segue,但功能更加强大和灵活。

iOS NavigationAndroid Navigation Component
UINavigationControllerNavController + NavHostFragment
Storyboard + SegueNavGraph (nav_graph.xml)
performSegue(withIdentifier:)navController.navigate(action)
prepare(for:sender:)Safe Args (编译时类型安全的参数传递)
navigationController?.popViewControllernavController.popBackStack()
navigationController?.popToRootViewControllernavController.popBackStack(rootId, false)
Universal LinksDeep Link

9.1 NavGraph 配置

XML res/navigation/nav_graph.xml
<?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 错误。

Kotlin HomeFragment.kt — 使用 Safe Args 导航
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

Kotlin 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 完整集成

XML res/navigation/nav_graph.xml — 完整示例
<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>
Kotlin MainActivity.kt — 完整 BottomNav 集成
@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()
    }
}
Chapter 10

ViewModel & LiveData

ViewModel 是 Android MVVM 架构的核心,解决了 iOS 开发中常见的"ViewController 过胖"问题。配合 LiveData/StateFlow,实现完整的响应式数据流。

10.1 ViewModel 生命周期

💡 ViewModel 的关键优势(对比 iOS)

Android ViewModel 在 屏幕旋转/配置变更 时会存活,而 Activity/Fragment 会销毁重建。这解决了 iOS 中因系统强制 VC 销毁导致数据丢失的问题。
iOS 中你可能用过在 AppDelegate 或 SceneDelegate 中存储状态来应对这个问题——Android 的 ViewModel 给了一个优雅的解决方案。

【ViewModel 生命周期 vs Activity/Fragment 生命周期】
Activity/Fragment
onCreate()
↕ 屏幕旋转 → 销毁重建
onDestroy()
onCreate() (重建后)
onDestroy() (用户退出)
ViewModel
ViewModel 创建
屏幕旋转 → ViewModel 存活!
ViewModel 继续存活
onCleared() (用户退出时)

10.2 LiveData vs Combine/RxSwift

概念iOSAndroid
可观察属性@Published var name = ""MutableLiveData<String>() 或 MutableStateFlow("")
订阅/观察$name.sink { } / AnyCancellableliveData.observe(owner) { }
生命周期感知需手动管理 cancellables自动感知(传入 LifecycleOwner)
主线程更新@MainActor 或 receive(on: .main)LiveData 自动在主线程,Flow 需 flowWithLifecycle
转换.map { } / .combineLatest()Transformations.map { } / combine()
冷/热流Publisher(冷) / PassthroughSubject(热)Flow(冷) / StateFlow/SharedFlow(热)

10.3 LiveData 实战

Kotlin UserViewModel.kt — 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。

Swift (Combine)
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)
    }
}
Kotlin (StateFlow)
@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 fragment_form.xml — 双向绑定
<?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>
Kotlin FormViewModel.kt — Data Binding
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 vs StateFlow 选择建议

LiveData:适合传统 View 系统,与 lifecycleOwner 集成简单,代码简洁
StateFlow:适合 Kotlin 协程 + Flow 全套,功能更强(支持 combine/zip/flatMap),推荐新项目使用
SharedFlow:适合一次性事件(导航、Snackbar),不存储历史值
总结:新项目推荐 StateFlow + SharedFlow,老项目可以 LiveData 和 StateFlow 混用

Chapter 11

协程 (Coroutines) 与 Flow

Kotlin 协程是 Android 异步编程的标准解决方案,对应 Swift 的 async/await + GCD。协程的核心思想是:用同步的方式写异步代码,避免回调地狱。

概念Swift/iOSKotlin/Android
异步函数async func fetchData()suspend fun fetchData()
调用异步函数await fetchData()fetchData() (suspend 函数内直接调用)
启动异步任务Task { ... }launch { ... }
并发任务async let / TaskGroupasync { ... } + Deferred.await()
主线程@MainActor / DispatchQueue.mainDispatchers.Main
后台线程DispatchQueue.global()Dispatchers.IO / Dispatchers.Default
取消Task.cancel()Job.cancel() / scope 取消时自动取消
超时withTimeout()withTimeout { } / withTimeoutOrNull { }
数据流AsyncStream / Combine PublisherFlow / StateFlow / SharedFlow

11.1 协程基础

Kotlin 协程核心概念
// 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 中使用协程

Kotlin 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。它支持冷启动、背压处理和丰富的操作符。

💡 冷流 vs 热流:理解 Flow / StateFlow / SharedFlow

Flow 有三种常见形态,选错会导致数据丢失或重复处理,必须搞清楚:

  • Flow(冷流):类似 iOS Publisher。没有订阅者时不产生数据;每个新订阅者都从头开始获取数据。适合"一次性请求",如网络请求、数据库单次查询。
  • StateFlow(热流,有状态):类似 iOS CurrentValueSubject。始终持有一个当前值,新订阅者立即收到最新值,多个订阅者共享同一个数据流。适合表示 UI 状态(加载中/成功/失败)。
  • SharedFlow(热流,无初始值):类似 iOS PassthroughSubject。不持有当前值,新订阅者不会收到历史数据。适合表示"一次性事件",如弹出 Toast、页面导航。

简单记忆:UI 状态用 StateFlow,一次性事件用 SharedFlow,数据管道用 Flow

Kotlin 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 错误处理

Kotlin 协程与 Flow 的错误处理
// 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!")
}
✅ 协程 vs GCD/async await 对比总结

Swift async/awaitKotlin 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(状态流)

Chapter 12

Room 数据库(对比 Core Data)

Room 是 Android 官方的数据库 ORM 库,对应 iOS 的 Core Data。相比 Core Data,Room 更加简洁直观,使用注解驱动,与 Kotlin 协程和 Flow 无缝集成。

概念Core DataRoom
数据库NSPersistentContainer@Database + RoomDatabase
数据模型NSManagedObject + .xcdatamodeld@Entity data class
数据访问NSFetchRequest + NSPredicate@Dao + @Query (SQL)
查询语言NSPredicate(自定义语法)标准 SQL
关系NSRelationship(可视化配置)外键 + @Relation 注解
迁移NSMigratePersistentStoresAutomaticallyMigration 类(手动 SQL)
响应式NSFetchedResultsControllerFlow<List<T>>
事务context.save()@Transaction 注解

12.1 Room 三大组件

12.1.1 @Entity — 数据表模型

Kotlin data/local/entity/ArticleEntity.kt
// @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 — 数据访问对象

Kotlin data/local/dao/ArticleDao.kt
// @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 — 数据库定义

Kotlin data/local/AppDatabase.kt
// @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 完整示例

Kotlin 完整的离线优先数据访问
// 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 vs Core Data 开发体验对比

Room 优点:标准 SQL 查询更直观,编译时检查 SQL 语法,与 Flow 无缝集成,学习曲线低
Core Data 优点:可视化 .xcdatamodeld 编辑器,支持 Fault(懒加载),批量操作更成熟
Room 最佳实践:在子线程执行(Room 默认禁止主线程查询),配合 Hilt 注入,使用 Flow 而不是 suspend + LiveData

Chapter 13

网络请求 — Retrofit + OkHttp

Retrofit 是 Android 最流行的 HTTP 客户端库,对应 iOS 的 Alamofire。OkHttp 是底层 HTTP 客户端(类比 URLSession),Retrofit 建立在 OkHttp 之上,提供更高级的 API 定义方式。

功能iOS (URLSession/Alamofire)Android (OkHttp/Retrofit)
底层 HTTP 客户端URLSessionOkHttp
高级 HTTP 库AlamofireRetrofit
API 定义Router enum / Endpoint struct@GET/@POST 注解 interface
JSON 解析Codable (JSONDecoder)Gson / Moshi / Kotlinx.serialization
拦截器RequestAdapter + RequestRetrierOkHttp Interceptor
认证URLCredential / InterceptorAuthenticator / Header Interceptor
日志EventMonitorHttpLoggingInterceptor
文件上传uploadTask / multipart@Multipart + @Part
取消task.cancel()Call.cancel() / 协程取消

13.1 数据模型(Gson 解析)

Swift (Codable)
// 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)
// 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 定义

Kotlin data/remote/api/ArticleApiService.kt
// 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(网络拦截器):在网络层执行,能看到重定向和重试请求,适合监控实际网络流量
Kotlin data/remote/interceptor/AuthInterceptor.kt
// 认证拦截器(对应 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 完整网络层配置

Kotlin di/NetworkModule.kt — Hilt 依赖注入配置
@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
            }
        }
    }
}
Chapter 14

Jetpack Compose(对比 SwiftUI)

Jetpack Compose 是 Android 的声明式 UI 框架,与 SwiftUI 在设计理念上高度一致。如果你已经熟悉 SwiftUI,学习 Compose 会非常自然。两者都基于"状态驱动 UI"的范式。

概念SwiftUIJetpack Compose
基础单元struct View@Composable fun
本地状态@State var count = 0var count by remember { mutableStateOf(0) }
双向绑定@Binding var text: Stringvar text by remember { mutableStateOf("") } + onValueChange
ViewModel 状态@StateObject / @ObservedObjectviewModel.uiState.collectAsState()
竖向布局VStackColumn
横向布局HStackRow
层叠布局ZStackBox
懒加载列表List / LazyVStackLazyColumn / LazyRow
修饰符.padding() .background()Modifier.padding().background()
主题@Environment(\.colorScheme)MaterialTheme.colorScheme
导航NavigationStack + .navigationDestinationNavController + Compose Navigation
动画withAnimation { } / .animation()animate*AsState / AnimatedVisibility
UI 刷新机制View 重新计算Recomposition(重组)

14.1 基础 Composable

SwiftUI
// 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
// 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
// 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)
    }
}
Compose LazyColumn
// 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

Kotlin 状态提升与 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 动画

Kotlin Compose 动画
// 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 系统互操作

Kotlin Compose 与传统 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))
            }
        }
    )
}
✅ Compose vs SwiftUI 对比总结

最相似:布局模型(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 中进行重操作

Chapter 15

依赖注入 — Hilt

Hilt 是 Google 官方推荐的 Android 依赖注入框架,基于 Dagger 构建。对应 iOS 的 Swinject 或手动 DI。Hilt 大幅减少了 Dagger 的样板代码,与 Jetpack 组件(ViewModel、WorkManager 等)无缝集成。

💡 为什么需要 DI?

没有 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 示例

Kotlin 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 */ }
}
Chapter 16

数据存储全解

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)

Swift (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) }
    }
}
Kotlin (SharedPreferences)
// 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 解决了这些问题。

✅ DataStore 的三大优势

1. 协程安全:所有读写都是异步的,通过 Flow 返回数据,不会阻塞主线程
2. 类型安全:使用强类型 Key(stringPreferencesKeyintPreferencesKey 等),避免拼写错误
3. 一致性保证:基于事务机制,不会出现 SharedPreferences 的并发写入问题

Kotlin DataStore 完整实现
// 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+ Scoped Storage 变化

从 Android 10(API 29)开始,Google 引入了分区存储(Scoped Storage),大幅限制了应用对外部存储的访问权限:

  • 应用不再能随意访问 SD 卡上的任意目录
  • 访问其他应用的文件需要通过 MediaStore API 或 Storage Access Framework
  • 使用 file:// URI 分享文件会崩溃,必须使用 FileProvider 生成 content:// URI

对应 iOS 的沙盒机制,Android 的 Scoped Storage 让权限管理更安全,但也需要调整文件操作的写法。

Kotlin 内部存储 vs 外部存储
// 内部存储(对应 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)
    }
}
Chapter 17

权限系统(对比 iOS 权限)

Android 的权限系统比 iOS 更复杂,分为"普通权限"(安装时自动授予)和"危险权限"(运行时请求)。对应 iOS 的 Privacy - XXX Usage Description。

权限类型iOSAndroid
相机NSCameraUsageDescriptionandroid.permission.CAMERA
麦克风NSMicrophoneUsageDescriptionandroid.permission.RECORD_AUDIO
定位(精确)NSLocationWhenInUseUsageDescriptionACCESS_FINE_LOCATION
定位(粗略)无区分ACCESS_COARSE_LOCATION
照片读取NSPhotoLibraryUsageDescriptionREAD_MEDIA_IMAGES (API 33+)
联系人NSContactsUsageDescriptionREAD_CONTACTS / WRITE_CONTACTS
通知UNUserNotificationCenter.requestAuthorizationPOST_NOTIFICATIONS (API 33+)
网络自动(无需声明)INTERNET(普通权限,自动授予)
蓝牙NSBluetoothAlwaysUsageDescriptionBLUETOOTH_CONNECT (API 31+)

17.1 运行时权限请求(新版 API)

iOS 权限请求
// 相机权限请求
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
}
Android 权限请求
// 新版 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("请求相机权限")
            }
        }
    }
}
Chapter 18

进阶 — 自定义 View(对比 UIView)

自定义 View 是 Android 高级开发的重要技能,对应 iOS 的自定义 UIView + Core Graphics。当内置控件无法满足需求时,通过重写 onMeasure/onLayout/onDraw 实现完全自定义的控件。

概念iOS UIViewAndroid View
尺寸计算intrinsicContentSize / systemLayoutSizeFittingonMeasure()
布局子 ViewlayoutSubviews()onLayout()
绘制draw(_ rect: CGRect) + UIGraphicsContextonDraw(canvas: Canvas) + Paint
触摸事件touchesBegan/Moved/EndedonTouchEvent(MotionEvent)
手势识别UIGestureRecognizerGestureDetector
属性动画UIView.animate / CAAnimationObjectAnimator / ValueAnimator
自定义属性@IBInspectabledeclare-styleable + obtainStyledAttributes

18.1 完整自定义 View 示例

Kotlin CircleProgressView.kt — 自定义圆形进度条
// 自定义圆形进度条 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

Kotlin 触摸事件与 GestureDetector
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(由简到复杂):

  • ViewPropertyAnimatorview.animate().alpha(0f).setDuration(300).start()——最简洁,专为 View 设计,对应 iOS UIView.animate(withDuration:)
  • ObjectAnimator:可以动画 View 上的任意属性(包括自定义 View 的属性),需要有对应的 setter
  • ValueAnimator:只产生数值变化,不自动更新 View,需要自己在 onAnimationUpdate 中处理——灵活度最高
Kotlin Android 动画(对比 UIView.animate)
// 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()
}
Chapter 19

性能优化

Android 性能优化与 iOS 有很多共通之处,都需要关注布局性能、内存管理、图片加载等。Android 有独特的碎片化问题和 Java/Kotlin 的 GC 特性需要特别注意。

优化方向iOS 工具Android 工具
布局性能View Debugger / InstrumentsLayout Inspector / Profiler
内存分析Memory Graph Debugger / LeaksMemory Profiler / LeakCanary
CPU 性能Time ProfilerCPU Profiler / Systrace
网络分析Network Profiler / CharlesNetwork Profiler / Charles / Flipper
图片工具Image I/O InstrumentsProfiler + Glide 内置统计
崩溃分析Crashlytics / Xcode OrganizerCrashlytics / Play Console

19.1 布局优化

XML 布局优化技巧
<!-- ❌ 错误:嵌套过深,性能差 -->
<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 内存管理与泄漏预防

Kotlin 常见内存泄漏场景及解决方案
// ❌ 常见内存泄漏 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
Kotlin Glide 高级用法
// 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),理解这个缓存层次有助于调优复杂列表场景。

Kotlin RecyclerView 性能最佳实践
// 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()  // 创建一次,复用
    // ...
}
Chapter 20

发布与签名

Android 应用发布流程与 iOS 有相似的概念,但操作细节差异较大。Android 使用 Keystore 进行签名,构建产物为 APK 或 AAB(Android App Bundle)。

发布环节iOSAndroid
签名文件Certificates + Provisioning ProfilesKeystore (.jks)
构建产物.ipa.apk (直接安装) / .aab (Google Play 推荐)
代码混淆Swift 编译器优化ProGuard / R8
内测分发TestFlightInternal Test / Firebase App Distribution
版本管理Build Number + VersionversionCode + versionName
审核时间1-3 天几小时 - 1 天
多渠道不支持Build Variants + Product Flavors

20.1 生成 Keystore

Bash 生成 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 签名配置

Kotlin DSL app/build.gradle.kts — 签名配置
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 proguard-rules.pro
# 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

Bash 构建命令
# 构建 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 StoreGoogle Play
注册Apple Developer Program ($99/年)Google Play Console ($25 一次性)
打包Xcode Archive → Export .ipa./gradlew bundleRelease → .aab
上传Transporter / Xcode OrganizerPlay Console 直接上传
测试轨道TestFlight (内测/公测)Internal → Closed → Open → Production
截图要求多种设备尺寸手机 + 平板(可选)
审核App Review(1-3天)自动审核 + 人工审核
版本管理Phased Release(分阶段发布)阶段性发布(5% → 10% → 50% → 100%)
应用内支付StoreKit / In-App PurchaseGoogle Play Billing Library
✅ 10年 iOS 开发者的 Android 学习路径建议

第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 语法。享受这个过程!