📱 Android 开发教程

IT 技术 80 阅读 更新于 2026-09-25 17:39

1. Android 开发概述

▼

什么是 Android

Android 是由 Google 主导开发的移动操作系统,基于 Linux 内核构建。它是全球市场份额最大的移动平台,覆盖手机、平板、手表、电视、汽车等多种设备形态。

Android 版本发展

版本API Level代号重要特性
1636BaklavaAI 集成、隐私沙盒增强
1535VanillaIceCream预测性返回手势、卫星通信扩展
1434Upside Down Cake卫星通信、照片选择器增强
1333Tiramisu通知权限、主题图标
1231Snow ConeMaterial You 动态主题
1130Red Velvet Cake一次性权限、对话气泡
1029Queen Cake深色模式、手势导航

开发语言选择

  • Kotlin(推荐):Google 官方首推语言,简洁、安全、现代化,空指针安全、扩展函数、协程支持
  • Java:传统 Android 开发语言,生态成熟,资料丰富
  • Flutter / Dart:跨平台方案,一套代码同时构建 iOS 和 Android
  • React Native:使用 JavaScript/TypeScript,可复用 Web 技能
  • Kotlin Multiplatform:共享业务逻辑代码,UI 各平台原生实现

💡

建议:

新项目优先使用 Kotlin,Google 官方文档和 Jetpack 库均以 Kotlin 为主。

Android 架构层次

  1. 应用层 (Applications):系统应用与第三方应用
  2. 应用框架层 (Application Framework):Activity Manager、Window Manager、Content Provider、View System
  3. 原生库层 (Native Libraries):SQLite、OpenGL、Webkit、libc
  4. Android Runtime:ART 虚拟机(替代旧版 Dalvik),采用 AOT 编译
  5. HAL (硬件抽象层):为硬件提供标准接口
  6. Linux 内核层:驱动、内存管理、进程管理、网络协议栈

Android 开发生态

  • Jetpack:Google 官方组件库,遵循最佳实践
  • Firebase:后端服务(分析、崩溃、推送、认证)
  • Google Play Services:地图、支付、登录等核心服务
  • AndroidX:替代旧版 Support Library,持续更新
  • Kotlin Coroutines:现代异步编程模型

2. 开发环境搭建

▼

系统要求

项目最低要求推荐配置
操作系统Windows 10/11、macOS 10.14+、Linux最新版 64 位系统
RAM8 GB16 GB+
磁盘空间16 GB32 GB+(SSD)
屏幕分辨率1280 × 8001920 × 1080+

安装 Android Studio

Android Studio 是 Google 官方的集成开发环境(IDE),基于 IntelliJ IDEA 平台。

  1. 访问官网 developer.android.com/studio 下载最新版
  2. 运行安装程序,选择标准安装(Standard)
  3. 首次启动时,会自动下载 Android SDK、Gradle 等工具
  4. 建议分配至少 4GB 内存给 IDE:Help → Change Memory Settings

配置 SDK

打开 Settings → Languages & Frameworks → Android SDK:

  • 勾选最新的稳定版 SDK Platform(如 API 34/35)
  • 在 SDK Tools 标签页中勾选:Android SDK Build-Tools、Android Emulator、Android SDK Platform-Tools
  • 安装 Google USB Driver(用于真机调试)
  • 安装 Intel HAXM 或启用 Hyper-V(模拟器加速)

配置模拟器 (AVD)

打开 Tools → Device Manager,创建虚拟设备:

  1. 选择设备类型(如 Pixel 7、Pixel Fold)
  2. 选择系统镜像(推荐 x86_64 版本,支持硬件加速)
  3. 设置 RAM 大小、内部存储容量
  4. 启用 HAXM 或 Hyper-V 加速以提升模拟器性能
  5. 可选启用冷启动/快照模式

真机调试配置

  1. 手机进入 设置 → 关于手机,连续点击 版本号 7 次开启开发者模式
  2. 进入 设置 → 开发者选项,开启 USB 调试
  3. 连接电脑后,在手机上确认调试授权对话框
  4. 无线调试(Android 11+):开发者选项 → 无线调试 → 使用配对码配对

Gradle 基础配置

// 项目级 build.gradle
plugins {
    id 'com.android.application' version '8.2.0' apply false
    id 'org.jetbrains.kotlin.android' version '1.9.20' apply false
    id 'com.google.dagger.hilt.android' version '2.50' apply false
}

// 模块级 app/build.gradle
android {
    namespace 'com.example.myapp'
    compileSdk 35

    defaultConfig {
        applicationId "com.example.myapp"
        minSdk 24
        targetSdk 35
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        debug {
            applicationIdSuffix ".debug"
            debuggable true
        }
        release {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_17
        targetCompatibility JavaVersion.VERSION_17
    }

    kotlinOptions {
        jvmTarget = "17"
    }

    buildFeatures {
        viewBinding true
        buildConfig true
    }
}

dependencies {
    implementation 'androidx.core:core-ktx:1.12.0'
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.11.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'

    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}

✅

提示:

使用 Gradle Wrapper(

gradlew

)可保证团队成员使用相同版本的 Gradle。

常用 Gradle 命令

# 构建 APK
./gradlew assembleDebug
./gradlew assembleRelease

# 构建 AAB
./gradlew bundleRelease

# 运行单元测试
./gradlew test

# 运行 UI 测试
./gradlew connectedAndroidTest

# 清理项目
./gradlew clean

# 查看依赖树
./gradlew app:dependencies

# 代码静态分析
./gradlew lint

3. 项目结构详解

▼

目录结构总览

MyApp/
├── app/                          # 主模块
│   ├── src/
│   │   ├── main/
│   │   │   ├── java/             # Kotlin/Java 源码
│   │   │   ├── res/              # 资源文件
│   │   │   │   ├── layout/       # 布局 XML
│   │   │   │   ├── drawable/     # 图片、形状
│   │   │   │   ├── values/       # 字符串、颜色、样式
│   │   │   │   ├── mipmap/       # 应用图标
│   │   │   │   ├── raw/          # 原始资源
│   │   │   │   ├── anim/         # 动画资源
│   │   │   │   ├── menu/         # 菜单资源
│   │   │   │   ├── navigation/   # 导航图
│   │   │   │   └── xml/          # XML 配置
│   │   │   └── AndroidManifest.xml
│   │   ├── test/                 # 单元测试
│   │   ├── androidTest/          # UI 测试
│   │   ├── debug/                # debug 变体资源
│   │   └── release/              # release 变体资源
│   └── build.gradle
├── gradle/
│   └── wrapper/
├── build.gradle                  # 项目级配置
├── settings.gradle               # 模块设置
├── gradle.properties             # Gradle 属性
├── local.properties              # 本地 SDK 路径(不入版本控制)
└── proguard-rules.pro            # 混淆规则

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">

    <!-- 权限声明 -->
    <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.POST_NOTIFICATIONS"/>

    <!-- 硬件特性声明 -->
    <uses-feature android:name="android.hardware.camera" android:required="false"/>

    <application
        android:name=".MyApplication"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.MyApp"
        android:allowBackup="true"
        android:supportsRtl="true"
        android:networkSecurityConfig="@xml/network_security_config"
        tools:targetApi="35">

        <!-- 主 Activity -->
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:screenOrientation="portrait"
            android:windowSoftInputMode="adjustResize">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>

            <!-- 深度链接 -->
            <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="example.com"/>
            </intent-filter>
        </activity>

        <!-- Service -->
        <service
            android:name=".service.MyService"
            android:foregroundServiceType="dataSync"
            android:exported="false"/>

        <!-- 文件提供者(用于拍照/分享) -->
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_paths"/>
        </provider>

    </application>
</manifest>

资源文件命名规范

资源类型命名规范示例
布局文件activity_.xml / fragment_.xml / item_*.xmlactivity_main.xml, item_user.xml
drawableic_.xml / bg_.xml / img_*ic_home.xml, bg_button_primary.xml
字符串snake_case 加前缀title_home, btn_submit, hint_email
颜色用途/语义命名color_primary, color_error, color_success
尺寸用途_类型margin_normal, text_size_title
id控件缩写+描述tv_title, btn_submit, et_email

Build Variants 构建变体

android {
    flavorDimensions += "environment"
    productFlavors {
        dev {
            dimension "environment"
            applicationIdSuffix ".dev"
            buildConfigField "String", "API_URL", '"https://dev-api.example.com"'
        }
        staging {
            dimension "environment"
            applicationIdSuffix ".staging"
            buildConfigField "String", "API_URL", '"https://staging-api.example.com"'
        }
        prod {
            dimension "environment"
            buildConfigField "String", "API_URL", '"https://api.example.com"'
        }
    }
}

组合后会产生:devDebug、devRelease、stagingDebug、stagingRelease、prodDebug、prodRelease 等变体。

R 文件

Android 会自动生成 R.java 文件,所有资源都会分配一个整型 ID。代码中通过 R.layout.activity_main、R.id.button1 等方式引用资源。

⚠️

注意:

R 文件的包名取决于

build.gradle

中配置的

namespace

。

4. 创建第一个 App

▼

Hello World 示例

布局文件 activity_main.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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <TextView
        android:id="@+id/tvMessage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello Android!"
        android:textSize="24sp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

    <Button
        android:id="@+id/btnClick"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:text="点击我"
        app:layout_constraintTop_toBottomOf="@id/tvMessage"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>

</androidx.constraintlayout.widget.ConstraintLayout>

Activity 代码 (Kotlin)

package com.example.myapp

import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity

class MainActivity : AppCompatActivity() {

    private lateinit var tvMessage: TextView
    private lateinit var btnClick: Button
    private var counter = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        tvMessage = findViewById(R.id.tvMessage)
        btnClick = findViewById(R.id.btnClick)

        btnClick.setOnClickListener {
            counter++
            tvMessage.text = "你点击了 $counter 次"
        }
    }
}

ViewBinding(推荐方式)

// build.gradle 中启用
android {
    buildFeatures {
        viewBinding true
    }
}

// Activity 中使用
class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.btnClick.setOnClickListener {
            binding.tvMessage.text = "Hello ViewBinding!"
        }
    }
}

运行应用

  1. 在 Android Studio 工具栏选择目标设备(模拟器或真机)
  2. 点击绿色的 ▶ Run 按钮或按 Shift + F10
  3. 首次运行需要构建 Gradle,耐心等待
  4. 应用会自动安装并启动

常用调试技巧

  • Logcat:查看应用日志(底部面板 → Logcat)
  • Layout Inspector:实时查看 UI 层级(Tools → Layout Inspector)
  • Database Inspector:查看 Room 数据库(App Inspection 面板)
  • Apply Changes:无需重启应用即可应用代码/资源改动(⚡ 图标)

✅

技巧:

使用

Log.d("TAG", "message")

输出调试信息,配合 Logcat 过滤器可快速定位问题。

5. UI 基础控件

▼

常用视图控件

TextView - 文本显示

<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="这是文本"
    android:textSize="18sp"
    android:textColor="#333333"
    android:textStyle="bold"
    android:maxLines="2"
    android:ellipsize="end"
    android:letterSpacing="0.05"
    android:lineSpacingMultiplier="1.2"/>

<!-- 富文本 SpannableString -->
val spannable = SpannableString("这是红色文字和加粗")
spannable.setSpan(ForegroundColorSpan(Color.RED), 2, 6, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
spannable.setSpan(StyleSpan(Typeface.BOLD), 7, 9, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
textView.text = spannable

EditText - 输入框

<com.google.android.material.textfield.TextInputLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:hint="邮箱"
    app:errorEnabled="true"
    app:startIconDrawable="@drawable/ic_email">

    <com.google.android.material.textfield.TextInputEditText
        android:id="@+id/etEmail"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="textEmailAddress"
        android:maxLength="50"/>
</com.google.android.material.textfield.TextInputLayout>

// 密码框
<com.google.android.material.textfield.TextInputLayout
    app:endIconMode="password_toggle"
    android:hint="密码">
    <com.google.android.material.textfield.TextInputEditText
        android:inputType="textPassword"/>
</com.google.android.material.textfield.TextInputLayout>

// Kotlin 获取输入
val email = binding.etEmail.text.toString()

Button - 按钮

<!-- 填充按钮(主操作) -->
<com.google.android.material.button.MaterialButton
    android:id="@+id/btnAction"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="提交"
    app:icon="@drawable/ic_send"
    app:iconGravity="textStart"
    app:cornerRadius="8dp"/>

<!-- 轮廓按钮 -->
<Button
    style="@style/Widget.Material3.Button.OutlinedButton"
    android:text="取消"/>

<!-- 文本按钮 -->
<Button
    style="@style/Widget.Material3.Button.TextButton"
    android:text="了解更多"/>

<!-- 图标按钮 -->
<ImageButton
    android:layout_width="48dp"
    android:layout_height="48dp"
    android:src="@drawable/ic_close"
    android:background="?attr/selectableItemBackgroundBorderless"
    android:contentDescription="关闭"/>

ImageView - 图片

<ImageView
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:src="@drawable/logo"
    android:scaleType="centerCrop"
    android:contentDescription="应用logo"/>

// scaleType 常用值:
// centerCrop - 等比缩放裁剪填满
// fitCenter - 等比缩放居中
// centerInside - 原尺寸或缩小居中
// fitXY - 拉伸填满(会变形)

CheckBox / RadioButton / Switch

<CheckBox
    android:id="@+id/cbAgree"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="我同意用户协议"/>

<RadioGroup
    android:id="@+id/rgGender"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="horizontal">
    <RadioButton android:id="@+id/rbMale" android:text="男"/>
    <RadioButton android:id="@+id/rbFemale" android:text="女"/>
</RadioGroup>

<com.google.android.material.switchmaterial.SwitchMaterial
    android:id="@+id/swDarkMode"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="深色模式"/>

<com.google.android.material.chip.Chip
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="标签"
    app:closeIconVisible="true"
    app:chipIcon="@drawable/ic_tag"/>

// 监听
binding.cbAgree.setOnCheckedChangeListener { _, isChecked ->
    binding.btnSubmit.isEnabled = isChecked
}

ProgressBar - 进度条

<!-- 圆形加载 -->
<ProgressBar
    android:id="@+id/pbLoading"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

<!-- 线性进度条 -->
<ProgressBar
    style="?android:attr/progressBarStyleHorizontal"
    android:id="@+id/pbProgress"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:max="100"
    android:progress="60"/>

<!-- Material 线性进度 -->
<com.google.android.material.progressindicator.LinearProgressIndicator
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:indeterminate="true"/>

常用尺寸单位

单位说明使用场景
dp (dip)密度无关像素布局宽高、边距
sp缩放无关像素,跟随系统字体文字大小
px物理像素极少使用

💡

提示:

1dp 在 160dpi 屏幕上 = 1px,在 320dpi(xxhdpi)屏幕上 = 2px。

6. 常用布局方式

▼

LinearLayout - 线性布局

子元素按单行或单列排列,最基础的布局。

<LinearLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:padding="16dp"
    android:gravity="center_horizontal">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="标题"
        android:textStyle="bold"/>

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_weight="1"/>

    <LinearLayout
        android:orientation="horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <Button android:layout_weight="1" android:text="取消"
            android:layout_marginEnd="8dp"/>
        <Button android:layout_weight="1" android:text="确定"/>
    </LinearLayout>
</LinearLayout>

ConstraintLayout - 约束布局(推荐)

Android 官方推荐的现代布局,通过约束关系定位子元素:

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="16dp">

    <!-- 居中 -->
    <TextView
        android:id="@+id/tvTitle"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent"/>

    <!-- Guideline 辅助线 -->
    <androidx.constraintlayout.widget.Guideline
        android:id="@+id/guideline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_percent="0.5"/>

    <!-- Barrier 屏障(多个控件底部对齐) -->
    <androidx.constraintlayout.widget.Barrier
        android:id="@+id/barrier"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:barrierDirection="bottom"
        app:constraint_referenced_ids="tv1,tv2"/>

    <!-- 水平链 spread -->
    <Button
        android:id="@+id/btn1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintHorizontal_chainStyle="spread"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toStartOf="@id/btn2"/>

    <!-- Bias 偏移 -->
    <ImageView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintHorizontal_bias="0.3"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

RelativeLayout - 相对布局

<RelativeLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="16dp">

    <ImageView
        android:id="@+id/avatar"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:layout_alignParentStart="true"
        android:layout_centerVertical="true"/>

    <TextView
        android:id="@+id/name"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toEndOf="@id/avatar"
        android:layout_marginStart="12dp"
        android:layout_alignTop="@id/avatar"/>

    <TextView
        android:id="@+id/desc"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/name"
        android:layout_toEndOf="@id/avatar"
        android:layout_marginStart="12dp"/>
</RelativeLayout>

FrameLayout - 帧布局

子元素以堆叠方式显示,常用于 Fragment 容器:

<FrameLayout
    android:id="@+id/fragment_container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

<!-- 叠加效果 -->
<FrameLayout
    android:layout_width="200dp"
    android:layout_height="200dp">
    <ImageView android:src="@drawable/image"/>
    <TextView
        android:layout_gravity="bottom|end"
        android:text="NEW"
        android:background="#FF0000"
        android:textColor="#FFF"
        android:padding="4dp"/>
</FrameLayout>

ScrollView / HorizontalScrollView

<ScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fillViewport="true">

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <!-- 这里放置可滚动内容 -->
    </LinearLayout>
</ScrollView>

<!-- 嵌套滚动 -->
<androidx.core.widget.NestedScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_behavior="@string/appbar_scrolling_view_behavior">
    <!-- RecyclerView 等内容 -->
</androidx.core.widget.NestedScrollView>

CoordinatorLayout + AppBar

<androidx.coordinatorlayout.widget.CoordinatorLayout
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.google.android.material.appbar.AppBarLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <com.google.android.material.appbar.MaterialToolbar
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            app:title="首页"
            app:layout_scrollFlags="scroll|enterAlways"/>

        <com.google.android.material.tabs.TabLayout
            android:id="@+id/tabs"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
    </com.google.android.material.appbar.AppBarLayout>

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_behavior="@string/appbar_scrolling_view_behavior"/>

</androidx.coordinatorlayout.widget.CoordinatorLayout>

✅

性能建议:

避免多层布局嵌套(超过 3 层会显著影响性能),优先使用 ConstraintLayout 实现复杂布局。

7. RecyclerView 列表展示

▼

RecyclerView 基础

RecyclerView 是 ListView 的升级版,支持更高效的视图复用和灵活的布局管理。

1. 添加依赖

implementation 'androidx.recyclerview:recyclerview:1.3.2'

2. 布局文件

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recyclerView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clipToPadding="false"
    android:paddingBottom="80dp"/>

3. 列表项布局 item_user.xml

<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:padding="16dp"
    android:background="?attr/selectableItemBackground">

    <ImageView
        android:id="@+id/ivAvatar"
        android:layout_width="50dp"
        android:layout_height="50dp"/>

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:orientation="vertical"
        android:layout_marginStart="12dp">

        <TextView
            android:id="@+id/tvName"
            android:textSize="16sp"
            android:textStyle="bold"/>
        <TextView
            android:id="@+id/tvEmail"
            android:textColor="#666"
            android:textSize="14sp"/>
    </LinearLayout>
</LinearLayout>

4. ListAdapter(推荐,内置 DiffUtil)

class UserAdapter : ListAdapter<User, UserAdapter.UserViewHolder>(UserDiffCallback()) {

    class UserViewHolder(view: View) : RecyclerView.ViewHolder(view) {
        val tvName: TextView = view.findViewById(R.id.tvName)
        val tvEmail: TextView = view.findViewById(R.id.tvEmail)
        val ivAvatar: ImageView = view.findViewById(R.id.ivAvatar)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.item_user, parent, false)
        return UserViewHolder(view)
    }

    override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
        val user = getItem(position)
        holder.tvName.text = user.name
        holder.tvEmail.text = user.email
        Glide.with(holder.itemView).load(user.avatarUrl).into(holder.ivAvatar)

        holder.itemView.setOnClickListener {
            onItemClickListener?.invoke(user)
        }
    }

    var onItemClickListener: ((User) -> Unit)? = null
}

class UserDiffCallback : DiffUtil.ItemCallback<User>() {
    override fun areItemsTheSame(oldItem: User, newItem: User) = oldItem.id == newItem.id
    override fun areContentsTheSame(oldItem: User, newItem: User) = oldItem == newItem
}

data class User(val id: Long, val name: String, val email: String, val avatarUrl: String)

5. 在 Activity 中使用

val adapter = UserAdapter()
adapter.onItemClickListener = { user ->
    // 处理点击
    Toast.makeText(this, "点击了 ${user.name}", Toast.LENGTH_SHORT).show()
}

binding.recyclerView.apply {
    layoutManager = LinearLayoutManager(this@MainActivity)
    adapter = this@MainActivity.adapter
    addItemDecoration(DividerItemDecoration(context, DividerItemDecoration.VERTICAL))
    setHasFixedSize(true)
    // 设置 ItemAnimator
    itemAnimator = DefaultItemAnimator()
}

// 提交数据(自动计算差异并刷新)
adapter.submitList(users)

多种 ViewType

override fun getItemViewType(position: Int): Int {
    return if (getItem(position).isHeader) TYPE_HEADER else TYPE_ITEM
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    return when (viewType) {
        TYPE_HEADER -> HeaderViewHolder(...)
        TYPE_ITEM -> ItemViewHolder(...)
        else -> throw IllegalArgumentException()
    }
}

常用 LayoutManager

类型说明
LinearLayoutManager纵向/横向列表
GridLayoutManager网格布局,可设置 spanCount
StaggeredGridLayoutManager瀑布流布局

SwipeRefreshLayout 下拉刷新

<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
    android:id="@+id/swipeRefresh"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

// 监听
binding.swipeRefresh.setOnRefreshListener {
    viewModel.refresh()
}

// 完成刷新
binding.swipeRefresh.isRefreshing = false

ItemTouchHelper 滑动删除/拖拽排序

val itemTouchHelper = ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(
    ItemTouchHelper.UP or ItemTouchHelper.DOWN,  // 拖拽方向
    ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT  // 滑动方向
) {
    override fun onMove(rv: RecyclerView, vh: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
        adapter.moveItem(vh.adapterPosition, target.adapterPosition)
        return true
    }

    override fun onSwiped(vh: RecyclerView.ViewHolder, direction: Int) {
        adapter.removeItem(vh.adapterPosition)
    }
})
itemTouchHelper.attachToRecyclerView(binding.recyclerView)

💡

推荐:

使用

ListAdapter

(内置 DiffUtil)可以简化列表刷新逻辑,性能更好。

8. Material Design 设计规范

▼

Material Design 3 概述

Material Design 3(Material You)是 Google 最新的设计系统,强调个性化(动态取色)、可访问性和一致性。

主题配置

<!-- res/values/themes.xml -->
<resources>
    <style name="Theme.MyApp" parent="Theme.Material3.DayNight.NoActionBar">
        <item name="colorPrimary">@color/purple_500</item>
        <item name="colorOnPrimary">@color/white</item>
        <item name="colorSecondary">@color/teal_200</item>
        <item name="colorError">@color/red_500</item>
        <item name="android:statusBarColor">?attr/colorPrimary</item>
    </style>
</resources>

// 启用动态取色(Android 12+)
// Application onCreate()
DynamicColors.applyToActivitiesIfAvailable(this)

常用 Material 组件

AppBar / Toolbar

<com.google.android.material.appbar.MaterialToolbar
    android:id="@+id/toolbar"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    app:title="首页"
    app:navigationIcon="@drawable/ic_menu"
    app:menu="@menu/main_menu"/>

// 代码配置
setSupportActionBar(binding.toolbar)
binding.toolbar.setNavigationOnClickListener { finish() }
binding.toolbar.setOnMenuItemClickListener { menuItem ->
    when (menuItem.itemId) {
        R.id.action_settings -> { true }
        else -> false
    }
}

FloatingActionButton

<com.google.android.material.floatingactionbutton.FloatingActionButton
    android:id="@+id/fab"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom|end"
    android:layout_margin="16dp"
    app:srcCompat="@drawable/ic_add"
    android:contentDescription="新增"/>

<!-- 扩展 FAB -->
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="新建"
    app:icon="@drawable/ic_add"/>

CardView - 卡片

<com.google.android.material.card.MaterialCardView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"
    app:cardCornerRadius="12dp"
    app:cardElevation="4dp"
    app:strokeColor="@color/gray_200"
    app:strokeWidth="1dp"
    android:clickable="true"
    android:focusable="true">

    <LinearLayout
        android:padding="16dp"
        android:orientation="vertical">
        <TextView android:text="标题" android:textSize="18sp"/>
        <TextView android:text="描述" android:textColor="#666"/>
    </LinearLayout>
</com.google.android.material.card.MaterialCardView>

BottomNavigationView - 底部导航

<com.google.android.material.bottomnavigation.BottomNavigationView
    android:id="@+id/bottomNav"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:menu="@menu/bottom_nav_menu"
    app:labelVisibilityMode="labeled"/>

// 监听选择
binding.bottomNav.setOnItemSelectedListener { item ->
    when (item.itemId) {
        R.id.nav_home -> { loadFragment(HomeFragment()); true }
        R.id.nav_search -> { loadFragment(SearchFragment()); true }
        R.id.nav_profile -> { loadFragment(ProfileFragment()); true }
        else -> false
    }
}

Snackbar - 提示条

Snackbar.make(view, "已删除 1 项", Snackbar.LENGTH_LONG)
    .setAction("撤销") {
        // 撤销逻辑
    }
    .setAnchorView(binding.fab)  // 显示在 FAB 上方
    .show()

Material Dialog

MaterialAlertDialogBuilder(this)
    .setTitle("确认删除")
    .setMessage("确定要删除这条数据吗?")
    .setIcon(R.drawable.ic_warning)
    .setPositiveButton("删除") { _, _ -> deleteItem() }
    .setNegativeButton("取消", null)
    .setCancelable(false)
    .show()

// 单选对话框
val items = arrayOf("选项 A", "选项 B", "选项 C")
MaterialAlertDialogBuilder(this)
    .setTitle("请选择")
    .setItems(items) { _, which ->
        Toast.makeText(this, items[which], Toast.LENGTH_SHORT).show()
    }
    .show()

// 多选对话框
val checkedItems = booleanArrayOf(false, true, false)
MaterialAlertDialogBuilder(this)
    .setTitle("多选")
    .setMultiChoiceItems(items, checkedItems) { _, which, isChecked ->
        checkedItems[which] = isChecked
    }
    .setPositiveButton("确定", null)
    .show()

BottomSheet - 底部弹窗

<!-- Modal BottomSheet -->
class MyBottomSheet : BottomSheetDialogFragment() {
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        return inflater.inflate(R.layout.dialog_bottom_sheet, container, false)
    }
}

// 显示
MyBottomSheet().show(supportFragmentManager, "sheet")

// 固定 BottomSheet(Behavior)
val behavior = BottomSheetBehavior.from(bottomSheetView)
behavior.state = BottomSheetBehavior.STATE_EXPANDED
behavior.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
    override fun onStateChanged(sheet: View, newState: Int) {}
    override fun onSlide(sheet: View, slideOffset: Float) {}
})

9. Activity 生命周期

▼

生命周期回调方法

Activity 从创建到销毁会经历一系列回调:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Activity 创建时调用,初始化布局和数据
        setContentView(R.layout.activity_main)
        // savedInstanceState 非空表示从之前状态恢复
    }

    override fun onStart() {
        super.onStart()
        // Activity 可见时调用
    }

    override fun onResume() {
        super.onResume()
        // Activity 获得焦点、可交互时调用
        // 适合恢复动画、注册监听
    }

    override fun onPause() {
        super.onPause()
        // Activity 失去焦点时调用
        // 保存临时数据、释放摄像头等资源
    }

    override fun onStop() {
        super.onStop()
        // Activity 不可见时调用
    }

    override fun onDestroy() {
        super.onDestroy()
        // Activity 即将销毁,释放资源
    }

    override fun onRestart() {
        super.onRestart()
        // 从停止状态重新启动
    }
}

生命周期场景

场景回调顺序
启动 ActivityonCreate → onStart → onResume
按 Home 键onPause → onStop
从后台返回onRestart → onStart → onResume
按返回键onPause → onStop → onDestroy
屏幕旋转onPause → onStop → onDestroy → onCreate → onStart → onResume
弹出对话框onPause(Activity 仍可见但不可交互)
A 启动 B,B 返回到 AA.onPause → B.onCreate → B.onStart → B.onResume → A.onStop → B.onPause → A.onRestart → A.onStart → A.onResume → B.onStop → B.onDestroy

保存与恢复状态

override fun onSaveInstanceState(outState: Bundle) {
    super.onSaveInstanceState(outState)
    outState.putInt("counter", counter)
    outState.putString("input", binding.etInput.text.toString())
    outState.putParcelable("user", currentUser)
}

override fun onRestoreInstanceState(savedInstanceState: Bundle) {
    super.onRestoreInstanceState(savedInstanceState)
    counter = savedInstanceState.getInt("counter", 0)
    binding.etInput.setText(savedInstanceState.getString("input"))
}

// 也可以在 onCreate 中恢复
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    savedInstanceState?.let {
        counter = it.getInt("counter", 0)
    }
}

Activity 启动模式 (launchMode)

模式说明使用场景
standard默认,每次创建新实例大部分 Activity
singleTop栈顶存在则复用(onNewIntent)通知打开的页面
singleTask任务栈中唯一实例,清除上方所有主页、登录
singleInstance独占一个任务栈通话界面
// Manifest 中配置
<activity
    android:name=".MainActivity"
    android:launchMode="singleTask"
    android:taskAffinity=""/>

// 代码中设置 Flag
val intent = Intent(this, MainActivity::class.java).apply {
    flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
}
startActivity(intent)

⚠️

注意:

屏幕旋转默认会重建 Activity,导致状态丢失。可使用

ViewModel

保存 UI 状态,或在 Manifest 中配置

android:configChanges="orientation|screenSize"

。

10. Fragment 碎片

▼

什么是 Fragment

Fragment 是 Activity 中可复用的 UI 模块,拥有自己的生命周期。一个 Activity 可以包含多个 Fragment,常用于平板、导航等场景。

Fragment 生命周期

onAttach() → onCreate() → onCreateView() → onViewCreated() →
onViewCreated() → onStart() → onResume() → onPause() → onStop() →
onDestroyView() → onDestroy() → onDetach()

创建 Fragment

class HomeFragment : Fragment(R.layout.fragment_home) {

    private var _binding: FragmentHomeBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentHomeBinding.inflate(inflater, container, false)
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding.btnAction.setOnClickListener {
            // 处理点击
        }

        // 观察 ViewModel
        viewLifecycleOwner.lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                viewModel.data.collect { updateUI(it) }
            }
        }
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null  // 避免内存泄漏
    }
}

FragmentManager 管理

// 添加 Fragment
supportFragmentManager.beginTransaction()
    .add(R.id.fragment_container, HomeFragment(), "home")
    .addToBackStack("home")
    .commit()

// 替换 Fragment
supportFragmentManager.beginTransaction()
    .replace(R.id.fragment_container, DetailFragment())
    .addToBackStack("detail")
    .setTransition(FragmentTransaction.TRANSIT_FRAGMENT_FADE)
    .commit()

// 返回上一个 Fragment
supportFragmentManager.popBackStack()
supportFragmentManager.popBackStack("home", FragmentManager.POP_BACK_STACK_INCLUSIVE)

// 查找 Fragment
val fragment = supportFragmentManager.findFragmentByTag("home")
val fragmentById = supportFragmentManager.findFragmentById(R.id.fragment_container)

Fragment 与 Activity 通信

// 方式1:ViewModel 共享(推荐)
class SharedViewModel : ViewModel() {
    val selected = MutableLiveData<Item>()
}

// Fragment 中获取 Activity 级别的 ViewModel
private val model: SharedViewModel by activityViewModels()

// 方式2:Fragment Result API
// Fragment A 发送
setFragmentResult("requestKey", bundleOf("data" to "hello"))

// Fragment B 接收
setFragmentResultListener("requestKey") { key, bundle ->
    val data = bundle.getString("data")
}

// 方式3:接口回调(旧方式,不推荐)

Navigation Component

implementation 'androidx.navigation:navigation-fragment-ktx:2.7.6'
implementation 'androidx.navigation:navigation-ui-ktx:2.7.6'

// 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">

    <fragment
        android:id="@+id/homeFragment"
        android:name="com.example.HomeFragment"
        android:label="首页">
        <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"/>
    </fragment>

    <fragment
        android:id="@+id/detailFragment"
        android:name="com.example.DetailFragment">
        <argument
            android:name="itemId"
            app:argType="long"/>
        <argument
            android:name="title"
            app:argType="string"
            app:nullable="true"
            android:defaultValue="@null"/>
    </fragment>
</navigation>

// 导航跳转(Safe Args 插件生成方向类)
val action = HomeFragmentDirections.actionHomeToDetail(itemId = 123L)
findNavController().navigate(action)

// 接收参数
val args: DetailFragmentArgs by navArgs()
val itemId = args.itemId

11. Intent 与页面跳转

▼

Intent 类型

  • 显式 Intent:明确指定目标组件(Activity/Service)
  • 隐式 Intent:描述要执行的动作,由系统匹配合适的组件

显式跳转 Activity

// 基础跳转
val intent = Intent(this, DetailActivity::class.java)
startActivity(intent)

// 携带数据
val intent = Intent(this, DetailActivity::class.java).apply {
    putExtra("user_id", 123)
    putExtra("user_name", "张三")
    putExtra("is_vip", true)
}
startActivity(intent)

// 传递 Bundle
val bundle = Bundle().apply {
    putLong("id", 123L)
    putString("name", "张三")
}
intent.putExtras(bundle)

// 传递复杂对象(需实现 Parcelable)
@Parcelize
data class User(val id: Long, val name: String) : Parcelable

val intent = Intent(this, DetailActivity::class.java).apply {
    putExtra("user", user)  // Parcelable
    putParcelableArrayListExtra("users", ArrayList(userList))
}
startActivity(intent)

接收数据

// DetailActivity 中
val userId = intent.getLongExtra("user_id", 0)
val userName = intent.getStringExtra("user_name")
val user = intent.getParcelableExtra<User>("user", User::class.java)
val users = intent.getParcelableArrayListExtra<User>("users")

// Bundle 方式
val bundle = intent.extras
val name = bundle?.getString("name")

隐式 Intent 常见动作

// 打开网页
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://google.com"))
startActivity(intent)

// 拨打电话(需权限 CALL_PHONE,推荐用 DIAL)
val intent = Intent(Intent.ACTION_DIAL, Uri.parse("tel:10086"))
startActivity(intent)

// 发送短信
val intent = Intent(Intent.ACTION_SENDTO, Uri.parse("smsto:10086")).apply {
    putExtra("sms_body", "短信内容")
}
startActivity(intent)

// 发送邮件
val intent = Intent(Intent.ACTION_SENDTO).apply {
    data = Uri.parse("mailto:test@example.com")
    putExtra(Intent.EXTRA_SUBJECT, "主题")
    putExtra(Intent.EXTRA_TEXT, "正文")
    putExtra(Intent.EXTRA_STREAM, fileUri)  // 附件
}
startActivity(intent)

// 分享内容
val intent = Intent(Intent.ACTION_SEND).apply {
    type = "text/plain"
    putExtra(Intent.EXTRA_TEXT, "分享内容")
}
startActivity(Intent.createChooser(intent, "分享到"))

// 打开地图
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("geo:39.9,116.3?q=北京"))
startActivity(intent)

// 设置闹钟
val intent = Intent(AlarmClock.ACTION_SET_ALARM).apply {
    putExtra(AlarmClock.EXTRA_HOUR, 8)
    putExtra(AlarmClock.EXTRA_MINUTES, 30)
    putExtra(AlarmClock.EXTRA_MESSAGE, "起床")
}
startActivity(intent)

// 选择联系人
val intent = Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI)
startActivity(intent)

Activity Result API(推荐)

// 自定义结果
private val launcher = registerForActivityResult(
    ActivityResultContracts.StartActivityForResult()
) { result ->
    if (result.resultCode == Activity.RESULT_OK) {
        val data = result.data?.getStringExtra("result")
    }
}
launcher.launch(Intent(this, SecondActivity::class.java))

// 拍照
private val cameraLauncher = registerForActivityResult(
    ActivityResultContracts.TakePicturePreview()
) { bitmap ->
    binding.imageView.setImageBitmap(bitmap)
}

// 选择图片
private val pickImageLauncher = registerForActivityResult(
    ActivityResultContracts.PickVisualMedia()
) { uri ->
    uri?.let { binding.imageView.setImageURI(it) }
}
pickImageLauncher.launch(PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly))

// 选择文件
private val fileLauncher = registerForActivityResult(
    ActivityResultContracts.GetContent()
) { uri -> /* 处理文件 */ }
fileLauncher.launch("application/pdf")

权限请求

private val permissionLauncher = registerForActivityResult(
    ActivityResultContracts.RequestPermission()
) { granted ->
    if (granted) {
        openCamera()
    } else {
        // 权限被拒绝
        if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
            // 用户选择了"拒绝并不再询问"以外的拒绝
            showRationaleDialog()
        }
    }
}

// 请求
permissionLauncher.launch(Manifest.permission.CAMERA)

// 批量请求
private val multiPermLauncher = registerForActivityResult(
    ActivityResultContracts.RequestMultiplePermissions()
) { result ->
    val allGranted = result.values.all { it }
}
multiPermLauncher.launch(arrayOf(
    Manifest.permission.CAMERA,
    Manifest.permission.RECORD_AUDIO
))

12. Service 后台服务

▼

Service 类型

  • 前台服务:显示通知,用户可见(如音乐播放、下载)
  • 后台服务:不可见,Android 8.0+ 受严格限制
  • 绑定服务:提供客户端-服务器接口供组件绑定

创建 Service

class MyService : Service() {
    override fun onBind(intent: Intent): IBinder? = null

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // 执行后台任务
        doBackgroundWork()
        // START_STICKY:被杀后重启
        // START_NOT_STICKY:被杀后不重启
        // START_REDELIVER_INTENT:被杀后重启并传递最后一个 Intent
        return START_STICKY
    }

    private fun doBackgroundWork() {
        // 在子线程中执行耗时操作
        Thread {
            // 任务逻辑
        }.start()
    }

    override fun onDestroy() {
        super.onDestroy()
        // 释放资源
    }
}

启动与停止 Service

// 启动
val intent = Intent(this, MyService::class.java)
ContextCompat.startForegroundService(this, intent)  // 前台服务(需 5 秒内调 startForeground)

// Android 12+ 限制,必须声明 foregroundServiceType
// <service android:foregroundServiceType="dataSync|location|mediaPlayback"/>

// 普通启动
startService(intent)

// 停止
stopService(Intent(this, MyService::class.java))
// 或在 Service 内部:stopSelf()

前台服务通知

private fun startForeground() {
    createNotificationChannel()

    val pendingIntent = PendingIntent.getActivity(
        this, 0, Intent(this, MainActivity::class.java),
        PendingIntent.FLAG_IMMUTABLE
    )

    val notification = NotificationCompat.Builder(this, CHANNEL_ID)
        .setContentTitle("正在播放")
        .setContentText("歌曲名 - 歌手")
        .setSmallIcon(R.drawable.ic_music)
        .setContentIntent(pendingIntent)
        .setOngoing(true)
        .addAction(R.drawable.ic_pause, "暂停", pausePendingIntent)
        .setStyle(androidx.media.app.NotificationCompat.MediaStyle()
            .setShowActionsInCompactView(0))
        .build()

    startForeground(NOTIFICATION_ID, notification)
}

private fun createNotificationChannel() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
        val channel = NotificationChannel(
            CHANNEL_ID,
            "音乐服务",
            NotificationManager.IMPORTANCE_LOW
        ).apply {
            description = "音乐播放通知"
            setShowBadge(false)
        }
        val manager = getSystemService(NotificationManager::class.java)
        manager.createNotificationChannel(channel)
    }
}

绑定服务

class BoundService : Service() {
    private val binder = LocalBinder()

    inner class LocalBinder : Binder() {
        fun getService(): BoundService = this@BoundService
    }

    override fun onBind(intent: Intent): IBinder = binder

    fun getData(): String = "Hello from Service"
}

// Activity 中绑定
private var service: BoundService? = null
private var bound = false

private val connection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName, binder: IBinder) {
        val localBinder = binder as BoundService.LocalBinder
        service = localBinder.getService()
        bound = true
    }
    override fun onServiceDisconnected(name: ComponentName) {
        bound = false
    }
}

override fun onStart() {
    super.onStart()
    Intent(this, BoundService::class.java).also {
        bindService(it, connection, Context.BIND_AUTO_CREATE)
    }
}

override fun onStop() {
    super.onStop()
    if (bound) {
        unbindService(connection)
        bound = false
    }
}

推荐:WorkManager

对于可延迟的后台任务(如数据同步、备份),推荐使用 WorkManager:

implementation 'androidx.work:work-runtime-ktx:2.9.0'

class SyncWorker(ctx: Context, params: WorkerParameters) :
    CoroutineWorker(ctx, params) {

    override suspend fun doWork(): Result {
        return try {
            val data = inputData.getString("url") ?: return Result.failure()
            // 执行同步逻辑
            val api = RetrofitClient.api
            val response = api.syncData(data)
            if (response.isSuccessful) {
                val output = workDataOf("result" to "success")
                Result.success(output)
            } else {
                Result.retry()  // 稍后重试
            }
        } catch (e: Exception) {
            Result.failure()
        }
    }
}

// 调度一次性任务
val request = OneTimeWorkRequestBuilder<SyncWorker>()
    .setInputData(workDataOf("url" to "https://api.example.com"))
    .setConstraints(
        Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .setRequiresBatteryNotLow(true)
            .build()
    )
    .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
    .setInitialDelay(10, TimeUnit.MINUTES)
    .addTag("sync_work")
    .build()

WorkManager.getInstance(context).enqueueUniqueWork(
    "sync",
    ExistingWorkPolicy.KEEP,
    request
)

// 周期性任务
val periodicRequest = PeriodicWorkRequestBuilder<SyncWorker>(
    15, TimeUnit.MINUTES  // 最小 15 分钟
).build()

// 观察状态
WorkManager.getInstance(context)
    .getWorkInfoByIdLiveData(request.id)
    .observe(this) { workInfo ->
        when (workInfo?.state) {
            WorkInfo.State.SUCCEEDED -> { /* 完成 */ }
            WorkInfo.State.FAILED -> { /* 失败 */ }
            else -> {}
        }
    }

// 取消任务
WorkManager.getInstance(context).cancelAllWork()
WorkManager.getInstance(context).cancelWorkById(request.id)
WorkManager.getInstance(context).cancelAllWorkByTag("sync_work")

⚠️

Android 12+ 限制:

前台服务启动受限,需在 Manifest 中声明

android:foregroundServiceType

,且必须从可见界面启动。

13. BroadcastReceiver 广播

▼

广播类型

  • 标准广播:异步发送,所有接收器同时收到
  • 有序广播:同步依次传递,可被截断、修改
  • 本地广播:只在应用内传播,更安全高效
  • 粘性广播:已废弃(API 28+),不建议使用

接收系统广播

class BootReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
            // 开机后执行的逻辑
            val workRequest = OneTimeWorkRequestBuilder<StartupWorker>().build()
            WorkManager.getInstance(context).enqueue(workRequest)
        }
    }
}

// 注册到 Manifest
<receiver
    android:name=".BootReceiver"
    android:exported="true"
    android:directBootAware="true">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
        <action android:name="android.intent.action.LOCKED_BOOT_COMPLETED"/>
    </intent-filter>
</receiver>

<!-- 需要权限 -->
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

动态注册广播

private lateinit var receiver: BroadcastReceiver

override fun onResume() {
    super.onResume()
    receiver = object : BroadcastReceiver() {
        override fun onReceive(context: Context?, intent: Intent?) {
            when (intent?.action) {
                Intent.ACTION_AIRPLANE_MODE_CHANGED -> {
                    val isAirplaneMode = intent.getBooleanExtra("state", false)
                    Log.d("TAG", "飞行模式: $isAirplaneMode")
                }
                Intent.ACTION_BATTERY_LOW -> {
                    // 低电量
                }
                ConnectivityManager.CONNECTIVITY_ACTION -> {
                    checkNetwork()
                }
            }
        }
    }

    val filter = IntentFilter().apply {
        addAction(Intent.ACTION_AIRPLANE_MODE_CHANGED)
        addAction(Intent.ACTION_BATTERY_LOW)
    }
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
        registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED)
    } else {
        registerReceiver(receiver, filter)
    }
}

override fun onPause() {
    super.onPause()
    unregisterReceiver(receiver)
}

发送自定义广播

// 标准广播
val intent = Intent("com.example.MY_ACTION").apply {
    putExtra("message", "hello")
    setPackage(packageName)  // Android 8.0+ 必须指定包名
}
sendBroadcast(intent)

// 有序广播
sendOrderedBroadcast(intent, null, object : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        val finalResult = resultCode
        Log.d("TAG", "最终结果: $finalResult")
    }
}, null, Activity.RESULT_OK, null, null)

// 接收器中处理有序广播
class MyOrderedReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context?, intent: Intent?) {
        if (intent?.action == "com.example.MY_ACTION") {
            // 可以修改结果
            resultCode = 100
            // 可以中断传递
            abortBroadcast()
        }
    }
}

LocalBroadcastManager(本地广播)

// 推荐使用 Flow/EventBus 替代,更现代化
// 旧方式:
val manager = LocalBroadcastManager.getInstance(this)

// 发送
val intent = Intent("LOCAL_ACTION").apply {
    putExtra("data", "hello")
}
manager.sendBroadcast(intent)

// 注册
manager.registerReceiver(receiver, IntentFilter("LOCAL_ACTION"))

// 取消注册
manager.unregisterReceiver(receiver)

现代替代方案:Flow / Channel

// 单例事件总线
object EventBus {
    private val _events = MutableSharedFlow<AppEvent>()
    val events = _events.asSharedFlow()

    suspend fun emit(event: AppEvent) {
        _events.emit(event)
    }
}

sealed class AppEvent {
    data class ShowToast(val message: String) : AppEvent()
    data object RefreshList : AppEvent()
}

// 发送
lifecycleScope.launch {
    EventBus.emit(AppEvent.ShowToast("操作成功"))
}

// 接收
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        EventBus.events.collect { event ->
            when (event) {
                is AppEvent.ShowToast -> Toast.makeText(this@MainActivity, event.message, Toast.LENGTH_SHORT).show()
                is AppEvent.RefreshList -> loadData()
            }
        }
    }
}

💡

Android 7.0+ 限制:

大部分隐式系统广播不再在 Manifest 中注册接收,需使用动态注册或 JobScheduler。

14. 数据存储方案

▼

Android 数据存储对比

方案适用场景特点
SharedPreferences键值对、简单配置XML 存储、简单易用
DataStore替代 SharedPreferences协程友好、异步、类型安全
Room 数据库结构化数据、复杂查询SQLite ORM、类型安全
文件存储图片、文件、缓存内部/外部存储
ContentProvider跨应用共享数据标准化接口
网络云端数据REST API、GraphQL

SharedPreferences 示例

// 写入
val prefs = getSharedPreferences("settings", Context.MODE_PRIVATE)
prefs.edit {
    putString("username", "张三")
    putInt("age", 25)
    putBoolean("isVip", true)
    putStringSet("tags", setOf("kotlin", "android"))
    apply()  // apply() 异步,commit() 同步
}

// 读取
val username = prefs.getString("username", "")
val age = prefs.getInt("age", 0)

// 监听变化
prefs.registerOnSharedPreferenceChangeListener { prefs, key ->
    when (key) {
        "username" -> { /* 用户名变化 */ }
    }
}

// Kotlin 便捷写法(推荐,使用 KTX)
val Context.settingsPrefs by preferences("settings")
var username by settingsPrefs.stringPreferences("username", "")
var age by settingsPrefs.intPreferences("age", 0)

DataStore(推荐替代 SharedPreferences)

implementation 'androidx.datastore:datastore-preferences:1.0.0'

// Preferences DataStore
val Context.dataStore by preferencesDataStore(name = "settings")

class SettingsRepository(private val context: Context) {
    private object Keys {
        val USERNAME = stringPreferencesKey("username")
        val AGE = intPreferencesKey("age")
        val DARK_MODE = booleanPreferencesKey("dark_mode")
    }

    val settings: Flow<Settings> = context.dataStore.data.map { prefs ->
        Settings(
            username = prefs[Keys.USERNAME] ?: "",
            age = prefs[Keys.AGE] ?: 0,
            darkMode = prefs[Keys.DARK_MODE] ?: false
        )
    }

    suspend fun updateUsername(name: String) {
        context.dataStore.edit { prefs ->
            prefs[Keys.USERNAME] = name
        }
    }

    suspend fun updateSettings(settings: Settings) {
        context.dataStore.edit { prefs ->
            prefs[Keys.USERNAME] = settings.username
            prefs[Keys.AGE] = settings.age
            prefs[Keys.DARK_MODE] = settings.darkMode
        }
    }
}

// Proto DataStore(更类型安全)
val Context.userProtoDataStore by dataStore(
    fileName = "user.pb",
    serializer = UserSerializer
)

文件存储

// 写入内部存储(私有,卸载时删除)
val file = File(filesDir, "data.txt")
file.writeText("Hello File")
file.appendText("\n新内容")

// 读取
val content = file.readText()
val lines = file.readLines()

// 缓存目录(系统空间不足时自动清理)
val cacheFile = File(cacheDir, "temp.json")
cacheFile.writeText("""{"key":"value"}""")
// 定期清理缓存
cacheDir.deleteRecursively()

// 外部存储(私有目录,无需权限)
val externalFile = File(getExternalFilesDir(null), "notes.txt")
externalFile.writeText("外部文件内容")

// 下载目录
val downloadFile = File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS), "file.pdf")

// Assets 目录(只读资源)
val assetContent = assets.open("config.json").bufferedReader().use { it.readText() }

Scoped Storage(分区存储)

Android 10+ 引入分区存储,应用只能访问自己的私有目录和公共媒体文件:

// 保存照片到公共 Pictures 目录
fun saveImageToGallery(context: Context, bitmap: Bitmap): Uri? {
    val values = ContentValues().apply {
        put(MediaStore.Images.Media.DISPLAY_NAME, "photo_${System.currentTimeMillis()}.jpg")
        put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
        put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/MyApp")
        put(MediaStore.Images.Media.IS_PENDING, 1)
    }

    val uri = context.contentResolver.insert(
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
        values
    )

    uri?.let {
        context.contentResolver.openOutputStream(it)?.use { stream ->
            bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream)
        }
        values.clear()
        values.put(MediaStore.Images.Media.IS_PENDING, 0)
        context.contentResolver.update(it, values, null, null)
    }
    return uri
}

// 查询媒体文件
fun queryImages(context: Context): List<ImageItem> {
    val projection = arrayOf(
        MediaStore.Images.Media._ID,
        MediaStore.Images.Media.DISPLAY_NAME,
        MediaStore.Images.Media.DATE_ADDED
    )
    val cursor = context.contentResolver.query(
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
        projection,
        null, null,
        "${MediaStore.Images.Media.DATE_ADDED} DESC"
    )

    val images = mutableListOf<ImageItem>()
    cursor?.use {
        while (it.moveToNext()) {
            val id = it.getLong(it.getColumnIndexOrThrow(MediaStore.Images.Media._ID))
            val name = it.getString(it.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME))
            val contentUri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id)
            images.add(ImageItem(id, name, contentUri))
        }
    }
    return images
}

15. Room 数据库

▼

Room 简介

Room 是 Google 提供的 SQLite 抽象层,提供编译时校验、与 LiveData/Flow 集成、协程支持。

添加依赖

implementation 'androidx.room:room-runtime:2.6.1'
implementation 'androidx.room:room-ktx:2.6.1'
ksp 'androidx.room:room-compiler:2.6.1'  // 推荐用 KSP 替代 kapt

1. 定义 Entity(实体类)

@Entity(
    tableName = "users",
    indices = [Index(value = ["email"], unique = true)]
)
data class User(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    @ColumnInfo(name = "user_name") val name: String,
    val age: Int,
    val email: String,
    @ColumnInfo(defaultValue = "0") val isActive: Int = 1,
    val createdAt: Long = System.currentTimeMillis()
)

// 一对多关系
@Entity(
    tableName = "orders",
    foreignKeys = [
        ForeignKey(
            entity = User::class,
            parentColumns = ["id"],
            childColumns = ["user_id"],
            onDelete = ForeignKey.CASCADE
        )
    ]
)
data class Order(
    @PrimaryKey val id: Long,
    @ColumnInfo(name = "user_id") val userId: Long,
    val amount: Double,
    @ColumnInfo(defaultValue = "pending") val status: String
)

// 嵌入关系
@Entity
data class UserWithAddress(
    @Embedded val user: User,
    @Embedded(prefix = "addr_") val address: Address
)

// 多对多关系
@Entity(primaryKeys = ["userId", "groupId"])
data class UserGroupCrossRef(
    val userId: Long,
    val groupId: Long
)

2. 定义 DAO(数据访问对象)

@Dao
interface UserDao {
    // 查询全部
    @Query("SELECT * FROM users ORDER BY name ASC")
    fun getAllUsers(): Flow<List<User>>

    // 按 ID 查询
    @Query("SELECT * FROM users WHERE id = :userId")
    suspend fun getUserById(userId: Long): User?

    // 条件查询
    @Query("SELECT * FROM users WHERE age BETWEEN :minAge AND :maxAge")
    suspend fun getUsersByAgeRange(minAge: Int, maxAge: Int): List<User>

    // 返回 LiveData
    @Query("SELECT * FROM users WHERE isActive = 1")
    fun getActiveUsers(): LiveData<List<User>>

    // 插入
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUser(user: User): Long

    @Insert
    suspend fun insertUsers(users: List<User>): List<Long>

    // 更新
    @Update
    suspend fun updateUser(user: User): Int

    // 删除
    @Delete
    suspend fun deleteUser(user: User): Int

    @Query("DELETE FROM users WHERE id = :id")
    suspend fun deleteById(id: Long): Int

    @Query("DELETE FROM users")
    suspend fun deleteAll()

    // 搜索
    @Query("SELECT * FROM users WHERE user_name LIKE '%' || :keyword || '%'")
    fun searchUsers(keyword: String): Flow<List<User>>

    // 聚合查询
    @Query("SELECT COUNT(*) FROM users")
    suspend fun getUserCount(): Int

    @Query("SELECT AVG(age) FROM users")
    suspend fun getAverageAge(): Double

    // 关系查询
    @Transaction
    @Query("SELECT * FROM users WHERE id = :userId")
    suspend fun getUserWithOrders(userId: Long): UserWithOrders

    // 原生 SQL
    @RawQuery(observedEntities = [User::class])
    fun searchUsersRaw(query: SupportSQLiteQuery): Flow<List<User>>
}

// 关系数据类
data class UserWithOrders(
    @Embedded val user: User,
    @Relation(
        parentColumn = "id",
        entityColumn = "user_id"
    )
    val orders: List<Order>
)

3. 定义 Database

@Database(
    entities = [User::class, Order::class, UserGroupCrossRef::class],
    version = 2,
    exportSchema = true
)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
    abstract fun orderDao(): OrderDao

    companion object {
        @Volatile
        private var INSTANCE: AppDatabase? = null

        fun getDatabase(context: Context): AppDatabase {
            return INSTANCE ?: synchronized(this) {
                val instance = Room.databaseBuilder(
                    context.applicationContext,
                    AppDatabase::class.java,
                    "app_database"
                )
                .addMigrations(MIGRATION_1_2)
                .addCallback(object : Callback() {
                    override fun onCreate(db: SupportSQLiteDatabase) {
                        // 数据库首次创建时执行
                    }
                    override fun onOpen(db: SupportSQLiteDatabase) {
                        // 数据库打开时执行
                    }
                })
                .fallbackToDestructiveMigration()  // 开发时使用
                .build()
                INSTANCE = instance
                instance
            }
        }
    }
}

// 类型转换器
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> =
        value?.split(",") ?: emptyList()

    @TypeConverter
    fun toStringList(list: List<String>?): String =
        list?.joinToString(",") ?: ""
}

4. 在 Repository 中使用

class UserRepository @Inject constructor(
    private val userDao: UserDao
) {
    val allUsers: Flow<List<User>> = userDao.getAllUsers()

    suspend fun insert(user: User) = userDao.insertUser(user)
    suspend fun delete(user: User) = userDao.deleteUser(user)
    suspend fun getById(id: Long) = userDao.getUserById(id)
    fun search(keyword: String) = userDao.searchUsers(keyword)
}

// ViewModel
@HiltViewModel
class UserViewModel @Inject constructor(
    private val repo: UserRepository
) : ViewModel() {

    val users = repo.allUsers.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000),
        initialValue = emptyList()
    )

    fun addUser(name: String, age: Int) {
        viewModelScope.launch {
            try {
                repo.insert(User(name = name, age = age))
            } catch (e: Exception) {
                // 处理错误
            }
        }
    }
}

数据库升级

val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(db: SupportSQLiteDatabase) {
        db.execSQL("ALTER TABLE users ADD COLUMN phone TEXT")
        db.execSQL("CREATE INDEX index_users_email ON users(email)")
    }
}

val MIGRATION_2_3 = object : Migration(2, 3) {
    override fun migrate(db: SupportSQLiteDatabase) {
        // 复杂迁移:创建新表、迁移数据、删除旧表
        db.execSQL("CREATE TABLE users_new (id INTEGER PRIMARY KEY, name TEXT NOT NULL, email TEXT UNIQUE)")
        db.execSQL("INSERT INTO users_new (id, name, email) SELECT id, user_name, email FROM users")
        db.execSQL("DROP TABLE users")
        db.execSQL("ALTER TABLE users_new RENAME TO users")
    }
}

// 自动迁移(简单字段增删)
@Database(
    entities = [User::class],
    version = 3,
    autoMigrations = [
        AutoMigration(from = 2, to = 3)
    ]
)
abstract class AppDatabase : RoomDatabase() { ... }

16. 网络请求

▼

常用网络库

  • Retrofit:类型安全的 REST 客户端(推荐)
  • OkHttp:底层 HTTP 客户端
  • Ktor Client:Kotlin 跨平台网络库
  • Volley:轻量级请求库(Google 出品)

Retrofit 完整示例

添加依赖

implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'

定义数据类

data class Article(
    val id: Int,
    val title: String,
    val content: String,
    @SerializedName("created_at") val createdAt: String,
    @SerializedName("image_url") val imageUrl: String? = null
)

data class ApiResponse<T>(
    val code: Int,
    val message: String,
    val data: T?
)

data class PageResponse<T>(
    val items: List<T>,
    val total: Int,
    val page: Int,
    val pageSize: Int
)

定义 API 接口

interface ApiService {
    @GET("articles")
    suspend fun getArticles(
        @Query("page") page: Int,
        @Query("size") size: Int = 20,
        @Query("category") category: String? = null
    ): ApiResponse<PageResponse<Article>>

    @GET("articles/{id}")
    suspend fun getArticleById(@Path("id") id: Int): ApiResponse<Article>

    @POST("articles")
    suspend fun createArticle(@Body article: Article): ApiResponse<Article>

    @FormUrlEncoded
    @POST("login")
    suspend fun login(
        @Field("username") username: String,
        @Field("password") password: String
    ): ApiResponse<Token>

    @Multipart
    @POST("upload")
    suspend fun uploadImage(
        @Part file: MultipartBody.Part,
        @Part("description") description: RequestBody
    ): ApiResponse<String>

    @PUT("articles/{id}")
    suspend fun updateArticle(
        @Path("id") id: Int,
        @Body article: Article
    ): ApiResponse<Article>

    @DELETE("articles/{id}")
    suspend fun deleteArticle(@Path("id") id: Int): ApiResponse<Unit>

    @Headers("Cache-Control: max-age=640000")
    @GET("config")
    suspend fun getConfig(): ApiResponse<AppConfig>

    // 流式响应(大文件下载)
    @Streaming
    @GET("download/{file}")
    suspend fun downloadFile(@Path("file") fileName: String): ResponseBody
}

创建 Retrofit 实例

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Provides
    @Singleton
    fun provideOkHttpClient(
        @ApplicationContext context: Context
    ): OkHttpClient {
        // 缓存配置
        val cacheSize = 10 * 1024 * 1024L  // 10MB
        val cache = Cache(context.cacheDir, cacheSize)

        // 日志拦截器
        val loggingInterceptor = HttpLoggingInterceptor().apply {
            level = if (BuildConfig.DEBUG)
                HttpLoggingInterceptor.Level.BODY
            else
                HttpLoggingInterceptor.Level.NONE
        }

        // Token 拦截器
        val authInterceptor = Interceptor { chain ->
            val token = TokenManager.getToken()
            val request = if (token != null) {
                chain.request().newBuilder()
                    .addHeader("Authorization", "Bearer $token")
                    .addHeader("Accept", "application/json")
                    .build()
            } else {
                chain.request()
            }
            chain.proceed(request)
        }

        // Token 刷新拦截器
        val tokenRefreshInterceptor = Interceptor { chain ->
            val response = chain.proceed(chain.request())
            if (response.code == 401) {
                // 尝试刷新 Token
                synchronized(this) {
                    val newToken = refreshToken()
                    if (newToken != null) {
                        response.close()
                        val newRequest = chain.request().newBuilder()
                            .addHeader("Authorization", "Bearer $newToken")
                            .build()
                        return@Interceptor chain.proceed(newRequest)
                    }
                }
            }
            response
        }

        return OkHttpClient.Builder()
            .cache(cache)
            .addInterceptor(loggingInterceptor)
            .addInterceptor(authInterceptor)
            .addInterceptor(tokenRefreshInterceptor)
            .connectTimeout(30, TimeUnit.SECONDS)
            .readTimeout(30, TimeUnit.SECONDS)
            .writeTimeout(30, TimeUnit.SECONDS)
            .build()
    }

    @Provides
    @Singleton
    fun provideRetrofit(client: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .baseUrl(BuildConfig.API_URL)
            .client(client)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    @Provides
    @Singleton
    fun provideApiService(retrofit: Retrofit): ApiService {
        return retrofit.create(ApiService::class.java)
    }
}

在 Repository 和 ViewModel 中调用

class ArticleRepository @Inject constructor(
    private val api: ApiService
) {
    suspend fun getArticles(page: Int): Result<List<Article>> {
        return try {
            val response = api.getArticles(page)
            if (response.code == 200 && response.data != null) {
                Result.success(response.data.items)
            } else {
                Result.failure(Exception(response.message))
            }
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

@HiltViewModel
class ArticleViewModel @Inject constructor(
    private val repo: ArticleRepository
) : ViewModel() {

    private val _state = MutableStateFlow<UiState<List<Article>>>(UiState.Loading)
    val state: StateFlow<UiState<List<Article>>> = _state.asStateFlow()

    fun loadArticles(page: Int) {
        viewModelScope.launch {
            _state.value = UiState.Loading
            val result = repo.getArticles(page)
            _state.value = result.fold(
                onSuccess = { UiState.Success(it) },
                onFailure = { UiState.Error(it.message ?: "未知错误") }
            )
        }
    }
}

sealed class UiState<out T> {
    data object Loading : UiState<Nothing>()
    data class Success<T>(val data: T) : UiState<T>()
    data class Error(val message: String) : UiState<Nothing>()
}

💡

线程:

Retrofit 与 suspend 函数配合时自动在 IO 线程执行,ViewModelScope 在主线程接收结果。

文件下载

suspend fun downloadFile(url: String, saveFile: File) {
    val response = api.downloadFile(url)
    response.byteStream().use { input ->
        saveFile.outputStream().use { output ->
            val buffer = ByteArray(8192)
            var bytesRead: Int
            while (input.read(buffer).also { bytesRead = it } != -1) {
                output.write(buffer, 0, bytesRead)
            }
        }
    }
}

17. JSON 解析

▼

常用 JSON 库

库特点推荐度
GsonGoogle 出品,简单易用⭐⭐⭐⭐
MoshiSquare 出品,性能优、Kotlin 友好⭐⭐⭐⭐⭐
kotlinx.serializationKotlin 官方,编译时生成,跨平台⭐⭐⭐⭐⭐

Gson 示例

implementation 'com.google.code.gson:gson:2.10.1'

data class User(
    @SerializedName("user_name") val name: String,
    val age: Int,
    @SerializedName("created_at") val createdAt: Date? = null
)

val gson = GsonBuilder()
    .setDateFormat("yyyy-MM-dd'T'HH:mm:ss")
    .setPrettyPrinting()
    .create()

// JSON 字符串 → 对象
val json = """{"user_name":"张三","age":25}"""
val user = gson.fromJson(json, User::class.java)

// 对象 → JSON
val jsonStr = gson.toJson(user)

// 解析数组
val jsonArray = """[{"user_name":"张三"},{"user_name":"李四"}]"""
val listType = object : TypeToken<List<User>>() {}.type
val users: List<User> = gson.fromJson(jsonArray, listType)

// 自定义序列化器
class DateSerializer : JsonSerializer<Date> {
    override fun serialize(src: Date?, typeOfSrc: Type?, context: JsonSerializationContext?): JsonElement {
        return JsonPrimitive(src?.time ?: 0)
    }
}

val gsonWithCustom = GsonBuilder()
    .registerTypeAdapter(Date::class.java, DateSerializer())
    .create()

Moshi 示例

implementation 'com.squareup.moshi:moshi:1.15.0'
implementation 'com.squareup.moshi:moshi-kotlin:1.15.0'
ksp 'com.squareup.moshi:moshi-kotlin-codegen:1.15.0'

@JsonClass(generateAdapter = true)
data class User(
    @Json(name = "user_name") val name: String,
    val age: Int,
    @Json(name = "created_at") val createdAt: Long
)

val moshi = Moshi.Builder()
    .addLast(KotlinJsonAdapterFactory())
    .build()

val adapter = moshi.adapter(User::class.java)
val user = adapter.fromJson(json)
val jsonStr = adapter.toJson(user)

// 列表
val listType = Types.newParameterizedType(List::class.java, User::class.java)
val listAdapter = moshi.adapter<List<User>>(listType)

// 自定义适配器
class DateAdapter {
    @ToJson fun toJson(@Timestamp date: Long): String = SimpleDateFormat("yyyy-MM-dd").format(Date(date))
    @FromJson @Timestamp fun fromJson(s: String): Long = SimpleDateFormat("yyyy-MM-dd").parse(s)!!.time
}

@Retention(AnnotationRetention.RUNTIME)
@JsonQualifier
annotation class Timestamp

kotlinx.serialization 示例

plugins {
    id 'org.jetbrains.kotlin.plugin.serialization' version '1.9.20'
}

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2'
}

@Serializable
data class User(
    @SerialName("user_name") val name: String,
    val age: Int,
    val roles: List<String> = emptyList()
)

// 配置 JSON
val json = Json {
    ignoreUnknownKeys = true
    isLenient = true
    encodeDefaults = true
    prettyPrint = true
}

// 解析
val user = json.decodeFromString<User>(jsonString)

// 序列化
val jsonStr = json.encodeToString(user)

// 多态序列化
@Serializable
sealed class Shape {
    @Serializable
    @SerialName("circle")
    data class Circle(val radius: Double) : Shape()

    @Serializable
    @SerialName("rectangle")
    data class Rectangle(val width: Double, val height: Double) : Shape()
}

val shape: Shape = Shape.Circle(5.0)
val shapeJson = json.encodeToString(shape)  // 自动包含 type 字段

18. 多线程与协程

▼

为什么需要多线程

Android 主线程(UI 线程)不能执行耗时操作(网络、数据库、文件 IO),否则会触发 ANR(Application Not Responding)。

传统方式

// 方式1:Thread
Thread {
    val data = fetchData()  // IO 操作
    runOnUiThread {
        textView.text = data
    }
}.start()

// 方式2:Handler
val handler = Handler(Looper.getMainLooper())
Thread {
    val data = fetchData()
    handler.post { textView.text = data }
}.start()

// 方式3:ExecutorService
val executor = Executors.newFixedThreadPool(4)
executor.execute {
    val data = fetchData()
    handler.post { updateUI(data) }
}

// 方式4:AsyncTask(已废弃,不推荐)

Kotlin 协程(推荐)

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3'

基础用法

// ViewModel 中使用
class MyViewModel : ViewModel() {
    fun loadData() {
        viewModelScope.launch {  // 自动随 ViewModel 销毁取消
            try {
                _state.value = UiState.Loading
                val result = withContext(Dispatchers.IO) {
                    // IO 线程执行耗时操作
                    api.getData()
                }
                _state.value = UiState.Success(result)
            } catch (e: Exception) {
                _state.value = UiState.Error(e.message ?: "Error")
            }
        }
    }
}

// Activity/Fragment 中使用
lifecycleScope.launch {
    // 自动随生命周期取消
    val data = withContext(Dispatchers.IO) { fetchData() }
    updateUI(data)
}

// 全局协程(避免使用,优先用 scope 绑定生命周期)
GlobalScope.launch {  // 不推荐
    // ...
}

常用 Dispatcher

Dispatcher线程适用场景
Dispatchers.Main主线程UI 更新
Dispatchers.IOIO 线程池(64线程)网络、数据库、文件
Dispatchers.DefaultCPU 密集线程池(CPU核心数)CPU 计算任务
Dispatchers.Unconfined不限定线程特殊场景

并发执行多个任务

viewModelScope.launch {
    // async 并发启动,await 等待结果
    val users = async(Dispatchers.IO) { api.getUsers() }
    val posts = async(Dispatchers.IO) { api.getPosts() }
    val config = async(Dispatchers.IO) { api.getConfig() }

    // 等待所有任务完成
    val result = Triple(users.await(), posts.await(), config.await())
    updateUI(result)

    // 或使用 awaitAll
    val results = awaitAll(users, posts, config)
}

// 取消所有任务
viewModelScope.coroutineContext.cancelChildren()

超时与重试

// 超时
viewModelScope.launch {
    try {
        val result = withTimeout(5000) {  // 5 秒超时
            api.fetchData()
        }
    } catch (e: TimeoutCancellationException) {
        showError("请求超时")
    }
}

// 重试机制
suspend fun <T> retry(
    times: Int = 3,
    initialDelay: Long = 100,
    maxDelay: Long = 1000,
    factor: Double = 2.0,
    block: suspend () -> T
): T {
    var currentDelay = initialDelay
    repeat(times - 1) {
        try {
            return block()
        } catch (e: Exception) {
            delay(currentDelay)
            currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay)
        }
    }
    return block()  // 最后一次尝试
}

viewModelScope.launch {
    val data = retry(times = 3) { api.fetchData() }
    updateUI(data)
}

Flow 数据流

// 定义 Flow
fun countDown(): Flow<Int> = flow {
    for (i in 10 downTo 1) {
        emit(i)
        delay(1000)
    }
}

// 收集 Flow
lifecycleScope.launch {
    countDown()
        .onStart { showLoading() }
        .onCompletion { hideLoading() }
        .catch { e -> showError(e.message) }
        .flowOn(Dispatchers.IO)  // 指定上游线程
        .collect { value ->
            binding.tvCount.text = value.toString()
        }
}

// 常用 Flow 操作符
fun getDataFlow(): Flow<List<Item>> = flow {
    emit(api.getData())
}
.map { items -> items.filter { it.isActive } }
.filter { it.size > 5 }
.distinctUntilChanged()
.debounce(300)  // 防抖
.buffer()       // 缓冲
.take(10)       // 取前 10 个
.drop(1)        // 跳过第 1 个
.combine(otherFlow) { a, b -> a + b }
.flatMapLatest { id -> api.getDetail(id) }  // 最新优先

// Room + Flow(数据变化自动通知)
@Query("SELECT * FROM users")
fun observeUsers(): Flow<List<User>>

// StateFlow(热流,总有最新值)
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()

// SharedFlow(事件流)
private val _events = MutableSharedFlow<UiEvent>()
val events = _events.asSharedFlow()

// 在 Compose 中收集
val state by viewModel.uiState.collectAsState()

// 传统 View 中收集
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.uiState.collect { state ->
            updateUI(state)
        }
    }
}

19. Jetpack 组件库

▼

Jetpack 概览

Jetpack 是 Google 提供的一套现代化 Android 开发库,遵循最佳实践。

核心组件

ViewModel

保存 UI 数据,不受配置更改影响:

class CounterViewModel : ViewModel() {
    private val _count = MutableLiveData(0)
    val count: LiveData<Int> = _count

    // StateFlow(推荐)
    private val _state = MutableStateFlow(0)
    val state: StateFlow<Int> = _state.asStateFlow()

    fun increment() {
        _count.value = (_count.value ?: 0) + 1
        _state.value++
    }

    // 清理资源
    override fun onCleared() {
        super.onCleared()
        // 取消订阅等
    }

    // ViewModel 工厂(带参数)
    companion object {
        fun factory(id: Long) = object : ViewModelProvider.Factory {
            override fun <T : ViewModel> create(modelClass: Class<T>): T {
                return DetailViewModel(id) as T
            }
        }
    }
}

// Activity 中
private val viewModel: CounterViewModel by viewModels()
private val sharedViewModel: SharedViewModel by activityViewModels()

// Fragment 中
private val viewModel: CounterViewModel by viewModels()
private val parentVM: SharedViewModel by activityViewModels()

// 带工厂(Hilt 自动处理)
@HiltViewModel
class DetailViewModel @Inject constructor(
    private val repo: Repository,
    @Assisted savedStateHandle: SavedStateHandle
) : ViewModel() {
    val itemId: Long = savedStateHandle["id"] ?: 0
}

LiveData / StateFlow

// LiveData(生命周期感知,旧方式)
val name = MutableLiveData<String>()
name.observe(this) { /* UI 更新 */ }

// LiveData 转换
val users: LiveData<List<User>> = Transformations.map(repo.users) { list ->
    list.filter { it.isActive }
}

// StateFlow(协程友好,推荐)
private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState.asStateFlow()

// 在 Activity 中收集
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.uiState.collect { state ->
            updateUI(state)
        }
    }
}

// 在 Compose 中收集
@Composable
fun Screen(viewModel: MyViewModel = hiltViewModel()) {
    val state by viewModel.uiState.collectAsState()
}

// Lifecycle-aware
lifecycleScope.launch {
    lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
        launch { viewModel.state.collect { ... } }
        launch { viewModel.events.collect { ... } }
    }
}

Lifecycle

// 实现 LifecycleObserver
class MyLocationListener(
    private val lifecycle: Lifecycle
) : DefaultLifecycleObserver {

    override fun onStart(owner: LifecycleOwner) {
        // 开始监听位置
    }

    override fun onStop(owner: LifecycleOwner) {
        // 停止监听
    }
}

// 注册
lifecycle.addObserver(MyLocationListener(lifecycle))

// 在协程中使用
lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.RESUMED) {
        // 仅在 RESUMED 时执行
        locationFlow.collect { updateMap(it) }
    }
}

Paging 3 分页加载

implementation 'androidx.paging:paging-runtime-ktx:3.2.1'

class UserPagingSource(
    private val api: ApiService
) : PagingSource<Int, User>() {

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, User> {
        val page = params.key ?: 1
        return try {
            val response = api.getUsers(page, params.loadSize)
            LoadResult.Page(
                data = response.data,
                prevKey = if (page == 1) null else page - 1,
                nextKey = if (response.data.isEmpty()) null else page + 1
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }

    override fun getRefreshKey(state: PagingState<Int, User>): Int? {
        return state.anchorPosition?.let { anchorPosition ->
            state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
                ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
        }
    }
}

class UserRepository @Inject constructor(private val api: ApiService) {
    fun getUsers(): Flow<PagingData<User>> {
        return Pager(
            config = PagingConfig(
                pageSize = 20,
                enablePlaceholders = false,
                prefetchDistance = 5,
                initialLoadSize = 40
            ),
            pagingSourceFactory = { UserPagingSource(api) }
        ).flow
    }
}

@HiltViewModel
class UserListViewModel @Inject constructor(
    repo: UserRepository
) : ViewModel() {
    val users = repo.getUsers().cachedIn(viewModelScope)
}

// Adapter
class UserPagingAdapter : PagingDataAdapter<User, UserViewHolder>(DIFF_CALLBACK) {
    companion object {
        val DIFF_CALLBACK = object : DiffUtil.ItemCallback<User>() {
            override fun areItemsTheSame(oldItem: User, newItem: User) = oldItem.id == newItem.id
            override fun areContentsTheSame(oldItem: User, newItem: User) = oldItem == newItem
        }
    }
}

// 在 Fragment 中
viewLifecycleOwner.lifecycleScope.launch {
    viewModel.users.collectLatest { pagingData ->
        adapter.submitData(pagingData)
    }
}

// 加载状态监听
adapter.addLoadStateListener { loadState ->
    binding.progressBar.isVisible = loadState.refresh is LoadState.Loading
    binding.btnRetry.isVisible = loadState.refresh is LoadState.Error
}

20. Jetpack Compose UI 框架

▼

什么是 Compose

Jetpack Compose 是 Android 全新的声明式 UI 框架,使用 Kotlin 编写 UI,无需 XML 布局文件。

启用 Compose

android {
    buildFeatures {
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion "1.5.4"
    }
}

dependencies {
    implementation platform('androidx.compose:compose-bom:2024.01.00')
    implementation 'androidx.compose.ui:ui'
    implementation 'androidx.compose.ui:ui-graphics'
    implementation 'androidx.compose.ui:ui-tooling-preview'
    implementation 'androidx.compose.material3:material3'
    implementation 'androidx.activity:activity-compose:1.8.2'
    implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0'
    implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.7.0'
    debugImplementation 'androidx.compose.ui:ui-tooling'
}

基础示例

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyAppTheme {
                GreetingScreen()
            }
        }
    }
}

@Composable
fun GreetingScreen() {
    var count by remember { mutableStateOf(0) }

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("示例") },
                navigationIcon = {
                    IconButton(onClick = { /* 返回 */ }) {
                        Icon(Icons.Default.ArrowBack, contentDescription = "返回")
                    }
                }
            )
        },
        floatingActionButton = {
            FloatingActionButton(onClick = { count++ }) {
                Icon(Icons.Default.Add, contentDescription = "新增")
            }
        }
    ) { paddingValues ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(paddingValues)
                .padding(16.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
        ) {
            Text(
                text = "点击次数:$count",
                fontSize = 24.sp,
                fontWeight = FontWeight.Bold
            )
            Spacer(modifier = Modifier.height(16.dp))
            Button(onClick = { count++ }) {
                Text("点击 +1")
            }
        }
    }
}

常用组件

// 文本
Text(
    text = "Hello",
    fontSize = 16.sp,
    color = Color.Gray,
    fontWeight = FontWeight.Bold,
    maxLines = 2,
    overflow = TextOverflow.Ellipsis
)

// 图片
AsyncImage(  // Coil
    model = "https://example.com/image.jpg",
    contentDescription = "image",
    modifier = Modifier.size(100.dp).clip(CircleShape),
    contentScale = ContentScale.Crop
)

// 输入框
var text by remember { mutableStateOf("") }
OutlinedTextField(
    value = text,
    onValueChange = { text = it },
    label = { Text("用户名") },
    leadingIcon = { Icon(Icons.Default.Person, null) },
    isError = text.isEmpty(),
    supportingText = { if (text.isEmpty()) Text("不能为空") }
)

// 列表
LazyColumn(
    contentPadding = PaddingValues(16.dp),
    verticalArrangement = Arrangement.spacedBy(8.dp)
) {
    items(items, key = { it.id }) { item ->
        ItemRow(item)
    }

    item {
        if (isLoading) {
            CircularProgressIndicator(modifier = Modifier.fillMaxWidth().padding(16.dp))
        }
    }
}

// 网格
LazyVerticalGrid(columns = GridCells.Fixed(2)) {
    items(items) { item -> GridItem(item) }
}

// 可滚动行
LazyRow {
    items(categories) { category ->
        CategoryChip(category)
    }
}

// 卡片
Card(
    modifier = Modifier.fillMaxWidth().padding(8.dp),
    elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
    shape = RoundedCornerShape(12.dp)
) {
    Column(modifier = Modifier.padding(16.dp)) {
        Text("标题", style = MaterialTheme.typography.titleMedium)
        Spacer(modifier = Modifier.height(8.dp))
        Text("描述", style = MaterialTheme.typography.bodyMedium)
    }
}

状态管理

// 局部状态
@Composable
fun CounterScreen() {
    var count by remember { mutableStateOf(0) }
    var name by remember { mutableStateOf("") }

    Counter(count, onIncrement = { count++ })
}

// 保存状态(配置变化时)
var count by rememberSaveable { mutableStateOf(0) }

// ViewModel 集成
@Composable
fun ListScreen(viewModel: ListViewModel = hiltViewModel()) {
    val state by viewModel.state.collectAsState()
    val events by viewModel.events.collectAsState(null)

    LaunchedEffect(events) {
        events?.let { event ->
            when (event) {
                is UiEvent.ShowToast -> {
                    // 显示 Toast
                }
            }
        }
    }

    when (state) {
        is UiState.Loading -> CircularProgressIndicator()
        is UiState.Success -> UserList((state as UiState.Success).data)
        is UiState.Error -> ErrorView((state as UiState.Error).message)
    }
}

// 副作用
LaunchedEffect(key1 = itemId) {
    // 在组合时启动协程
    viewModel.loadDetail(itemId)
}

DisposableEffect(key1 = userId) {
    val subscription = subscribeToUser(userId)
    onDispose {
        subscription.cancel()
    }
}

SideEffect {
    // 每次重组时执行
    analytics.trackScreen("HomeScreen")
}

主题系统

@Composable
fun MyAppTheme(content: @Composable () -> Unit) {
    val colorScheme = if (isSystemInDarkTheme()) {
        darkColorScheme(
            primary = Purple80,
            secondary = PurpleGrey80,
            tertiary = Pink80
        )
    } else {
        lightColorScheme(
            primary = Purple40,
            secondary = PurpleGrey40,
            tertiary = Pink40
        )
    }

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

// 动态取色(Android 12+)
@Composable
fun MyAppTheme(content: @Composable () -> Unit) {
    val context = LocalContext.current
    val colorScheme = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
        dynamicLightColorScheme(context)
    } else {
        lightColorScheme()
    }
    MaterialTheme(colorScheme = colorScheme, content = content)
}

导航

implementation 'androidx.navigation:navigation-compose:2.7.6'

@Composable
fun AppNav() {
    val navController = rememberNavController()

    NavHost(navController = navController, startDestination = "home") {
        composable("home") {
            HomeScreen(onNavigateToDetail = { id ->
                navController.navigate("detail/$id")
            })
        }

        composable(
            route = "detail/{itemId}",
            arguments = listOf(navArgument("itemId") { type = NavType.LongType })
        ) { backStackEntry ->
            val itemId = backStackEntry.arguments?.getLong("itemId") ?: 0
            DetailScreen(itemId = itemId, onBack = { navController.popBackStack() })
        }

        composable(
            route = "profile?tab={tab}",
            arguments = listOf(
                navArgument("tab") {
                    type = NavType.StringType
                    defaultValue = "info"
                }
            )
        ) {
            ProfileScreen()
        }
    }
}

💡

趋势:

Google 新发布的应用和库越来越倾向于 Compose,新项目建议考虑使用。

21. 性能优化

▼

启动速度优化

  • 使用 App Startup 库延迟初始化第三方 SDK
  • 避免在 Application.onCreate() 中执行耗时操作
  • 使用 Trace.beginSection() 追踪启动阶段
  • 启用 R8 代码压缩,减小 APK 体积
  • 使用 Baseline Profile 优化 ART 编译
// App Startup
implementation 'androidx.startup:startup-runtime:1.1.1'

class AnalyticsInitializer : Initializer<Analytics> {
    override fun create(context: Context): Analytics {
        return Analytics.init(context)
    }
    override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
}

// Manifest
<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    android:exported="false"
    tools:node="merge">
    <meta-data
        android:name="com.example.AnalyticsInitializer"
        android:value="androidx.startup"/>
</provider>

// Baseline Profile
implementation 'androidx.profileinstaller:profileinstaller:1.3.1'

// benchmark 模块
@ExperimentalBaselineProfilesApi
class BaselineProfileGenerator {
    @get:Rule val rule = BaselineProfileRule()

    @Test
    fun generateBaselineProfile() = rule.collectBaselineProfile(
        packageName = "com.example.myapp"
    ) {
        startActivityAndWait()
        // 关键用户路径
    }
}

内存优化

// 避免内存泄漏
// ❌ 错误:匿名内部类持有 Activity 引用
handler.postDelayed(object : Runnable {
    override fun run() {
        textView.text = "hello"  // 隐式持有 Activity
    }
}, 60000)

// ✅ 正确:使用 LifecycleScope
lifecycleScope.launch {
    delay(60000)
    binding.textView.text = "hello"
}

// 避免 Activity 泄漏
// ❌ 静态持有 Activity 引用
companion object { var activity: MainActivity? = null }

// ✅ 使用 WeakReference 或 Application Context
val weakActivity = WeakReference(activity)

// 避免 Handler 泄漏
// ❌ 错误
val handler = Handler()

// ✅ 正确:静态内部类 + WeakReference
class SafeHandler(activity: MainActivity) : Handler(Looper.getMainLooper()) {
    private val weakActivity = WeakReference(activity)
    override fun handleMessage(msg: Message) {
        weakActivity.get()?.let { /* 处理消息 */ }
    }
}

// 避免 Bitmap 内存溢出
val options = BitmapFactory.Options().apply {
    inJustDecodeBounds = true
}
BitmapFactory.decodeFile(path, options)
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
options.inJustDecodeBounds = false
val bitmap = BitmapFactory.decodeFile(path, options)

// 大图加载
implementation 'com.github.bumptech.glide:glide:4.16.0'

Glide.with(context)
    .load(url)
    .override(800, 600)  // 限制尺寸
    .diskCacheStrategy(DiskCacheStrategy.ALL)
    .into(imageView)

布局优化

  • 使用 <include> 复用布局
  • 使用 <merge> 减少根节点
  • 使用 ViewStub 延迟加载不常用视图
  • 扁平化布局,减少嵌套层数
  • 使用 Layout Inspector 工具分析布局
  • 避免过度绘制(overdraw)
<!-- 复用布局 -->
<include layout="@layout/layout_toolbar"/>

<!-- 延迟加载 -->
<ViewStub
    android:id="@+id/stub"
    android:layout="@layout/layout_expensive"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"/>

<!-- Kotlin 中加载 -->
binding.stub.inflate()

<!-- merge 减少层级 -->
<merge xmlns:android="...">
    <TextView ... />
    <Button ... />
</merge>

<!-- ConstraintLayout 优化 -->
<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">
    <!-- 扁平结构,避免嵌套 LinearLayout -->
</androidx.constraintlayout.widget.ConstraintLayout>

列表优化

  • RecyclerView 使用 setHasFixedSize(true)
  • 使用 DiffUtil 或 ListAdapter 精准刷新
  • 图片加载使用 Glide / Coil,开启缓存
  • 避免在 onBindViewHolder 中创建对象或设置监听器
  • 设置 RecycledViewPool 跨 RecyclerView 共享
  • 使用 setItemViewCacheSize(20) 增加缓存数量
  • 预加载 Prefetch 提前准备下一个 item
recyclerView.apply {
    setHasFixedSize(true)
    setItemViewCacheSize(20)
    recycledViewPool.setMaxRecycledViews(0, 30)

    // 嵌套 RecyclerView 共享 ViewPool
    setRecycledViewPool(sharedPool)
}

APK 瘦身

  • 启用 minifyEnabled true 和 R8 压缩
  • 启用 shrinkResources true 移除未使用资源
  • 使用 WebP 格式替代 PNG(体积减少 30%+)
  • 使用 Android App Bundle(AAB)按需下发资源
  • 删除未使用的资源:Analyze → Inspect Code → Unused Resources
  • 使用 resConfigs 限制语言资源
  • 检查并删除无用依赖
android {
    defaultConfig {
        // 仅保留中文、英文资源
        resourceConfigurations += setOf("zh", "en")
    }

    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    // Splits:按 ABI 拆分
    splits {
        abi {
            enable true
            reset()
            include "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
            universalApk false
        }
    }
}

// APK Analyzer 工具
// Build → Analyze APK → 查看各部分体积

性能分析工具

工具用途
Android ProfilerCPU / 内存 / 网络 / 能耗监控
Layout Inspector实时查看 UI 层级
Database Inspector查看 Room 数据库数据
StrictMode检测主线程违规操作
LeakCanary内存泄漏检测
Android Benchmark性能基准测试
APK Analyzer分析 APK 体积构成
Systrace / Perfetto系统级性能追踪
// StrictMode
StrictMode.setThreadPolicy(
    StrictMode.ThreadPolicy.Builder()
        .detectAll()
        .penaltyLog()
        .build()
)
StrictMode.setVmPolicy(
    StrictMode.VmPolicy.Builder()
        .detectLeakedClosableObjects()
        .detectLeakedSqlLiteObjects()
        .penaltyLog()
        .build()
)

// LeakCanary
debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.13'

22. 调试与测试

▼

Logcat 日志

import android.util.Log

Log.v("TAG", "Verbose")  // 详细
Log.d("TAG", "Debug")    // 调试
Log.i("TAG", "Info")     // 信息
Log.w("TAG", "Warning")  // 警告
Log.e("TAG", "Error")    // 错误
Log.wtf("TAG", "WTF")    // 严重错误

// Kotlin 封装
object AppLog {
    private const val TAG = "MyApp"
    private val isDebug = BuildConfig.DEBUG

    fun d(msg: String, tag: String = TAG) {
        if (isDebug) Log.d(tag, msg)
    }

    fun e(msg: String, e: Throwable? = null, tag: String = TAG) {
        Log.e(tag, msg, e)
    }

    fun json(json: String, tag: String = TAG) {
        if (isDebug) {
            try {
                val gson = GsonBuilder().setPrettyPrinting().create()
                val obj = gson.fromJson(json, Any::class.java)
                d(gson.toJson(obj), tag)
            } catch (e: Exception) {
                d(json, tag)
            }
        }
    }
}

// Timber(推荐)
implementation 'com.jakewharton.timber:timber:5.0.1'

// Application 中
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) {
            Timber.plant(Timber.DebugTree())
        } else {
            Timber.plant(CrashReportingTree())
        }
    }
}

// 使用
Timber.d("Debug message")
Timber.e(exception, "Error with %s", detail)
Timber.tag("CustomTag").i("Message")

断点调试

  • F8:单步跳过(Step Over)
  • F7:单步进入(Step Into)
  • Shift+F8:跳出(Step Out)
  • F9:运行到下一个断点
  • Alt+F9:运行到光标处
  • 条件断点:右键断点设置条件,仅在满足时触发
  • 日志断点:不暂停,仅输出日志
  • 异常断点:在抛出异常时暂停

单元测试(JUnit)

testImplementation 'junit:junit:4.13.2'
testImplementation 'org.mockito:mockito-core:5.8.0'
testImplementation 'org.mockito.kotlin:mockito-kotlin:5.2.1'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
testImplementation 'app.cash.turbine:turbine:1.0.0'  // Flow 测试

// 测试类
class CalculatorTest {
    private lateinit var calculator: Calculator

    @Before
    fun setup() {
        calculator = Calculator()
    }

    @After
    fun tearDown() {
        // 清理
    }

    @Test
    fun testAdd() {
        val result = calculator.add(2, 3)
        assertEquals(5, result)
    }

    @Test
    fun testDivide() {
        assertThrows(ArithmeticException::class.java) {
            calculator.divide(1, 0)
        }
    }

    @Test
    fun testAddWithParams() {
        // 参数化测试
        listOf(
            Triple(1, 2, 3),
            Triple(0, 0, 0),
            Triple(-1, 1, 0)
        ).forEach { (a, b, expected) ->
            assertEquals(expected, calculator.add(a, b))
        }
    }
}

// Mockito 测试
class UserRepositoryTest {
    private lateinit var api: ApiService
    private lateinit var repo: UserRepository

    @Before
    fun setup() {
        api = mock()
        repo = UserRepository(api)
    }

    @Test
    fun testGetUser() = runTest {
        // Given
        whenever(api.getUser(123)).thenReturn(ApiResponse(200, "ok", User("张三", 25)))

        // When
        val result = repo.getUser(123)

        // Then
        assertEquals("张三", result.getOrNull()?.name)
        verify(api).getUser(123)
    }
}

// ViewModel 测试
@OptIn(ExperimentalCoroutinesApi::class)
class UserViewModelTest {
    private val testDispatcher = UnconfinedTestDispatcher()
    private val testScope = TestScope(testDispatcher)

    @Before
    fun setup() {
        Dispatchers.setMain(testDispatcher)
    }

    @After
    fun tearDown() {
        Dispatchers.resetMain()
    }

    @Test
    fun testLoadUsers() = testScope.runTest {
        val repo = mock<UserRepository>()
        val users = flowOf(listOf(User("张三", 25)))
        whenever(repo.allUsers).thenReturn(users)

        val viewModel = UserViewModel(repo)

        viewModel.uiState.test {
            assertEquals(UiState.Loading, awaitItem())
            assertEquals(UiState.Success(listOf(User("张三", 25))), awaitItem())
        }
    }
}

UI 测试(Espresso)

androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.5.1'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.1'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test:rules:1.5.0'

@RunWith(AndroidJUnit4::class)
class LoginActivityTest {

    @get:Rule
    val activityRule = ActivityScenarioRule(LoginActivity::class.java)

    @Before
    fun setup() {
        Intents.init()
    }

    @After
    fun tearDown() {
        Intents.release()
    }

    @Test
    fun testLoginSuccess() {
        // 输入用户名
        onView(withId(R.id.etUsername))
            .perform(typeText("testuser"), closeSoftKeyboard())

        // 输入密码
        onView(withId(R.id.etPassword))
            .perform(typeText("123456"), closeSoftKeyboard())

        // 点击登录
        onView(withId(R.id.btnLogin))
            .perform(click())

        // 验证跳转
        intended(hasComponent(HomeActivity::class.java.name))

        // 验证欢迎文本
        onView(withId(R.id.tvWelcome))
            .check(matches(withText(containsString("欢迎"))))
    }

    @Test
    fun testLoginEmptyUsername() {
        onView(withId(R.id.btnLogin)).perform(click())
        onView(withId(R.id.etUsername))
            .check(matches(hasErrorText("用户名不能为空")))
    }

    @Test
    fun testRecyclerView() {
        // 滑动到指定位置
        onView(withId(R.id.recyclerView))
            .perform(RecyclerViewActions.scrollToPosition<UserAdapter.UserViewHolder>(5))

        // 点击某个 item
        onView(withId(R.id.recyclerView))
            .perform(RecyclerViewActions.actionOnItemAtPosition<UserAdapter.UserViewHolder>(
                5, click()
            ))

        // 验证 item 文本
        onView(withText("张三"))
            .check(matches(isDisplayed()))
    }
}

// Compose UI 测试
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'

@RunWith(AndroidJUnit4::class)
class MyComposeTest {
    @get:Rule
    val composeTestRule = createComposeRule()

    @Test
    fun testCounter() {
        composeTestRule.setContent {
            CounterScreen()
        }

        composeTestRule.onNodeWithText("点击 +1").performClick()
        composeTestRule.onNodeWithText("点击次数:1").assertIsDisplayed()
    }
}

Robolectric(无需设备)

testImplementation 'org.robolectric:robolectric:4.11.1'

@RunWith(RobolectricTestRunner::class)
@Config(sdk = [33], application = MyApplication::class)
class MainActivityTest {
    @Test
    fun testButtonClick() {
        val activity = Robolectric.buildActivity(MainActivity::class.java)
            .create().start().resume().get()

        val button = activity.findViewById<Button>(R.id.btn)
        button.performClick()

        val textView = activity.findViewById<TextView>(R.id.tvResult)
        assertEquals("Clicked", textView.text.toString())

        // 验证 Activity 跳转
        val intent = ShadowActivity(activity).peekNextStartedActivity()
        assertEquals(DetailActivity::class.java.name, intent.component?.className)
    }
}

测试覆盖率

// build.gradle
android {
    buildTypes {
        debug {
            testCoverageEnabled true
        }
    }
}

// JaCoCo 配置
apply plugin: 'jacoco'

task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) {
    reports {
        xml.required = true
        html.required = true
    }

    def fileFilter = [
        '**/R.class',
        '**/R$*.class',
        '**/BuildConfig.*',
        '**/*_HiltModules*',
        '**/Hilt_*'
    ]
    def mainSrc = "$project.projectDir/src/main/java"

    sourceDirectories.setFrom(files([mainSrc]))
    classDirectories.setFrom(files([
        fileTree(dir: "$buildDir/tmp/kotlin-classes/debug", excludes: fileFilter)
    ]))
    executionData.setFrom(fileTree(dir: "$buildDir", includes: [
        "outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec"
    ]))
}

23. 应用发布上架

▼

签名 APK

发布版本必须使用正式签名(不可使用 debug 签名)。

生成签名密钥

keytool -genkey -v -keystore my-release-key.jks -keyalg RSA \
    -keysize 2048 -validity 10000 -alias my-key-alias

# 查看证书信息
keytool -list -v -keystore my-release-key.jks

# 导出证书
keytool -export -alias my-key-alias -keystore my-release-key.jks -file cert.crt

# Google Play App Signing 使用 upload key
# 上传密钥和 App Signing 密钥分离,更安全

配置 Gradle 签名

// gradle.properties(不要提交到 Git)
RELEASE_STORE_FILE=/path/to/my-release-key.jks
RELEASE_STORE_PASSWORD=xxx
RELEASE_KEY_ALIAS=my-key-alias
RELEASE_KEY_PASSWORD=xxx

// 或使用环境变量
// signingConfigs.release.storePassword System.getenv("KEY_STORE_PASSWORD")

// app/build.gradle
android {
    signingConfigs {
        release {
            storeFile file(RELEASE_STORE_FILE)
            storePassword RELEASE_STORE_PASSWORD
            keyAlias RELEASE_KEY_ALIAS
            keyPassword RELEASE_KEY_PASSWORD
            v1SigningEnabled true
            v2SigningEnabled true
        }
    }
    buildTypes {
        release {
            signingConfig signingConfigs.release
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

构建发布包

# APK
./gradlew assembleRelease

# AAB (Google Play 推荐)
./gradlew bundleRelease

# 指定渠道
./gradlew assembleProdRelease

# 输出位置
# APK: app/build/outputs/apk/release/app-release.apk
# AAB: app/build/outputs/bundle/release/app-release.aab

# 映射文件(用于反混淆崩溃日志)
# app/build/outputs/mapping/release/mapping.txt
# 务必保存每个版本的 mapping.txt!

Google Play 上架流程

  1. 注册 Google Play 开发者账号(一次性 25 美元)
  2. 在 Google Play Console 创建应用
  3. 填写应用信息:
    • 标题(30 字符内)、简短描述(80 字符)、详细描述(4000 字符)
    • 应用图标 512x512 PNG
    • 特色图片 1024x500 PNG
    • 截图:手机(最少 2 张)、7 寸平板、10 寸平板
  4. 设置内容分级(IARC 问卷)
  5. 填写隐私政策 URL(强制)
  6. 设置定价和分发地区
  7. 声明广告、目标受众
  8. 上传 AAB 文件
  9. 创建发布轨道:内部测试 → 封闭测试 → 开放测试 → 正式版
  10. 提交审核(一般 1-7 天)

国内应用市场

市场开发者费用审核时间备注
华为应用市场免费1-3 天华为手机预装
小米应用商店免费1-3 天MIUI 生态
OPPO 软件商店免费1-3 天ColorOS 生态
VIVO 应用商店免费1-3 天FuntouchOS
应用宝(腾讯)免费1-3 天微信/QQ 生态
360 手机助手免费1-3 天老牌市场

上架必备资料:

  • 软件著作权登记证书(中国)
  • ICP 备案(有网站)
  • 隐私政策(详细)
  • 用户协议
  • 应用截图(高清、带文案)
  • 应用图标(各尺寸)

多渠道打包

// 使用 productFlavors
android {
    flavorDimensions += "channel"
    productFlavors {
        huawei { dimension "channel" }
        xiaomi { dimension "channel" }
        oppo { dimension "channel" }
        tencent { dimension "channel" }
    }
}

// 读取渠道号
val channel = try {
    val appInfo = packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA)
    appInfo.metaData.getString("CHANNEL")
} catch (e: Exception) { "unknown" }

// Manifest 中配置
<meta-data android:name="CHANNEL" android:value="${CHANNEL_NAME}"/>

版本管理最佳实践

// build.gradle
defaultConfig {
    versionCode 10     // 整数,每次升级递增(Google Play 要求严格递增)
    versionName "2.1.0"  // 用户可见的版本号
}

// 语义化版本:主版本.次版本.修订号
// 2.1.0 = 新增功能,向下兼容
// 2.2.0 = 新增功能
// 2.2.1 = 修复 bug
// 3.0.0 = 重大变更,不兼容旧版本

// 强制更新机制
@Serializable
data class VersionInfo(
    val latestVersion: String,
    val minRequiredVersion: String,
    val downloadUrl: String,
    val forceUpdate: Boolean,
    val changelog: String
)

// 启动时检查版本
suspend fun checkUpdate(): VersionInfo? {
    val current = packageManager.getPackageInfo(packageName, 0).versionName
    val remote = api.getVersionInfo()
    return if (compareVersion(current, remote.minRequiredVersion) < 0) {
        remote
    } else null
}

24. 架构模式

▼

常见架构对比

架构特点适用场景
MVCController 臃肿,难测试小项目,不推荐
MVPPresenter 持有 View 接口,易测试中等项目
MVVM数据驱动,与 Jetpack 完美配合大部分项目(推荐)
MVI单向数据流,状态可预测复杂 UI 项目
Clean Architecture分层清晰,高度解耦大型企业项目

MVVM 架构详解

// 分层:UI (Activity/Fragment/Compose) → ViewModel → Repository → Data Source

// UI State
sealed class UiState<out T> {
    data object Loading : UiState<Nothing>()
    data class Success<T>(val data: T) : UiState<T>()
    data class Error(val message: String, val throwable: Throwable? = null) : UiState<Nothing>()
    data object Empty : UiState<Nothing>()
}

// UI Event(一次性事件)
sealed class UiEvent {
    data class ShowToast(val message: String) : UiEvent()
    data class Navigate(val route: String) : UiEvent()
    data object NavigateBack : UiEvent()
}

// Repository
class UserRepository @Inject constructor(
    private val remoteDataSource: UserRemoteDataSource,
    private val localDataSource: UserLocalDataSource
) {
    fun getUsers(): Flow<List<User>> = flow {
        // 缓存优先策略
        val cached = localDataSource.getUsers()
        if (cached.isNotEmpty()) emit(cached)

        try {
            val fresh = remoteDataSource.getUsers()
            localDataSource.saveUsers(fresh)
            emit(fresh)
        } catch (e: Exception) {
            if (cached.isEmpty()) throw e
        }
    }

    suspend fun getUser(id: Long): User {
        return localDataSource.getUser(id) ?: remoteDataSource.getUser(id).also {
            localDataSource.saveUser(it)
        }
    }
}

// ViewModel
@HiltViewModel
class UserListViewModel @Inject constructor(
    private val repo: UserRepository
) : ViewModel() {

    private val _state = MutableStateFlow<UiState<List<User>>>(UiState.Loading)
    val state: StateFlow<UiState<List<User>>> = _state.asStateFlow()

    private val _events = Channel<UiEvent>(Channel.BUFFERED)
    val events = _events.receiveAsFlow()

    init {
        loadUsers()
    }

    fun loadUsers() {
        viewModelScope.launch {
            repo.getUsers()
                .onStart { _state.value = UiState.Loading }
                .catch { e ->
                    _state.value = UiState.Error(e.message ?: "Unknown")
                }
                .collect { users ->
                    _state.value = if (users.isEmpty()) UiState.Empty else UiState.Success(users)
                }
        }
    }

    fun onUserClick(user: User) {
        viewModelScope.launch {
            _events.send(UiEvent.Navigate("detail/${user.id}"))
        }
    }
}

// Fragment 中使用
class UserListFragment : Fragment(R.layout.fragment_user_list) {
    private val viewModel: UserListViewModel by viewModels()
    private lateinit var binding: FragmentUserListBinding

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        binding = FragmentUserListBinding.bind(view)

        viewLifecycleOwner.lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                launch {
                    viewModel.state.collect { state ->
                        when (state) {
                            UiState.Loading -> {
                                binding.progressBar.isVisible = true
                                binding.recyclerView.isVisible = false
                            }
                            is UiState.Success -> {
                                binding.progressBar.isVisible = false
                                binding.recyclerView.isVisible = true
                                adapter.submitList(state.data)
                            }
                            is UiState.Error -> {
                                binding.progressBar.isVisible = false
                                binding.tvError.text = state.message
                                binding.tvError.isVisible = true
                            }
                            UiState.Empty -> {
                                binding.tvEmpty.isVisible = true
                            }
                        }
                    }
                }

                launch {
                    viewModel.events.collect { event ->
                        when (event) {
                            is UiEvent.ShowToast -> Toast.makeText(requireContext(), event.message, Toast.LENGTH_SHORT).show()
                            is UiEvent.Navigate -> findNavController().navigate(event.route)
                            is UiEvent.NavigateBack -> findNavController().popBackStack()
                        }
                    }
                }
            }
        }
    }
}

MVI 架构

// Intent(用户意图)
sealed class UserIntent {
    data object LoadUsers : UserIntent()
    data class UserClicked(val userId: Long) : UserIntent()
    data class SearchUsers(val query: String) : UserIntent()
}

// State(状态)
data class UserState(
    val isLoading: Boolean = false,
    val users: List<User> = emptyList(),
    val error: String? = null,
    val searchQuery: String = ""
)

// ViewModel
@HiltViewModel
class UserViewModel @Inject constructor(
    private val repo: UserRepository
) : ViewModel() {

    private val _state = MutableStateFlow(UserState())
    val state: StateFlow<UserState> = _state.asStateFlow()

    fun handleIntent(intent: UserIntent) {
        when (intent) {
            UserIntent.LoadUsers -> loadUsers()
            is UserIntent.UserClicked -> navigateToDetail(intent.userId)
            is UserIntent.SearchUsers -> searchUsers(intent.query)
        }
    }

    private fun loadUsers() {
        viewModelScope.launch {
            _state.update { it.copy(isLoading = true) }
            try {
                val users = repo.getAllUsers()
                _state.update { it.copy(isLoading = false, users = users) }
            } catch (e: Exception) {
                _state.update { it.copy(isLoading = false, error = e.message) }
            }
        }
    }
}

// 在 Compose 中使用
@Composable
fun UserScreen(viewModel: UserViewModel = hiltViewModel()) {
    val state by viewModel.state.collectAsState()

    LaunchedEffect(Unit) {
        viewModel.handleIntent(UserIntent.LoadUsers)
    }

    UserContent(
        state = state,
        onUserClick = { viewModel.handleIntent(UserIntent.UserClicked(it.id)) },
        onSearch = { viewModel.handleIntent(UserIntent.SearchUsers(it)) }
    )
}

Clean Architecture

// 分层:Presentation → Domain → Data

// Domain 层(纯 Kotlin,无 Android 依赖)
// UseCase
class GetUsersUseCase @Inject constructor(
    private val repo: UserRepository
) {
    operator fun invoke(): Flow<Result<List<User>>> = flow {
        emit(Result.Loading)
        try {
            val users = repo.getUsers()
            emit(Result.Success(users))
        } catch (e: Exception) {
            emit(Result.Error(e))
        }
    }
}

// Domain 接口
interface UserRepository {
    fun getUsers(): Flow<List<User>>
    suspend fun getUser(id: Long): User
}

// Domain 实体
data class User(
    val id: Long,
    val name: String,
    val email: String
)

// Data 层实现
class UserRepositoryImpl @Inject constructor(
    private val remote: UserRemoteDataSource,
    private val local: UserLocalDataSource,
    private val mapper: UserMapper
) : UserRepository {
    override fun getUsers(): Flow<List<User>> = flow {
        val localUsers = local.getUsers().map(mapper::toDomain)
        if (localUsers.isNotEmpty()) emit(localUsers)

        val remoteUsers = remote.getUsers().map(mapper::toDomain)
        local.saveUsers(remoteUsers.map(mapper::toEntity))
        emit(remoteUsers)
    }
}

// Presentation 层依赖 Domain UseCase
@HiltViewModel
class UserViewModel @Inject constructor(
    private val getUsersUseCase: GetUsersUseCase
) : ViewModel() {
    val users = getUsersUseCase().stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000),
        initialValue = Result.Loading
    )
}

推荐的目录结构

app/src/main/java/com/example/myapp/
├── di/                        # Hilt 模块
│   ├── NetworkModule.kt
│   ├── DatabaseModule.kt
│   └── RepositoryModule.kt
├── domain/                    # Domain 层
│   ├── model/
│   │   └── User.kt
│   ├── repository/
│   │   └── UserRepository.kt
│   └── usecase/
│       └── GetUsersUseCase.kt
├── data/                      # Data 层
│   ├── remote/
│   │   ├── ApiService.kt
│   │   ├── dto/
│   │   └── UserRemoteDataSource.kt
│   ├── local/
│   │   ├── AppDatabase.kt
│   │   ├── dao/
│   │   ├── entity/
│   │   └── UserLocalDataSource.kt
│   └── repository/
│       └── UserRepositoryImpl.kt
├── presentation/              # Presentation 层
│   ├── common/
│   │   ├── UiState.kt
│   │   └── UiEvent.kt
│   ├── user/
│   │   ├── UserListFragment.kt
│   │   ├── UserListViewModel.kt
│   │   └── UserAdapter.kt
│   └── detail/
│       ├── DetailFragment.kt
│       └── DetailViewModel.kt
└── util/                      # 工具类
    ├── Extensions.kt
    └── Constants.kt

25. 依赖注入 Hilt

▼

什么是依赖注入

依赖注入(DI)是一种设计模式,对象不再自己创建依赖,而是从外部注入。Hilt 是 Google 官方推荐的 Android DI 库,基于 Dagger。

优势

  • 代码解耦,易于测试
  • 自动管理依赖的生命周期
  • 与 Jetpack 组件深度集成
  • 编译时校验,避免运行时错误

添加依赖

// 项目 build.gradle
plugins {
    id 'com.google.dagger.hilt.android' version '2.50' apply false
}

// app build.gradle
plugins {
    id 'com.google.dagger.hilt.android'
    id 'com.google.devtools.ksp'  // 或 kapt
}

dependencies {
    implementation 'com.google.dagger:hilt-android:2.50'
    ksp 'com.google.dagger:hilt-compiler:2.50'  // 或 kapt
    implementation 'androidx.hilt:hilt-navigation-compose:1.1.0'
}

// Application 类
@HiltAndroidApp
class MyApplication : Application()

基本用法

// @Inject 构造注入(推荐)
class UserRepository @Inject constructor(
    private val api: ApiService,
    private val db: AppDatabase
) {
    fun getUsers() = api.getUsers()
}

// @HiltViewModel 注入 ViewModel
@HiltViewModel
class UserViewModel @Inject constructor(
    private val repo: UserRepository
) : ViewModel() {
    val users = repo.getUsers()
}

// Fragment/Activity 注入
@AndroidEntryPoint
class UserListFragment : Fragment() {
    private val viewModel: UserViewModel by viewModels()
    // 也可以直接注入
    @Inject lateinit var analytics: AnalyticsTracker
}

// Service / BroadcastReceiver 注入
@AndroidEntryPoint
class MyService : Service() {
    @Inject lateinit var repo: UserRepository
}

@AndroidEntryPoint
class MyReceiver : BroadcastReceiver() {
    @Inject lateinit var repo: UserRepository
}

@Module 提供依赖

@Module
@InstallIn(SingletonComponent::class)  // 作用域
object NetworkModule {

    @Provides
    @Singleton
    fun provideOkHttpClient(): OkHttpClient {
        return OkHttpClient.Builder()
            .addInterceptor(HttpLoggingInterceptor())
            .build()
    }

    @Provides
    @Singleton
    fun provideRetrofit(client: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com/")
            .client(client)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
    }

    @Provides
    @Singleton
    fun provideApiService(retrofit: Retrofit): ApiService {
        return retrofit.create(ApiService::class.java)
    }
}

@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {

    @Provides
    @Singleton
    fun provideDatabase(@ApplicationContext context: Context): AppDatabase {
        return Room.databaseBuilder(context, AppDatabase::class.java, "app.db").build()
    }

    @Provides
    fun provideUserDao(db: AppDatabase): UserDao = db.userDao()
}

// 接口绑定
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {

    @Binds
    @Singleton
    abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository
}

Hilt 作用域(Components)

Component生命周期对应注解
SingletonComponentApplication@Singleton
ActivityComponentActivity@ActivityScoped
FragmentComponentFragment@FragmentScoped
ViewComponentView@ViewScoped
ServiceComponentService@ServiceScoped
ViewModelComponentViewModel@ViewModelScoped
@Module
@InstallIn(ActivityComponent::class)
object ActivityModule {

    @Provides
    @ActivityScoped
    fun provideActivitySpecificDep(@ActivityContext context: Context): Something {
        return Something(context)
    }
}

Qualifier(限定符)

// 多个相同类型的依赖
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthInterceptorOkHttp

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class LoggingInterceptorOkHttp

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @AuthInterceptorOkHttp
    @Provides
    fun provideAuthClient(): OkHttpClient { /* ... */ }

    @LoggingInterceptorOkHttp
    @Provides
    fun provideLoggingClient(): OkHttpClient { /* ... */ }
}

// 使用
class ApiService @Inject constructor(
    @AuthInterceptorOkHttp private val client: OkHttpClient
)

多模块项目

// 在子模块中
@Module
@InstallIn(SingletonComponent::class)
@Module(includes = [CoreModule::class])  // 包含其他模块
object FeatureModule {
    // ...
}

// Entry Point(非 Hilt 管理的类中获取依赖)
@EntryPoint
@InstallIn(SingletonComponent::class)
interface AnalyticsEntryPoint {
    fun analytics(): Analytics
}

class NonHiltClass {
    fun doSomething(context: Context) {
        val entryPoint = EntryPointAccessors.fromApplication(
            context.applicationContext,
            AnalyticsEntryPoint::class.java
        )
        val analytics = entryPoint.analytics()
    }
}

💡

提示:

Hilt 与 Compose 配合使用

hiltViewModel()

函数获取 ViewModel。

26. 动画与转场

▼

View 动画

属性动画 (ObjectAnimator)

// 位移
ObjectAnimator.ofFloat(view, "translationX", 0f, 200f).apply {
    duration = 500
    interpolator = AccelerateDecelerateInterpolator()
    start()
}

// 缩放
ObjectAnimator.ofFloat(view, "scaleX", 1f, 1.5f).start()

// 旋转
ObjectAnimator.ofFloat(view, "rotation", 0f, 360f).apply {
    duration = 1000
    repeatCount = ObjectAnimator.INFINITE
    repeatMode = ObjectAnimator.REVERSE
    start()
}

// 透明度
ObjectAnimator.ofFloat(view, "alpha", 1f, 0f).start()

// 组合动画
val animatorSet = AnimatorSet().apply {
    playTogether(
        ObjectAnimator.ofFloat(view, "translationX", 0f, 200f),
        ObjectAnimator.ofFloat(view, "alpha", 1f, 0.5f),
        ObjectAnimator.ofFloat(view, "rotation", 0f, 45f)
    )
    duration = 1000
    start()
}

// 链式动画(PropertyValuesHolder)
val scaleX = PropertyValuesHolder.ofFloat("scaleX", 1f, 1.5f, 1f)
val scaleY = PropertyValuesHolder.ofFloat("scaleY", 1f, 1.5f, 1f)
ObjectAnimator.ofPropertyValuesHolder(view, scaleX, scaleY).apply {
    duration = 500
    start()
}

// 使用 ViewPropertyAnimator(简化版)
view.animate()
    .translationX(200f)
    .alpha(0.5f)
    .rotation(45f)
    .setDuration(500)
    .setInterpolator(AccelerateDecelerateInterpolator())
    .withEndAction { /* 动画结束 */ }
    .start()

XML 动画资源

<!-- res/anim/slide_in_right.xml -->
<set xmlns:android="http://schemas.android.com/apk/res/android">
    <translate
        android:fromXDelta="100%p"
        android:toXDelta="0"
        android:duration="300"/>
    <alpha
        android:fromAlpha="0"
        android:toAlpha="1"
        android:duration="300"/>
</set>

<!-- res/anim/fade_out.xml -->
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:fromAlpha="1"
    android:toAlpha="0"
    android:duration="300"/>

// 代码中使用
val anim = AnimationUtils.loadAnimation(context, R.anim.slide_in_right)
view.startAnimation(anim)

// Activity 转场动画
overridePendingTransition(R.anim.slide_in_right, R.anim.slide_out_left)

Transition 框架(布局变化动画)

// 布局变化时自动动画
val transition = TransitionSet().apply {
    addTransition(ChangeBounds())
    addTransition(Fade(Fade.MODE_IN))
    addTransition(Slide(Gravity.BOTTOM))
    duration = 300
}

TransitionManager.beginDelayedTransition(container, transition)
// 修改布局
view.visibility = View.GONE
textView.text = "新内容"

// Scene 场景切换
val scene1 = Scene.getSceneForLayout(container, R.layout.layout_1, context)
val scene2 = Scene.getSceneForLayout(container, R.layout.layout_2, context)
TransitionManager.go(scene2, AutoTransition())

MotionLayout(高级布局动画)

<!-- layout/activity_motion.xml -->
<androidx.constraintlayout.motion.widget.MotionLayout
    android:id="@+id/motionLayout"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layoutDescription="@xml/motion_scene">

    <ImageView
        android:id="@+id/image"
        android:layout_width="200dp"
        android:layout_height="200dp"
        android:src="@drawable/photo"/>
</androidx.constraintlayout.motion.widget.MotionLayout>

<!-- xml/motion_scene.xml -->
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <Transition
        app:constraintSetStart="@id/start"
        app:constraintSetEnd="@id/end"
        app:duration="500">

        <OnSwipe
            app:touchAnchorId="@id/image"
            app:dragDirection="dragUp"/>

        <KeyFrameSet>
            <KeyAttribute
                app:framePosition="50"
                app:motionTarget="@id/image">
                <CustomAttribute
                    app:attributeName="rotation"
                    app:customFloatValue="180"/>
            </KeyAttribute>
        </KeyFrameSet>
    </Transition>

    <ConstraintSet android:id="@+id/start">
        <Constraint android:id="@id/image"
            android:layout_width="200dp"
            android:layout_height="200dp"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"/>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint android:id="@id/image"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    </ConstraintSet>
</MotionScene>

// 代码控制
binding.motionLayout.transitionToStart()
binding.motionLayout.transitionToEnd()
binding.motionLayout.progress = 0.5f

共享元素转场

// Activity A 启动 Activity B
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
    this,
    Pair(imageView, "shared_image"),
    Pair(titleView, "shared_title")
)
startActivity(intent, options.toBundle())

// 在两个 Activity 的 XML 中设置相同的 transitionName
<ImageView
    android:transitionName="shared_image"
    ... />

<TextView
    android:transitionName="shared_title"
    ... />

// 启用 Activity 转场
// values/styles.xml
<item name="android:windowActivityTransitions">true</item>
<item name="android:windowContentTransitions">true</item>

Compose 动画

// animate*AsState
@Composable
fun AnimatedBox() {
    var expanded by remember { mutableStateOf(false) }
    val size by animateDpAsState(
        targetValue = if (expanded) 200.dp else 100.dp,
        animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy)
    )

    Box(
        modifier = Modifier
            .size(size)
            .background(Color.Blue)
            .clickable { expanded = !expanded }
    )
}

// infiniteTransition
@Composable
fun RotatingIcon() {
    val infiniteTransition = rememberInfiniteTransition(label = "rotate")
    val rotation by infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 360f,
        animationSpec = infiniteRepeatable(
            animation = tween(2000, easing = LinearEasing),
            repeatMode = RepeatMode.Restart
        ),
        label = "rotation"
    )

    Icon(
        imageVector = Icons.Default.Refresh,
        contentDescription = null,
        modifier = Modifier.rotate(rotation)
    )
}

// AnimatedVisibility
@Composable
fun AnimatedContent() {
    var visible by remember { mutableStateOf(true) }

    AnimatedVisibility(
        visible = visible,
        enter = slideInVertically() + fadeIn(),
        exit = slideOutVertically() + fadeOut()
    ) {
        Text("Hello")
    }

    Button(onClick = { visible = !visible }) {
        Text("Toggle")
    }
}

// updateTransition(多个属性同步动画)
@Composable
fun MultiPropertyAnimation() {
    var selected by remember { mutableStateOf(false) }
    val transition = updateTransition(selected, label = "box")

    val size by transition.animateDp(label = "size") { if (it) 200.dp else 100.dp }
    val color by transition.animateColor(label = "color") { if (it) Color.Red else Color.Blue }

    Box(
        modifier = Modifier
            .size(size)
            .background(color)
            .clickable { selected = !selected }
    )
}

Lottie 动画

implementation 'com.airbnb.android:lottie:6.3.0'

<com.airbnb.lottie.LottieAnimationView
    android:id="@+id/lottieView"
    android:layout_width="200dp"
    android:layout_height="200dp"
    app:lottie_rawRes="@raw/loading"
    app:lottie_autoPlay="true"
    app:lottie_loop="true"/>

// 代码控制
binding.lottieView.setAnimation("loading.json")  // assets 目录
binding.lottieView.playAnimation()
binding.lottieView.pauseAnimation()
binding.lottieView.cancelAnimation()
binding.lottieView.speed = 2f
binding.lottieView.repeatCount = LottieDrawable.INFINITE

// 远程 URL
binding.lottieView.setAnimationFromUrl("https://example.com/anim.json")

// Compose
LottieAnimation(
    composition = composition,
    iterations = LottieConstants.IterateForever
)

27. 权限详解

▼

Android 权限分类

类型说明示例
普通权限安装时自动授予INTERNET, ACCESS_NETWORK_STATE
危险权限运行时需要用户授权CAMERA, LOCATION, CONTACTS
签名权限仅同签名应用可获得系统级权限
特殊权限需要跳转设置页面授权SYSTEM_ALERT_WINDOW, MANAGE_EXTERNAL_STORAGE

危险权限分组(API 33+)

权限组权限
CALENDARREAD_CALENDAR, WRITE_CALENDAR
CAMERACAMERA
CONTACTSREAD_CONTACTS, WRITE_CONTACTS
LOCATIONACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION
MICROPHONERECORD_AUDIO
PHONECALL_PHONE, READ_CALL_LOG
SENSORSBODY_SENSORS
SMSSEND_SMS, READ_SMS
STORAGEREAD/WRITE_EXTERNAL_STORAGE(API 28+)
NOTIFICATIONSPOST_NOTIFICATIONS(API 33+)

完整的运行时权限请求流程

class PermissionHelper(
    private val activity: ComponentActivity
) {
    private val requestPermissionLauncher = activity.registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { isGranted ->
        permissionCallback?.invoke(isGranted)
    }

    private val requestMultiplePermissionsLauncher = activity.registerForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) { result ->
        multiplePermissionsCallback?.invoke(result)
    }

    private var permissionCallback: ((Boolean) -> Unit)? = null
    private var multiplePermissionsCallback: ((Map<String, Boolean>) -> Unit)? = null

    fun requestPermission(
        permission: String,
        rationaleTitle: String = "",
        rationaleMessage: String = "",
        onResult: (Boolean) -> Unit
    ) {
        permissionCallback = onResult

        when {
            // 已授权
            ContextCompat.checkSelfPermission(activity, permission) == PackageManager.PERMISSION_GRANTED -> {
                onResult(true)
            }
            // 需要显示解释
            activity.shouldShowRequestPermissionRationale(permission) -> {
                MaterialAlertDialogBuilder(activity)
                    .setTitle(rationaleTitle)
                    .setMessage(rationaleMessage)
                    .setPositiveButton("确定") { _, _ ->
                        requestPermissionLauncher.launch(permission)
                    }
                    .setNegativeButton("取消") { _, _ ->
                        onResult(false)
                    }
                    .show()
            }
            // 直接请求
            else -> {
                requestPermissionLauncher.launch(permission)
            }
        }
    }

    fun requestMultiplePermissions(
        permissions: Array<String>,
        onResult: (Map<String, Boolean>) -> Unit
    ) {
        multiplePermissionsCallback = onResult
        requestMultiplePermissionsLauncher.launch(permissions)
    }

    fun openAppSettings() {
        val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
            data = Uri.fromParts("package", activity.packageName, null)
        }
        activity.startActivity(intent)
    }
}

// 使用
val permissionHelper = PermissionHelper(this)

permissionHelper.requestPermission(
    permission = Manifest.permission.CAMERA,
    rationaleTitle = "需要相机权限",
    rationaleMessage = "应用需要使用相机拍照,请授予权限"
) { granted ->
    if (granted) {
        openCamera()
    } else {
        // 检查是否"不再询问"
        if (!shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
            showGoToSettingsDialog()
        }
    }
}

常见权限请求示例

// 拍照权限
Manifest.permission.CAMERA

// 位置权限
Manifest.permission.ACCESS_FINE_LOCATION  // 精确
Manifest.permission.ACCESS_COARSE_LOCATION  // 粗略
// API 34+ 后台位置
Manifest.permission.ACCESS_BACKGROUND_LOCATION  // 需单独请求

// 通知权限(API 33+)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    permissionHelper.requestPermission(Manifest.permission.POST_NOTIFICATIONS) { granted ->
        if (granted) showNotification()
    }
}

// 读取相册(API 33+)
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    arrayOf(Manifest.permission.READ_MEDIA_IMAGES, Manifest.permission.READ_MEDIA_VIDEO)
} else {
    arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE)
}

// 录音权限
Manifest.permission.RECORD_AUDIO

// 悬浮窗(特殊权限)
if (!Settings.canDrawOverlays(this)) {
    val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
        Uri.parse("package:$packageName"))
    startActivityForResult(intent, REQUEST_CODE_OVERLAY)
}

// 所有文件访问权限(API 30+)
if (!Environment.isExternalStorageManager()) {
    val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
        Uri.parse("package:$packageName"))
    startActivity(intent)
}

Android 各版本权限变化

版本关键变化
6.0 (23)引入运行时权限
8.0 (26)隐式广播限制
10 (29)分区存储(Scoped Storage)
11 (30)一次性权限、后台位置限制
12 (31)蓝牙权限拆分、前台服务限制
13 (33)通知权限、媒体权限细分
14 (34)后台活动限制增强

⚠️

最佳实践:

在用户真正需要时才请求权限,并提供清晰的理由说明。

28. 相机与多媒体

▼

CameraX(推荐)

CameraX 是 Google 提供的相机库,简化相机开发,兼容性好。

implementation 'androidx.camera:camera-core:1.3.1'
implementation 'androidx.camera:camera-camera2:1.3.1'
implementation 'androidx.camera:camera-lifecycle:1.3.1'
implementation 'androidx.camera:camera-view:1.3.1'

布局文件

<androidx.camera.view.PreviewView
    android:id="@+id/previewView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:scaleType="fillCenter"/>

<Button
    android:id="@+id/btnCapture"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="拍照"/>

Activity 代码

class CameraActivity : AppCompatActivity() {
    private lateinit var binding: ActivityCameraBinding
    private var imageCapture: ImageCapture? = null
    private lateinit var cameraExecutor: ExecutorService

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityCameraBinding.inflate(layoutInflater)
        setContentView(binding.root)

        if (allPermissionsGranted()) {
            startCamera()
        } else {
            requestPermissions()
        }

        binding.btnCapture.setOnClickListener { takePhoto() }
        cameraExecutor = Executors.newSingleThreadExecutor()
    }

    private fun startCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)

        cameraProviderFuture.addListener({
            val cameraProvider = cameraProviderFuture.get()

            // 预览
            val preview = Preview.Builder()
                .build()
                .also {
                    it.setSurfaceProvider(binding.previewView.surfaceProvider)
                }

            // 拍照
            imageCapture = ImageCapture.Builder()
                .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
                .setFlashMode(ImageCapture.FLASH_MODE_AUTO)
                .build()

            // 图像分析
            val imageAnalyzer = ImageAnalysis.Builder()
                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                .build()
                .also {
                    it.setAnalyzer(cameraExecutor) { imageProxy ->
                        processImage(imageProxy)
                    }
                }

            // 选择后置摄像头
            val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA

            try {
                cameraProvider.unbindAll()
                cameraProvider.bindToLifecycle(
                    this,
                    cameraSelector,
                    preview,
                    imageCapture,
                    imageAnalyzer
                )
            } catch (e: Exception) {
                Log.e("Camera", "绑定失败", e)
            }
        }, ContextCompat.getMainExecutor(this))
    }

    private fun takePhoto() {
        val imageCapture = imageCapture ?: return

        val photoFile = File(
            getExternalFilesDir(Environment.DIRECTORY_PICTURES),
            "photo_${System.currentTimeMillis()}.jpg"
        )

        val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()

        imageCapture.takePicture(
            outputOptions,
            ContextCompat.getMainExecutor(this),
            object : ImageCapture.OnImageSavedCallback {
                override fun onImageSaved(output: ImageCapture.OutputFileResults) {
                    val savedUri = output.savedUri ?: Uri.fromFile(photoFile)
                    // 保存到相册
                    MediaScannerConnection.scanFile(
                        this@CameraActivity,
                        arrayOf(photoFile.absolutePath),
                        null, null
                    )
                    Toast.makeText(this@CameraActivity, "照片已保存", Toast.LENGTH_SHORT).show()
                }

                override fun onError(exc: ImageCaptureException) {
                    Log.e("Camera", "拍照失败: ${exc.message}", exc)
                }
            }
        )
    }

    private fun processImage(imageProxy: ImageProxy) {
        val mediaImage = imageProxy.image
        if (mediaImage != null) {
            val inputImage = InputImage.fromMediaImage(
                mediaImage,
                imageProxy.imageInfo.rotationDegrees
            )
            // 使用 ML Kit 进行人脸检测等
        }
        imageProxy.close()
    }

    override fun onDestroy() {
        super.onDestroy()
        cameraExecutor.shutdown()
    }

    private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
        ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED
    }

    companion object {
        private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
    }
}

ExoPlayer / Media3 视频播放

implementation 'androidx.media3:media3-exoplayer:1.2.1'
implementation 'androidx.media3:media3-ui:1.2.1'

<androidx.media3.ui.PlayerView
    android:id="@+id/playerView"
    android:layout_width="match_parent"
    android:layout_height="250dp"
    app:show_buffering="when_playing"/>

class VideoActivity : AppCompatActivity() {
    private var player: ExoPlayer? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_video)
        initializePlayer()
    }

    private fun initializePlayer() {
        player = ExoPlayer.Builder(this).build().also { exoPlayer ->
            binding.playerView.player = exoPlayer

            val mediaItem = MediaItem.fromUri("https://example.com/video.mp4")
            exoPlayer.setMediaItem(mediaItem)

            // 监听
            exoPlayer.addListener(object : Player.Listener {
                override fun onPlaybackStateChanged(playbackState: Int) {
                    when (playbackState) {
                        Player.STATE_BUFFERING -> showLoading()
                        Player.STATE_READY -> hideLoading()
                        Player.STATE_ENDED -> replay()
                    }
                }
            })

            exoPlayer.prepare()
            exoPlayer.playWhenReady = true
        }
    }

    override fun onPause() {
        super.onPause()
        player?.pause()
    }

    override fun onDestroy() {
        super.onDestroy()
        player?.release()
        player = null
    }
}

音频播放 (MediaPlayer)

val mediaPlayer = MediaPlayer().apply {
    setAudioAttributes(
        AudioAttributes.Builder()
            .setContentType(AudioAttributes.CONTENT_TYPE_MUSIC)
            .setUsage(AudioAttributes.USAGE_MEDIA)
            .build()
    )
    setDataSource("https://example.com/music.mp3")
    setOnPreparedListener { it.start() }
    setOnCompletionListener { /* 播放完成 */ }
    prepareAsync()
}

mediaPlayer.pause()
mediaPlayer.seekTo(30000)  // 跳到 30 秒
mediaPlayer.release()

// 播放本地资源
val mediaPlayer = MediaPlayer.create(context, R.raw.sound)
mediaPlayer.start()

图片选择器 (Photo Picker)

// API 33+ 的系统图片选择器(无需权限)
val pickImagesLauncher = registerForActivityResult(
    ActivityResultContracts.PickMultipleVisualMedia(maxItems = 5)
) { uris ->
    uris.forEach { uri ->
        // 处理选中的图片
        imageView.setImageURI(uri)
    }
}

pickImagesLauncher.launch(
    PickVisualMediaRequest(ActivityResultContracts.PickVisualMedia.ImageOnly)
)

// 单张图片
val pickImageLauncher = registerForActivityResult(
    ActivityResultContracts.PickVisualMedia()
) { uri ->
    uri?.let { imageView.setImageURI(it) }
}

29. 位置与地图

▼

获取位置信息

implementation 'com.google.android.gms:play-services-location:21.1.0'

class LocationHelper(private val context: Context) {

    private val fusedLocationClient: FusedLocationProviderClient =
        LocationServices.getFusedLocationProviderClient(context)

    // 获取当前位置(单次)
    @SuppressLint("MissingPermission")
    fun getCurrentLocation(onResult: (Location?) -> Unit) {
        val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, 1000)
            .setMinUpdateIntervalMillis(500)
            .build()

        fusedLocationClient.getCurrentLocation(Priority.PRIORITY_HIGH_ACCURACY, null)
            .addOnSuccessListener { location ->
                onResult(location)
            }
            .addOnFailureListener { e ->
                Log.e("Location", "获取位置失败", e)
                onResult(null)
            }
    }

    // 持续监听位置
    @SuppressLint("MissingPermission")
    fun startLocationUpdates(
        interval: Long = 5000,
        onLocation: (Location) -> Unit
    ) {
        val locationRequest = LocationRequest.Builder(Priority.PRIORITY_HIGH_ACCURACY, interval)
            .setMinUpdateIntervalMillis(interval / 2)
            .build()

        val locationCallback = object : LocationCallback() {
            override fun onLocationResult(result: LocationResult) {
                result.lastLocation?.let { onLocation(it) }
            }
        }

        fusedLocationClient.requestLocationUpdates(
            locationRequest,
            locationCallback,
            Looper.getMainLooper()
        )
    }

    // 地理编码(地址 → 坐标)
    suspend fun getLatLngFromAddress(address: String): LatLng? = withContext(Dispatchers.IO) {
        val geocoder = Geocoder(context)
        try {
            val results = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                var result: List<Address>? = null
                geocoder.getFromLocationName(address, 1) { list ->
                    result = list
                }
                result
            } else {
                @Suppress("DEPRECATION")
                geocoder.getFromLocationName(address, 1)
            }
            results?.firstOrNull()?.let { LatLng(it.latitude, it.longitude) }
        } catch (e: Exception) {
            null
        }
    }

    // 反向地理编码(坐标 → 地址)
    suspend fun getAddressFromLatLng(lat: Double, lng: Double): String? = withContext(Dispatchers.IO) {
        val geocoder = Geocoder(context)
        try {
            val results = geocoder.getFromLocation(lat, lng, 1)
            results?.firstOrNull()?.getAddressLine(0)
        } catch (e: Exception) {
            null
        }
    }
}

Google Maps SDK

// 在 Google Cloud Console 获取 API Key
// AndroidManifest.xml
<meta-data
    android:name="com.google.android.geo.API_KEY"
    android:value="YOUR_API_KEY"/>

<!-- 布局 -->
<androidx.fragment.app.FragmentContainerView
    android:id="@+id/map"
    android:name="com.google.android.gms.maps.SupportMapFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

class MapsActivity : AppCompatActivity(), OnMapReadyCallback {
    private lateinit var googleMap: GoogleMap

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_maps)

        val mapFragment = supportFragmentManager
            .findFragmentById(R.id.map) as SupportMapFragment
        mapFragment.getMapAsync(this)
    }

    override fun onMapReady(map: GoogleMap) {
        googleMap = map

        // 添加标记
        val beijing = LatLng(39.9042, 116.4074)
        googleMap.addMarker(
            MarkerOptions()
                .position(beijing)
                .title("北京")
                .snippet("中国首都")
                .icon(BitmapDescriptorFactory.defaultMarker(BitmapDescriptorFactory.HUE_RED))
        )

        // 移动相机
        googleMap.moveCamera(CameraUpdateFactory.newLatLngZoom(beijing, 12f))

        // 设置地图类型
        googleMap.mapType = GoogleMap.MAP_TYPE_NORMAL  // SATELLITE, TERRAIN, HYBRID

        // 启用我的位置按钮
        googleMap.isMyLocationEnabled = true
        googleMap.uiSettings.isZoomControlsEnabled = true
        googleMap.uiSettings.isCompassEnabled = true

        // 标记点击监听
        googleMap.setOnMarkerClickListener { marker ->
            Toast.makeText(this, marker.title, Toast.LENGTH_SHORT).show()
            false
        }

        // 地图点击监听
        googleMap.setOnMapClickListener { latLng ->
            googleMap.addMarker(MarkerOptions().position(latLng).title("自定义标记"))
        }

        // 绘制多边形
        val polygonOptions = PolygonOptions()
            .add(LatLng(39.9, 116.3))
            .add(LatLng(39.9, 116.5))
            .add(LatLng(40.0, 116.5))
            .add(LatLng(40.0, 116.3))
            .fillColor(Color.parseColor("#40FF0000"))
            .strokeColor(Color.RED)
            .strokeWidth(5f)
        googleMap.addPolygon(polygonOptions)

        // 绘制路径
        val polylineOptions = PolylineOptions()
            .addAll(pathPoints)
            .color(Color.BLUE)
            .width(10f)
        googleMap.addPolyline(polylineOptions)
    }
}

高德地图(国内替代)

implementation 'com.amap.api:3dmap:9.8.0'
implementation 'com.amap.api:search:9.7.0'

// Manifest
<meta-data
    android:name="com.amap.api.v2.apikey"
    android:value="YOUR_AMAP_KEY"/>

<com.amap.api.maps.MapView
    android:id="@+id/mapView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

class AMapActivity : AppCompatActivity() {
    private lateinit var mapView: MapView
    private lateinit var aMap: AMap

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_amap)

        mapView = findViewById(R.id.mapView)
        mapView.onCreate(savedInstanceState)
        aMap = mapView.map

        // 添加标记
        aMap.addMarker(MarkerOptions().position(LatLng(39.9, 116.4)).title("北京"))
        aMap.moveCamera(CameraUpdateFactory.newLatLngZoom(LatLng(39.9, 116.4), 12f))
    }

    override fun onResume() { super.onResume(); mapView.onResume() }
    override fun onPause() { super.onPause(); mapView.onPause() }
    override fun onDestroy() { super.onDestroy(); mapView.onDestroy() }
}

30. 推送通知 FCM

▼

Firebase Cloud Messaging

FCM 是 Google 提供的免费跨平台消息推送服务。

接入步骤

  1. 在 Firebase Console 创建项目
  2. 添加 Android 应用,填写包名和 SHA-1
  3. 下载 google-services.json 放到 app/ 目录
  4. 添加依赖
// 项目 build.gradle
plugins {
    id 'com.google.gms.google-services' version '4.4.0' apply false
}

// app build.gradle
plugins {
    id 'com.google.gms.google-services'
}

dependencies {
    implementation platform('com.google.firebase:firebase-bom:32.7.1')
    implementation 'com.google.firebase:firebase-messaging'
    implementation 'com.google.firebase:firebase-analytics'
}

实现 FCM Service

class MyFirebaseService : FirebaseMessagingService() {

    // 收到消息
    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        Log.d("FCM", "From: ${remoteMessage.from}")

        // 数据消息
        if (remoteMessage.data.isNotEmpty()) {
            Log.d("FCM", "Data: ${remoteMessage.data}")
            handleDataMessage(remoteMessage.data)
        }

        // 通知消息
        remoteMessage.notification?.let { notification ->
            Log.d("FCM", "Title: ${notification.title}, Body: ${notification.body}")
            showNotification(notification.title, notification.body)
        }
    }

    // Token 刷新
    override fun onNewToken(token: String) {
        Log.d("FCM", "New token: $token")
        // 上传到服务器
        sendTokenToServer(token)
    }

    private fun showNotification(title: String?, body: String?) {
        createNotificationChannel()

        val intent = Intent(this, MainActivity::class.java).apply {
            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
        }
        val pendingIntent = PendingIntent.getActivity(
            this, 0, intent,
            PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
        )

        val notification = NotificationCompat.Builder(this, CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_notification)
            .setContentTitle(title ?: "新消息")
            .setContentText(body)
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .setContentIntent(pendingIntent)
            .setAutoCancel(true)
            .setStyle(NotificationCompat.BigTextStyle().bigText(body))
            .build()

        NotificationManagerCompat.from(this).apply {
            if (ActivityCompat.checkSelfPermission(
                    this@MyFirebaseService,
                    Manifest.permission.POST_NOTIFICATIONS
                ) == PackageManager.PERMISSION_GRANTED
            ) {
                notify(System.currentTimeMillis().toInt(), notification)
            }
        }
    }

    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                CHANNEL_ID,
                "消息通知",
                NotificationManager.IMPORTANCE_HIGH
            )
            val manager = getSystemService(NotificationManager::class.java)
            manager.createNotificationChannel(channel)
        }
    }

    companion object {
        private const val CHANNEL_ID = "fcm_channel"
    }
}

// Manifest
<service
    android:name=".MyFirebaseService"
    android:exported="false">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT"/>
    </intent-filter>
</service>

<!-- 默认通知图标 -->
<meta-data
    android:name="com.google.firebase.messaging.default_notification_icon"
    android:resource="@drawable/ic_notification"/>
<meta-data
    android:name="com.google.firebase.messaging.default_notification_color"
    android:resource="@color/colorAccent"/>
<meta-data
    android:name="com.google.firebase.messaging.default_notification_channel_id"
    android:value="fcm_channel"/>

获取 FCM Token

// 在 MainActivity 或 Application 中
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
    if (!task.isSuccessful) {
        Log.w("FCM", "获取 token 失败", task.exception)
        return@addOnCompleteListener
    }

    val token = task.result
    Log.d("FCM", "Token: $token")
    // 发送到服务器
    sendTokenToServer(token)
}

// 订阅主题
FirebaseMessaging.getInstance().subscribeToTopic("news")
    .addOnCompleteListener { task ->
        if (task.isSuccessful) {
            Log.d("FCM", "已订阅 news 主题")
        }
    }

// 取消订阅
FirebaseMessaging.getInstance().unsubscribeFromTopic("news")

本地通知

class NotificationHelper(private val context: Context) {

    private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

    init {
        createNotificationChannels()
    }

    private fun createNotificationChannels() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val highChannel = NotificationChannel(
                "high_priority",
                "重要通知",
                NotificationManager.IMPORTANCE_HIGH
            ).apply {
                description = "需要及时查看的通知"
                enableLights(true)
                lightColor = Color.RED
                enableVibration(true)
            }

            val lowChannel = NotificationChannel(
                "low_priority",
                "一般通知",
                NotificationManager.IMPORTANCE_LOW
            ).apply {
                description = "不重要的通知"
            }

            notificationManager.createNotificationChannels(listOf(highChannel, lowChannel))
        }
    }

    fun showNotification(
        title: String,
        content: String,
        channelId: String = "high_priority",
        bigText: Boolean = false
    ) {
        val notification = NotificationCompat.Builder(context, channelId)
            .setSmallIcon(R.drawable.ic_notification)
            .setContentTitle(title)
            .setContentText(content)
            .setAutoCancel(true)
            .apply {
                if (bigText) {
                    setStyle(NotificationCompat.BigTextStyle().bigText(content))
                }
            }
            .build()

        if (ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)
            == PackageManager.PERMISSION_GRANTED) {
            notificationManager.notify(System.currentTimeMillis().toInt(), notification)
        }
    }

    // 定时通知
    fun scheduleNotification(title: String, content: String, delayMillis: Long) {
        val intent = Intent(context, NotificationReceiver::class.java).apply {
            putExtra("title", title)
            putExtra("content", content)
        }
        val pendingIntent = PendingIntent.getBroadcast(
            context, 0, intent,
            PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
        )

        val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
        alarmManager.set(
            AlarmManager.RTC_WAKEUP,
            System.currentTimeMillis() + delayMillis,
            pendingIntent
        )
    }
}

国内推送替代方案

  • 华为 Push:HMS Core Push Kit
  • 小米 Push:MiPush
  • 极光推送:JPush(统一集成多家)
  • 个推:Getui
  • 阿里云推送:EMAS Push

31. 深度链接

▼

深度链接类型

  • Deep Links:自定义 scheme(如 myapp://path)
  • Web Links:http/https 链接
  • Android App Links:验证的 https 链接(自动打开,无需选择)

配置深度链接

<!-- Manifest -->
<activity android:name=".DetailActivity" android:exported="true">

    <!-- 自定义 scheme 深度链接 -->
    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data
            android:scheme="myapp"
            android:host="product"
            android:pathPrefix="/detail"/>
    </intent-filter>

    <!-- Android App 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="/product"/>
    </intent-filter>

    <!-- 分享文本 -->
    <intent-filter>
        <action android:name="android.intent.action.SEND"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:mimeType="text/plain"/>
    </intent-filter>
</activity>

处理深度链接

class DetailActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_detail)

        handleIntent(intent)
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        handleIntent(intent)
    }

    private fun handleIntent(intent: Intent) {
        val action = intent.action
        val data = intent.data

        when {
            // 深度链接
            Intent.ACTION_VIEW == action && data != null -> {
                when {
                    data.scheme == "myapp" && data.host == "product" -> {
                        val productId = data.lastPathSegment
                        val ref = data.getQueryParameter("ref")
                        loadProduct(productId, ref)
                    }
                    data.scheme == "https" && data.host == "www.example.com" -> {
                        val path = data.path
                        if (path?.startsWith("/product/") == true) {
                            val productId = path.substringAfter("/product/")
                            loadProduct(productId)
                        }
                    }
                }
            }
            // 分享文本
            Intent.ACTION_SEND == action && intent.type == "text/plain" -> {
                val sharedText = intent.getStringExtra(Intent.EXTRA_TEXT)
                handleSharedText(sharedText)
            }
        }
    }
}

// 使用示例
// 自定义 scheme: myapp://product/detail/123?ref=wechat
// App Links: https://www.example.com/product/123

验证 Android App Links

// 1. 在服务器配置 Digital Asset Links
// https://www.example.com/.well-known/assetlinks.json
[
    {
        "relation": ["delegate_permission/common"],
        "target": {
            "namespace": "android_app",
            "package_name": "com.example.myapp",
            "sha256_cert_fingerprints": [
                "14:6D:E9:83:C5:73:06:50:D8:EE:B9:95:2F:34:FC:64:16:A0:83:42:E6:1D:BE:A8:8A:04:96:B2:3F:CF:44:E5"
            ]
        }
    }
]

// 2. 获取 SHA-256 指纹
// Android Studio: Gradle → signingReport
// 或 keytool -list -v -keystore my-release-key.jks

// 3. 验证链接
adb shell pm verify-app-links --re-verify com.example.myapp
adb shell pm get-app-links com.example.myapp

// 4. 测试
adb shell am start -a android.intent.action.VIEW \
    -d "https://www.example.com/product/123" \
    com.example.myapp

Navigation Component 与深度链接

<!-- nav_graph.xml -->
<fragment
    android:id="@+id/detailFragment"
    android:name="com.example.DetailFragment">

    <deepLink
        android:id="@+id/deepLink"
        app:uri="myapp://product/{id}"
        app:uri="https://www.example.com/product/{id}"/>

    <argument
        android:name="id"
        app:argType="string"/>
</fragment>

// 编程式导航到深度链接
val deepLinkIntent = Intent(
    Intent.ACTION_VIEW,
    "myapp://product/123".toUri(),
    this,
    MainActivity::class.java
)
startActivity(deepLinkIntent)

// 创建 PendingIntent(用于通知)
val pendingIntent = NavDeepLinkBuilder(context)
    .setComponentName(MainActivity::class.java)
    .setGraph(R.navigation.nav_graph)
    .setDestination(R.id.detailFragment)
    .setArguments(bundleOf("id" to "123"))
    .createPendingIntent()

32. 图片加载库

▼

常用图片库对比

库语言特点
GlideJava/KotlinGoogle 推荐,功能全面
CoilKotlinKotlin 优先,Compose 友好
PicassoJava简单易用,Square 出品

Glide

implementation 'com.github.bumptech.glide:glide:4.16.0'
ksp 'com.github.bumptech.glide:ksp:4.16.0'

// 基础使用
Glide.with(context)
    .load("https://example.com/image.jpg")
    .into(imageView)

// 常用配置
Glide.with(context)
    .load(url)
    .placeholder(R.drawable.placeholder)  // 占位图
    .error(R.drawable.error)              // 错误图
    .fallback(R.drawable.fallback)        // null URL 时显示
    .centerCrop()                          // 裁剪方式
    .circleCrop()                          // 圆形
    .override(800, 600)                    // 指定尺寸
    .diskCacheStrategy(DiskCacheStrategy.ALL)
    .skipMemoryCache(false)
    .priority(Priority.HIGH)
    .thumbnail(0.1f)                       // 缩略图
    .transition(DrawableTransitionOptions.withCrossFade(300))
    .into(imageView)

// 监听加载状态
Glide.with(context)
    .load(url)
    .listener(object : RequestListener<Drawable> {
        override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
            Log.e("Glide", "加载失败: ${e?.message}")
            return false  // 返回 true 阻止 error 图显示
        }

        override fun onResourceReady(resource: Drawable, model: Any, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
            return false
        }
    })
    .into(imageView)

// 预加载
Glide.with(context)
    .load(url)
    .preload()  // 预加载到缓存

// 清除缓存
Glide.get(context).clearMemory()  // 主线程
Thread { Glide.get(context).clearDiskCache() }.start()  // 子线程

// 自定义 GlideModule
@GlideModule
class MyAppGlideModule : AppGlideModule() {
    override fun applyOptions(context: Context, builder: GlideBuilder) {
        builder.setDefaultRequestOptions(
            RequestOptions()
                .diskCacheStrategy(DiskCacheStrategy.ALL)
                .encodeQuality(90)
        )
    }

    override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
        // 注册自定义 ModelLoader
    }
}

// 生成 API
val glideUrl = GlideApp.with(context)
    .load(url)
    .placeholder(R.drawable.placeholder)
    .into(imageView)

Coil(Kotlin 推荐)

implementation 'io.coil-kt:coil:2.5.0'
implementation 'io.coil-kt:coil-compose:2.5.0'  // Compose
implementation 'io.coil-kt:coil-gif:2.5.0'       // GIF
implementation 'io.coil-kt:coil-svg:2.5.0'       // SVG
implementation 'io.coil-kt:coil-video:2.5.0'     // 视频帧

// 基础使用
imageView.load("https://example.com/image.jpg") {
    placeholder(R.drawable.placeholder)
    error(R.drawable.error)
    crossfade(true)
    transformations(CircleCropTransformation())
    size(800, 600)
}

// 链式调用
imageView.load(url) {
    crossfade(true)
    placeholder(R.drawable.placeholder)
    transformations(
        RoundedCornersTransformation(12f),
        BlurTransformation(context, 25f, 4f)
    )
}

// Compose 中使用
@Composable
fun ImageItem(url: String) {
    AsyncImage(
        model = ImageRequest.Builder(LocalContext.current)
            .data(url)
            .crossfade(true)
            .build(),
        placeholder = painterResource(R.drawable.placeholder),
        error = painterResource(R.drawable.error),
        contentDescription = "image",
        contentScale = ContentScale.Crop,
        modifier = Modifier.size(100.dp).clip(RoundedCornerShape(8.dp))
    )

    // 简化版
    AsyncImage(
        model = url,
        contentDescription = null
    )
}

// 图片加载器配置
val imageLoader = ImageLoader.Builder(context)
    .memoryCachePolicy(CachePolicy.ENABLED)
    .diskCachePolicy(CachePolicy.ENABLED)
    .networkCachePolicy(CachePolicy.ENABLED)
    .crossfade(true)
    .build()

// 设置全局
Coil.setImageLoader(imageLoader)

// 下载图片
val request = ImageRequest.Builder(context)
    .data(url)
    .target(
        onStart = { /* 开始 */ },
        onSuccess = { drawable -> /* 成功 */ },
        onError = { /* 失败 */ }
    )
    .build()

context.imageLoader.enqueue(request)

SubsamplingScaleImageView(大图加载)

implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'

<com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
    android:id="@+id/imageView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

binding.imageView.setImage(ImageSource.Uri(uri))
binding.imageView.setImage(ImageSource.Resource(R.drawable.large_map))
binding.imageView.setDoubleTapZoomScale(2f)
binding.imageView.setMaxScale(5f)

33. 安全最佳实践

▼

数据安全

EncryptedSharedPreferences(加密存储)

implementation 'androidx.security:security-crypto:1.1.0-alpha06'

val masterKey = MasterKey.Builder(context)
    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
    .build()

val securePrefs = EncryptedSharedPreferences.create(
    context,
    "secure_prefs",
    masterKey,
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)

securePrefs.edit {
    putString("token", accessToken)
    putString("password", password)
    apply()
}

EncryptedFile(加密文件)

val encryptedFile = EncryptedFile.Builder(
    context,
    File(context.filesDir, "secret_data.txt"),
    masterKey,
    EncryptedFile.FileEncryptionScheme.AES256_GCM_HKDF_4KB
).build()

// 写入
encryptedFile.openFileOutput().use { outputStream ->
    outputStream.write("sensitive data".toByteArray())
}

// 读取
encryptedFile.openFileInput().use { inputStream ->
    val content = inputStream.bufferedReader().readText()
}

网络安全

网络安全配置

<!-- res/xml/network_security_config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <!-- 默认只允许 HTTPS -->
    <base-config cleartextTrafficPermitted="false">
        <trust-anchors>
            <certificates src="system"/>
        </trust-anchors>
    </base-config>

    <!-- 开发环境允许 HTTP -->
    <debug-overrides>
        <trust-anchors>
            <certificates src="user"/>  <!-- 允许 Charles 等代理证书 -->
        </trust-anchors>
    </debug-overrides>

    <!-- 特定域名配置 -->
    <domain-config cleartextTrafficPermitted="false">
        <domain includeSubdomains="true">api.example.com</domain>
        <pin-set expiration="2025-12-31">
            <pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin>
            <pin digest="SHA-256">fwza0LRMXouZHRC8Ei+4PyuldPDcf3UKsO/04cY1bkg=</pin>
        </pin-set>
    </domain-config>
</network-security-config>

<!-- Manifest -->
<application
    android:networkSecurityConfig="@xml/network_security_config"
    ... />

证书固定 (Certificate Pinning)

// OkHttp 证书固定
val certificatePinner = CertificatePinner.Builder()
    .add("api.example.com", "sha256/7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=")
    .add("api.example.com", "sha256/fwza0LRMXouZHRC8Ei+4PyuldPDcf3UKsO/04cY1bkg=")
    .build()

val client = OkHttpClient.Builder()
    .certificatePinner(certificatePinner)
    .build()

代码安全

ProGuard / R8 混淆规则

# proguard-rules.pro

# 保留 Application
-keep public class * extends android.app.Application

# 保留 Activity
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Service

# 保留实体类(JSON 解析需要)
-keep class com.example.model.** { *; }

# 保留注解
-keepattributes *Annotation*
-keepattributes Signature

# Kotlin
-keep class kotlin.Metadata { *; }
-dontwarn kotlin.**
-keepclassmembers class **$WhenMappings {
    <fields>;
}

# Retrofit
-keep,allowobfuscation,allowshrinking interface retrofit2.Call
-keep,allowobfuscation,allowshrinking class retrofit2.Response
-keep,allowobfuscation,allowshrinking class kotlin.coroutines.Continuation

# Gson
-keepattributes Signature
-keepattributes *Annotation*
-keep class com.google.gson.** { *; }
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer

# Room
-keep class * extends androidx.room.RoomDatabase
-keep @androidx.room.Entity class *
-dontwarn androidx.room.paging.**

# 保留 Parcelable
-keepclassmembers class * implements android.os.Parcelable {
    public static final ** CREATOR;
}

# 保留 Serializable
-keepclassmembers class * implements java.io.Serializable {
    static final long serialVersionUID;
    private static final java.io.ObjectStreamField[] serialPersistentFields;
    private void writeObject(java.io.ObjectOutputStream);
    private void readObject(java.io.ObjectInputStream);
}

# 保留枚举
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}

# WebView JavaScript 接口
-keepclassmembers class * {
    @android.webkit.JavascriptInterface <methods>;
}

隐藏敏感信息

// ❌ 不要硬编码
const val API_KEY = "sk_live_xxxxxxxxxxxx"

// ✅ 使用 local.properties
// local.properties(不入库)
API_KEY=sk_live_xxxxxxxxxxxx

// build.gradle
android {
    defaultConfig {
        buildConfigField "String", "API_KEY", "\"${getProperty("API_KEY")}\""
    }
}

def getProperty(String name) {
    def properties = new Properties()
    def localProperties = rootProject.file("local.properties")
    if (localProperties.exists()) {
        localProperties.withInputStream { properties.load(it) }
    }
    return properties.getProperty(name) ?: ""
}

// 使用
val apiKey = BuildConfig.API_KEY

// ✅ 或使用 Secrets Gradle Plugin
plugins {
    id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin' version '2.0.1'
}

// secrets.properties
MAPS_API_KEY=your_key

// 自动生成 BuildConfig
// BuildConfig.MAPS_API_KEY

安全组件使用

// 防止截屏
window.setFlags(
    WindowManager.LayoutParams.FLAG_SECURE,
    WindowManager.LayoutParams.FLAG_SECURE
)

// 隐藏最近任务中的截图
// 同上 FLAG_SECURE

// Root 检测(基础)
fun isDeviceRooted(): Boolean {
    return checkRootMethod1() || checkRootMethod2() || checkRootMethod3()
}

private fun checkRootMethod1(): Boolean {
    val paths = arrayOf(
        "/system/app/Superuser.apk",
        "/sbin/su",
        "/system/bin/su",
        "/system/xbin/su"
    )
    return paths.any { File(it).exists() }
}

// 模拟器检测
fun isEmulator(): Boolean {
    return (Build.FINGERPRINT.startsWith("generic")
            || Build.FINGERPRINT.startsWith("unknown")
            || Build.MODEL.contains("google_sdk")
            || Build.MODEL.contains("Emulator")
            || Build.MODEL.contains("Android SDK built for x86")
            || Build.MANUFACTURER.contains("Genymotion")
            || Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")
            || "google_sdk" == Build.PRODUCT)
}

// 防 Hook (基础)
fun detectHook(): Boolean {
    try {
        throw Exception()
    } catch (e: Exception) {
        for (stackTraceElement in e.stackTrace) {
            if (stackTraceElement.className.contains("Xposed") ||
                stackTraceElement.className.contains("de.robv.android.xposed")) {
                return true
            }
        }
    }
    return false
}

34. 国际化 i18n

▼

多语言支持

// 目录结构
res/
├── values/                   # 默认(英文)
│   └── strings.xml
├── values-zh/                # 中文
│   └── strings.xml
├── values-zh-rCN/            # 简体中文
│   └── strings.xml
├── values-zh-rTW/            # 繁体中文
│   └── strings.xml
├── values-ja/                # 日文
│   └── strings.xml
├── values-es/                # 西班牙语
│   └── strings.xml
└── values-ar/                # 阿拉伯语(RTL)
    └── strings.xml

strings.xml 示例

<!-- values/strings.xml -->
<resources>
    <string name="app_name">MyApp</string>
    <string name="welcome">Welcome, %1$s!</string>
    <string name="item_count">%d items</string>
    <string name="html_text"><![CDATA[<b>Bold</b> and <i>italic</i>]]></string>

    <!-- 带数量 -->
    <plurals name="message_count">
        <item quantity="zero">No messages</item>
        <item quantity="one">One message</item>
        <item quantity="other">%d messages</item>
    </plurals>

    <!-- 字符串数组 -->
    <string-array name="countries">
        <item>China</item>
        <item>USA</item>
        <item>Japan</item>
    </string-array>
</resources>

<!-- values-zh/strings.xml -->
<resources>
    <string name="app_name">我的应用</string>
    <string name="welcome">欢迎,%1$s!</string>
    <string name="item_count">%d 项</string>

    <plurals name="message_count">
        <item quantity="other">%d 条消息</item>
    </plurals>
</resources>

代码中使用

// 字符串
val welcome = getString(R.string.welcome, "张三")  // "欢迎,张三!"
textView.text = welcome

// 带数量
val count = 5
val msg = resources.getQuantityString(R.plurals.message_count, count, count)
textView.text = msg  // "5 条消息"

// HTML
textView.text = HtmlCompat.fromHtml(
    getString(R.string.html_text),
    HtmlCompat.FROM_HTML_MODE_LEGACY
)

// 字符串数组
val countries = resources.getStringArray(R.array.countries)

日期时间本地化

// 日期格式化
val formatter = DateFormat.getDateInstance(DateFormat.LONG, Locale.getDefault())
val dateStr = formatter.format(Date())  // "2024年1月11日"

// 时间格式化
val timeFormatter = DateFormat.getTimeInstance(DateFormat.SHORT)
val timeStr = timeFormatter.format(Date())  // "下午3:45"

// 数字格式化
val numberFormat = NumberFormat.getInstance(Locale.getDefault())
val numStr = numberFormat.format(1234567.89)  // "1,234,567.89" 或 "1.234.567,89"

// 货币
val currencyFormat = NumberFormat.getCurrencyInstance()
val moneyStr = currencyFormat.format(99.99)  // "¥99.99" 或 "$99.99"

// Kotlin 现代写法
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
    .withLocale(Locale.getDefault())
val now = LocalDateTime.now().format(dateTimeFormatter)

RTL(从右到左)支持

<!-- Manifest -->
<application
    android:supportsRtl="true"
    ... />

<!-- 使用 start/end 替代 left/right -->
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:paddingStart="16dp"
    android:paddingEnd="16dp"
    android:layout_marginStart="8dp"
    android:layout_marginEnd="8dp"
    android:drawableStart="@drawable/ic_icon"
    android:drawablePadding="8dp"
    android:textAlignment="viewStart"/>

<!-- 布局方向 -->
<LinearLayout
    android:layoutDirection="locale"
    android:orientation="horizontal">
    <!-- 子元素自动跟随语言方向 -->
</LinearLayout>

应用内切换语言

// Android 13+ 使用系统 API
val appLocale = LocaleListCompat.forLanguageTags("zh-CN")
AppCompatDelegate.setApplicationLocales(appLocale)

// 保存到 preferences
val locales = AppCompatDelegate.getApplicationLocales()

// Android 13+ 配置
// res/xml/locales_config.xml
<?xml version="1.0" encoding="utf-8"?>
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
    <locale android:name="en"/>
    <locale android:name="zh-Hans"/>
    <locale android:name="zh-Hant"/>
    <locale android:name="ja"/>
</locale-config>

<!-- Manifest -->
<application
    android:localeConfig="@xml/locales_config"
    ... />

其他资源本地化

// 不同语言的图标
res/
├── drawable-en/   # 英文图标
├── drawable-zh/   # 中文图标
├── drawable-ar/   # 阿拉伯文图标

// 不同语言的布局
res/
├── layout/        # 默认
├── layout-land/   # 横屏
├── layout-ar/     # 阿拉伯语(RTL 布局调整)

35. 无障碍适配

▼

为什么需要无障碍

  • 服务视障、听障、认知障碍、运动障碍用户
  • 法律要求(美国 ADA、欧盟 EAA)
  • 提升所有用户体验
  • Google Play 推荐

核心原则

原则实践
可感知提供文字替代、高对比度、字幕
可操作支持键盘、TalkBack、手势
可理解清晰的标签、错误提示
稳健兼容辅助技术

TalkBack 适配

<!-- 为图片提供描述 -->
<ImageView
    android:src="@drawable/photo"
    android:contentDescription="一张海滩日落的照片"/>

<!-- 装饰性图片设为空 -->
<ImageView
    android:src="@drawable/divider"
    android:contentDescription="@null"
    android:importantForAccessibility="no"/>

<!-- 为图标按钮提供描述 -->
<ImageButton
    android:src="@drawable/ic_delete"
    android:contentDescription="删除此文章"/>

<!-- 为状态变化提供反馈 -->
<Button
    android:stateListAnimator="@animator/button_state"
    android:contentDescription="提交表单"/>

<!-- 为自定义 View 提供描述 -->
<com.example.RatingBar
    android:contentDescription="评分 4 星,共 5 星"/>

<!-- 标签关联 -->
<TextView
    android:id="@+id/label_email"
    android:text="邮箱"
    android:labelFor="@+id/et_email"/>

<EditText
    android:id="@+id/et_email"
    android:hint="请输入邮箱"/>

<!-- 分组 -->
<LinearLayout
    android:importantForAccessibility="yes"
    android:contentDescription="用户卡片:张三,25 岁,北京">
    <!-- 子元素被合并描述 -->
</LinearLayout>

自定义无障碍操作

ViewCompat.setAccessibilityDelegate(view, object : AccessibilityDelegateCompat() {
    override fun onInitializeAccessibilityNodeInfo(v: View, info: AccessibilityNodeInfoCompat) {
        super.onInitializeAccessibilityNodeInfo(v, info)

        // 添加自定义操作
        val customAction = AccessibilityNodeInfoCompat.AccessibilityActionCompat(
            R.id.action_custom,
            "收藏此文章"
        )
        info.addAction(customAction)

        // 设置角色描述
        info.roleDescription = "滑动卡片"
    }

    override fun performAccessibilityAction(v: View, action: Int, arguments: Bundle?): Boolean {
        if (action == R.id.action_custom) {
            favoriteArticle()
            return true
        }
        return super.performAccessibilityAction(v, action, arguments)
    }
})

// 发送无障碍事件
view.sendAccessibilityEvent(AccessibilityEvent.TYPE_ANNOUNCEMENT)
view.announceForAccessibility("已添加到购物车")

焦点顺序管理

<!-- 自定义焦点顺序 -->
<TextView
    android:id="@+id/tv1"
    android:nextFocusDown="@+id/et1"/>

<EditText
    android:id="@+id/et1"
    android:nextFocusDown="@+id/btn1"/>

<!-- 焦点组 -->
<LinearLayout
    android:focusable="true"
    android:focusableInTouchMode="true">
    <!-- 子元素作为一个整体获得焦点 -->
</LinearLayout>

<!-- 禁止获得焦点 -->
<TextView
    android:focusable="false"
    android:importantForAccessibility="noHideDescendants"/>

颜色和对比度

// 检查对比度
val contrastRatio = ContrastColorUtil.getInstance(context)
    .calculateContrast(textColor, backgroundColor)

// 建议:
// 正文文字:至少 4.5:1
// 大文字(18sp+ 或 14sp+ 加粗):至少 3:1
// UI 组件和图形:至少 3:1

// 避免仅用颜色传达信息
// ❌ 错误:红色表示必填字段
// ✅ 正确:红色 + 星号 * 表示必填

<TextView
    android:text="姓名 *"
    android:textColor="@color/text_primary"/>

// 高对比度主题支持
val uiModeManager = getSystemService(Context.UI_SERVICE) as UiModeManager
if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_WATCH) {
    // 手表高对比度模式
}

动画与减弱动效

// 检测用户是否偏好减弱动效
val reduceMotion = context.resources.configuration
    .isLayoutSizeAtLeast(Configuration.SCREENLAYOUT_SIZE_NORMAL)

// API 34+ 直接获取
val animationScale = Settings.Global.getFloat(
    context.contentResolver,
    Settings.Global.ANIMATOR_DURATION_SCALE,
    1f
)

if (animationScale == 0f) {
    // 用户关闭了动画,使用淡入淡出替代复杂动画
    view.animate().alpha(1f).setDuration(100).start()
} else {
    // 正常播放动画
}

// Jetpack Compose
@Composable
fun shouldReduceMotion(): Boolean {
    val context = LocalContext.current
    return remember {
        Settings.Global.getFloat(
            context.contentResolver,
            Settings.Global.ANIMATOR_DURATION_SCALE,
            1f
        ) == 0f
    }
}

无障碍测试工具

工具用途
TalkBack屏幕阅读器测试
Accessibility Scanner自动检测问题
Lint静态分析
Color Contrast Analyzer对比度检查
Espresso AccessibilityChecksUI 测试集成
// Espresso 无障碍检查
AccessibilityChecks.enable().apply {
    setRunChecksFromRootView(true)
    setSuppressingResultMatcher(
        Matchers.allOf(
            Matchers.hasMatchedView(withId(R.id.ignored_view)),
            Matchers.matchesAccessibilityCheckResult(...)
        )
    )
}

36. WebView 混合开发

▼

WebView 基础使用

<WebView
    android:id="@+id/webView"
    android:layout_width="match_parent"
    android:layout_height="match_parent"/>

class WebViewActivity : AppCompatActivity() {
    private lateinit var webView: WebView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_webview)

        webView = findViewById(R.id.webView)

        // 配置 WebView
        webView.settings.apply {
            javaScriptEnabled = true
            domStorageEnabled = true  // 启用 localStorage
            allowFileAccess = false   // 安全:禁用文件访问
            allowContentAccess = false
            setSupportZoom(true)
            builtInZoomControls = true
            displayZoomControls = false
            useWideViewPort = true    // 自适应屏幕
            loadWithOverviewMode = true
            mixedContentMode = WebSettings.MIXED_CONTENT_NEVER_ALLOW  // HTTPS only
            cacheMode = WebSettings.LOAD_DEFAULT
            mediaPlaybackRequiresUserGesture = true
        }

        // 设置 WebViewClient(处理页面内跳转)
        webView.webViewClient = object : WebViewClient() {
            override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
                val url = request.url.toString()
                // 处理自定义 scheme
                if (url.startsWith("myapp://")) {
                    handleCustomScheme(url)
                    return true
                }
                return false  // 在 WebView 内加载
            }

            override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) {
                super.onPageStarted(view, url, favicon)
                showLoading()
            }

            override fun onPageFinished(view: WebView?, url: String?) {
                super.onPageFinished(view, url)
                hideLoading()
            }

            override fun onReceivedError(view: WebView?, request: WebResourceRequest?, error: WebResourceError?) {
                super.onReceivedError(view, request, error)
                if (request?.isForMainFrame == true) {
                    showErrorPage()
                }
            }

            override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
                // 生产环境不要这样处理!
                // handler?.proceed()  // 不安全
                handler?.cancel()
            }
        }

        // 设置 WebChromeClient(处理 JS 对话框、进度条)
        webView.webChromeClient = object : WebChromeClient() {
            override fun onProgressChanged(view: WebView?, newProgress: Int) {
                binding.progressBar.progress = newProgress
                if (newProgress == 100) binding.progressBar.visibility = View.GONE
            }

            override fun onReceivedTitle(view: WebView?, title: String?) {
                supportActionBar?.title = title
            }

            override fun onJsAlert(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean {
                MaterialAlertDialogBuilder(this@WebViewActivity)
                    .setTitle("提示")
                    .setMessage(message)
                    .setPositiveButton("确定") { _, _ -> result?.confirm() }
                    .show()
                return true
            }

            // 文件上传
            override fun onShowFileChooser(webView: WebView?, filePathCallback: ValueCallback<Array<Uri>>?, fileChooserParams: FileChooserParams?): Boolean {
                val intent = fileChooserParams?.createIntent()
                try {
                    fileUploadLauncher.launch(intent)
                    uploadCallback = filePathCallback
                } catch (e: Exception) {
                    return false
                }
                return true
            }
        }

        // 注入 JS 接口
        webView.addJavascriptInterface(JsBridge(this), "AndroidBridge")

        // 加载页面
        val url = intent.getStringExtra("url") ?: "https://example.com"
        webView.loadUrl(url)
    }

    override fun onBackPressed() {
        if (webView.canGoBack()) {
            webView.goBack()
        } else {
            super.onBackPressed()
        }
    }

    override fun onDestroy() {
        webView.apply {
            stopLoading()
            webChromeClient = null
            webViewClient = null
            clearHistory()
            clearCache(true)
            loadUrl("about:blank")
            onPause()
            removeAllViews()
            destroyDrawingCache()
            destroy()
        }
        super.onDestroy()
    }
}

JavaScript 与 Native 交互

// Native 提供给 JS 的接口
class JsBridge(private val context: Context) {

    @JavascriptInterface
    fun showToast(message: String) {
        Handler(Looper.getMainLooper()).post {
            Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
        }
    }

    @JavascriptInterface
    fun getUserInfo(): String {
        val user = User("张三", 25)
        return Gson().toJson(user)
    }

    @JavascriptInterface
    fun openActivity(activityName: String) {
        try {
            val intent = Intent(context, Class.forName(activityName))
            context.startActivity(intent)
        } catch (e: Exception) {
            Log.e("JsBridge", "打开 Activity 失败", e)
        }
    }

    @JavascriptInterface
    fun logEvent(eventName: String, params: String) {
        Analytics.logEvent(eventName, parseParams(params))
    }

    @JavascriptInterface
    fun share(title: String, content: String, url: String) {
        Handler(Looper.getMainLooper()).post {
            val intent = Intent(Intent.ACTION_SEND).apply {
                type = "text/plain"
                putExtra(Intent.EXTRA_SUBJECT, title)
                putExtra(Intent.EXTRA_TEXT, "$content\n$url")
            }
            context.startActivity(Intent.createChooser(intent, "分享到"))
        }
    }
}

// JS 中调用
<script>
    // 调用 Native
    AndroidBridge.showToast('Hello from JS!');
    const userInfo = JSON.parse(AndroidBridge.getUserInfo());
    AndroidBridge.logEvent('page_view', '{"page":"home"}');

    // Native 调用 JS
    function onReceiveFromNative(data) {
        console.log('Received:', data);
    }
</script>

// Native 调用 JS
webView.evaluateJavascript("onReceiveFromNative('${json}')") { result ->
    Log.d("WebView", "JS 返回: $result")
}

// 异步获取 JS 返回值
webView.evaluateJavascript("getUserAge()") { result ->
    val age = result.toIntOrNull() ?: 0
}

安全注意事项

// ✅ 安全设置
settings.apply {
    javaScriptEnabled = true  // 仅在必要时开启
    allowFileAccess = false
    allowContentAccess = false
    allowFileAccessFromFileURLs = false
    allowUniversalAccessFromFileURLs = false
}

// ❌ 避免:addJavascriptInterface 在 API 17 以下有漏洞
// ✅ 解决:使用 @JavascriptInterface 注解(API 17+ 自动防护)

// ✅ 验证 URL 来源
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
    val url = request.url.toString()
    if (!url.startsWith("https://trusted-domain.com")) {
        // 拒绝或提示用户
        return true
    }
    return false
}

// ✅ 处理 SSL 错误
override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
    handler?.cancel()  // 取消加载
    // 不要 proceed(),除非你有充分理由
}

// ✅ 限制 JavaScript 访问的接口
// 只暴露必要的方法,使用 @JavascriptInterface 注解

37. 传感器与硬件

▼

常用传感器

传感器用途常量
加速度计运动检测、摇一摇TYPE_ACCELEROMETER
陀螺仪旋转检测、ARTYPE_GYROSCOPE
磁力计指南针TYPE_MAGNETIC_FIELD
光线自动亮度TYPE_LIGHT
接近通话时灭屏TYPE_PROXIMITY
气压计海拔高度TYPE_PRESSURE
心率健康应用TYPE_HEART_RATE
步数计运动健康TYPE_STEP_COUNTER

加速度传感器(摇一摇检测)

class ShakeDetector(context: Context) : SensorEventListener {

    private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
    private val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
    private var shakeListener: (() -> Unit)? = null

    private var lastShakeTime = 0L
    private var lastX = 0f
    private var lastY = 0f
    private var lastZ = 0f
    private var lastUpdate = 0L

    fun start(listener: () -> Unit) {
        shakeListener = listener
        sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_UI)
    }

    fun stop() {
        sensorManager.unregisterListener(this)
    }

    override fun onSensorChanged(event: SensorEvent) {
        val currentTime = System.currentTimeMillis()
        if (currentTime - lastUpdate < 100) return

        val timeDiff = currentTime - lastUpdate
        lastUpdate = currentTime

        val x = event.values[0]
        val y = event.values[1]
        val z = event.values[2]

        val speed = Math.sqrt(((x - lastX) * (x - lastX) + (y - lastY) * (y - lastY) + (z - lastZ) * (z - lastZ)).toDouble()) / timeDiff * 10000

        if (speed > SHAKE_THRESHOLD) {
            if (currentTime - lastShakeTime > 1000) {
                lastShakeTime = currentTime
                shakeListener?.invoke()
            }
        }

        lastX = x
        lastY = y
        lastZ = z
    }

    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}

    companion object {
        private const val SHAKE_THRESHOLD = 800
    }
}

// 使用
val shakeDetector = ShakeDetector(this)
shakeDetector.start {
    Toast.makeText(this, "检测到摇一摇!", Toast.LENGTH_SHORT).show()
}

光线传感器

class LightSensorHelper(private val context: Context) : SensorEventListener {

    private val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
    private val lightSensor = sensorManager.getDefaultSensor(Sensor.TYPE_LIGHT)
    private var listener: ((Float) -> Unit)? = null

    fun start(onLightChange: (Float) -> Unit) {
        listener = onLightChange
        sensorManager.registerListener(this, lightSensor, SensorManager.SENSOR_DELAY_NORMAL)
    }

    fun stop() {
        sensorManager.unregisterListener(this)
    }

    override fun onSensorChanged(event: SensorEvent) {
        val lux = event.values[0]
        listener?.invoke(lux)
        // lux: 0 = 黑暗, 50 = 室内, 10000 = 日光
    }

    override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
}

指纹识别 (Biometric)

implementation 'androidx.biometric:biometric:1.2.0-alpha05'

class BiometricHelper(private val activity: FragmentActivity) {

    private val executor = ContextCompat.getMainExecutor(activity)
    private val biometricPrompt = BiometricPrompt(activity, executor,
        object : BiometricPrompt.AuthenticationCallback() {
            override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                super.onAuthenticationError(errorCode, errString)
                Log.e("Biometric", "错误: $errString")
                authCallback?.onError(errString.toString())
            }

            override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                super.onAuthenticationSucceeded(result)
                authCallback?.onSuccess(result.cryptoObject)
            }

            override fun onAuthenticationFailed() {
                super.onAuthenticationFailed()
                authCallback?.onFailed()
            }
        })

    private var authCallback: AuthCallback? = null

    fun authenticate(
        title: String = "身份验证",
        subtitle: String = "请使用指纹验证身份",
        negativeButtonText: String = "取消",
        callback: AuthCallback
    ) {
        authCallback = callback

        // 检查设备是否支持
        val biometricManager = BiometricManager.from(activity)
        when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) {
            BiometricManager.BIOMETRIC_SUCCESS -> { /* 支持 */ }
            BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
                callback.onError("设备不支持生物识别")
                return
            }
            BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
                // 引导用户设置
                val intent = Intent(Settings.ACTION_BIOMETRIC_ENROLL)
                activity.startActivity(intent)
                return
            }
        }

        val promptInfo = BiometricPrompt.PromptInfo.Builder()
            .setTitle(title)
            .setSubtitle(subtitle)
            .setNegativeButtonText(negativeButtonText)
            .setAllowedAuthenticators(
                BiometricManager.Authenticators.BIOMETRIC_STRONG
                        or BiometricManager.Authenticators.DEVICE_CREDENTIAL
            )
            .build()

        biometricPrompt.authenticate(promptInfo)
    }

    // 带加密的生物识别
    fun authenticateWithCrypto(callback: AuthCallback) {
        val keyStore = KeyStore.getInstance("AndroidKeyStore")
        keyStore.load(null)

        val keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore")
        val keyGenParameterSpec = KeyGenParameterSpec.Builder(
            "my_key",
            KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
        )
            .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
            .setUserAuthenticationRequired(true)
            .build()

        keyGenerator.init(keyGenParameterSpec)
        keyGenerator.generateKey()

        val cipher = Cipher.getInstance(KeyProperties.KEY_ALGORITHM_AES + "/GCM/NoPadding")
        val key = keyStore.getKey("my_key", null) as SecretKey
        cipher.init(Cipher.ENCRYPT_MODE, key)

        val cryptoObject = BiometricPrompt.CryptoObject(cipher)
        biometricPrompt.authenticate(promptInfo, cryptoObject)
    }

    interface AuthCallback {
        fun onSuccess(cryptoObject: BiometricPrompt.CryptoObject?)
        fun onError(message: String)
        fun onFailed()
    }
}

// 使用
val biometricHelper = BiometricHelper(this)
biometricHelper.authenticate(
    title = "身份验证",
    subtitle = "请使用指纹登录"
    callback = object : BiometricHelper.AuthCallback {
        override fun onSuccess(cryptoObject: BiometricPrompt.CryptoObject?) {
            navigateToHome()
        }
        override fun onError(message: String) {
            Toast.makeText(this@LoginActivity, message, Toast.LENGTH_SHORT).show()
        }
        override fun onFailed() {
            Toast.makeText(this@LoginActivity, "验证失败", Toast.LENGTH_SHORT).show()
        }
    }
)

蓝牙 (BLE)

// 权限
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>

class BleHelper(private val context: Context) {
    private val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
    private val bluetoothAdapter = bluetoothManager.adapter
    private var scanCallback: ((BluetoothDevice) -> Unit)? = null

    private val leScanCallback = object : ScanCallback() {
        override fun onScanResult(callbackType: Int, result: ScanResult) {
            super.onScanResult(callbackType, result)
            scanCallback?.invoke(result.device)
        }
    }

    fun startScan(onDeviceFound: (BluetoothDevice) -> Unit) {
        scanCallback = onDeviceFound
        val scanner = bluetoothAdapter.bluetoothLeScanner
        scanner.startScan(leScanCallback)

        // 10 秒后停止扫描
        Handler(Looper.getMainLooper()).postDelayed({
            scanner.stopScan(leScanCallback)
        }, 10000)
    }

    fun stopScan() {
        bluetoothAdapter.bluetoothLeScanner.stopScan(leScanCallback)
    }

    fun connect(device: BluetoothDevice, callback: GattCallback) {
        val gatt = device.connectGatt(context, false, object : BluetoothGattCallback() {
            override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
                if (newState == BluetoothProfile.STATE_CONNECTED) {
                    gatt.discoverServices()
                }
            }

            override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
                if (status == BluetoothGatt.GATT_SUCCESS) {
                    callback.onConnected(gatt)
                }
            }

            override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
                callback.onDataReceived(characteristic.value)
            }
        })
    }

    interface GattCallback {
        fun onConnected(gatt: BluetoothGatt)
        fun onDataReceived(data: ByteArray)
    }
}

38. Kotlin 多平台

▼

Kotlin Multiplatform Mobile (KMM)

KMM 允许你在 Android 和 iOS 之间共享业务逻辑代码,UI 各自原生实现。

共享什么

适合共享不适合共享
业务逻辑UI 层
数据层(Repository)平台特定 UI
网络请求平台特定功能
数据库操作动画
状态管理导航

项目结构

MyApp/
├── shared/                    # 共享模块
│   └── src/
│       ├── commonMain/        # 跨平台代码
│       │   └── kotlin/
│       │       ├── data/
│       │       ├── domain/
│       │       └── presentation/
│       ├── androidMain/       # Android 特定代码
│       └── iosMain/           # iOS 特定代码
├── androidApp/                # Android 应用
└── iosApp/                    # iOS 应用 (Swift/Xcode)

Expect/Actual 模式

// commonMain - 声明期望
expect class Platform() {
    val name: String
}

expect fun getUuid(): String

expect class DatabaseDriverFactory {
    fun createDriver(): SqlDriver
}

// androidMain - Android 实现
actual class Platform actual constructor() {
    actual val name: String = "Android ${Build.VERSION.RELEASE}"
}

actual fun getUuid(): String = UUID.randomUUID().toString()

actual class DatabaseDriverFactory(private val context: Context) {
    actual fun createDriver(): SqlDriver {
        return AndroidSqliteDriver(AppDatabase.Schema, context, "app.db")
    }
}

// iosMain - iOS 实现
actual class Platform actual constructor() {
    actual val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
}

actual fun getUuid(): String = NSUUID().UUIDString()

actual class DatabaseDriverFactory {
    actual fun createDriver(): SqlDriver {
        return NativeSqliteDriver(AppDatabase.Schema, "app.db")
    }
}

共享 ViewModel

// commonMain
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.MutableStateFlow

class SharedViewModel(private val repo: UserRepository) {
    private val _users = MutableStateFlow<List<User>>(emptyList())
    val users: StateFlow<List<User>> = _users

    private val _loading = MutableStateFlow(false)
    val loading: StateFlow<Boolean> = _loading

    private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)

    fun loadUsers() {
        scope.launch {
            _loading.value = true
            try {
                val users = repo.getUsers()
                _users.value = users
            } finally {
                _loading.value = false
            }
        }
    }

    fun clear() {
        scope.cancel()
    }
}

// Android 使用
class UserListFragment : Fragment() {
    private val sharedViewModel = SharedViewModel(UserRepositoryImpl())

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewLifecycleOwner.lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                sharedViewModel.users.collect { users ->
                    adapter.submitList(users)
                }
            }
        }
        sharedViewModel.loadUsers()
    }
}

// iOS 使用 (Swift)
class UserListViewModel: ObservableObject {
    @Published var users: [User] = []
    @Published var loading = false

    private let shared = SharedViewModel(repo: UserRepositoryImpl())

    func loadUsers() {
        shared.loadUsers()
        // 观察 shared.users
    }
}

Ktor 客户端(跨平台网络)

// commonMain
implementation 'io.ktor:ktor-client-core:2.3.7'
implementation 'io.ktor:ktor-client-content-negotiation:2.3.7'
implementation 'io.ktor:ktor-serialization-kotlinx-json:2.3.7'

// androidMain
implementation 'io.ktor:ktor-client-okhttp:2.3.7'

// iosMain
implementation 'io.ktor:ktor-client-darwin:2.3.7'

// 共享网络层
class HttpClientFactory {
    fun create(): HttpClient {
        return HttpClient {
            install(ContentNegotiation) {
                json(Json {
                    ignoreUnknownKeys = true
                })
            }
            install(Logging) {
                level = LogLevel.BODY
            }
        }
    }
}

class ApiClient(private val client: HttpClient) {
    suspend fun getUsers(): List<User> {
        return client.get("https://api.example.com/users").body()
    }

    suspend fun createUser(user: User): User {
        return client.post("https://api.example.com/users") {
            contentType(ContentType.Application.Json)
            setBody(user)
        }.body()
    }
}

SQLDelight(跨平台数据库)

// build.gradle
plugins {
    id 'app.cash.sqldelight' version '2.0.1'
}

sqldelight {
    databases {
        create("AppDatabase") {
            packageName.set("com.example.db")
        }
    }
}

// src/commonMain/sqldelight/com/example/db/User.sq
CREATE TABLE user (
    id INTEGER NOT NULL PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT NOT NULL
);

selectAll:
SELECT * FROM user;

insert:
INSERT INTO user(name, email) VALUES (?, ?);

selectById:
SELECT * FROM user WHERE id = ?;

// Kotlin 生成代码
class UserRepository(driver: SqlDriver) {
    private val database = AppDatabase(driver)

    fun getAll(): List<User> = database.userQueries.selectAll().executeAsList()
    fun insert(name: String, email: String) = database.userQueries.insert(name, email)

    // Flow 响应式查询
    fun observeAll(): Flow<List<User>> = database.userQueries.selectAll()
        .asFlow()
        .mapToList(Dispatchers.IO)
}

💡

适用场景:

当你的 Android 和 iOS 应用有相似的业务逻辑,但希望 UI 保持原生体验时,KMM 是最佳选择。

39. CI/CD 流水线

▼

常用 CI/CD 平台

平台特点适用场景
GitHub ActionsGitHub 集成、免费额度大GitHub 托管项目
GitLab CI私有部署、强大GitLab 托管项目
Jenkins自托管、插件丰富企业内部
Bitrise移动专用、可视化移动团队
Firebase App Distribution免费、易分享内测分发

GitHub Actions 完整配置

# .github/workflows/android.yml
name: Android CI

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

env:
  JAVA_VERSION: '17'
  GRADLE_OPTS: "-Dorg.gradle.daemon=false"

jobs:
  # 单元测试
  test:
    name: Unit Tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up JDK
        uses: actions/setup-java@v4
        with:
          java-version: ${{ env.JAVA_VERSION }}
          distribution: 'temurin'
          cache: 'gradle'

      - name: Setup Android SDK
        uses: android-actions/setup-android@v3

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Run unit tests
        run: ./gradlew testDebugUnitTest

      - name: Generate test report
        uses: dorny/test-reporter@v1
        if: success() || failure()
        with:
          name: Unit Tests
          path: '**/build/test-results/test*/TEST-*.xml'
          reporter: java-junit

      - name: Upload coverage report
        uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: app/build/reports/jacoco/

  # Lint 检查
  lint:
    name: Lint Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: ${{ env.JAVA_VERSION }}
          distribution: 'temurin'
          cache: 'gradle'
      - run: chmod +x gradlew
      - run: ./gradlew lintDebug
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: lint-report
          path: app/build/reports/lint-results-*.html

  # 构建 APK
  build:
    name: Build APK
    runs-on: ubuntu-latest
    needs: [test, lint]
    if: github.event_name == 'push'
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: ${{ env.JAVA_VERSION }}
          distribution: 'temurin'
          cache: 'gradle'

      - name: Decode keystore
        run: |
          echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > release-key.jks

      - name: Build Release APK
        env:
          KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
          KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
        run: ./gradlew assembleRelease

      - name: Upload APK
        uses: actions/upload-artifact@v4
        with:
          name: app-release-apk
          path: app/build/outputs/apk/release/*.apk

      - name: Upload mapping
        uses: actions/upload-artifact@v4
        with:
          name: mapping
          path: app/build/outputs/mapping/release/mapping.txt

  # 发布到 Firebase App Distribution
  distribute:
    name: Distribute to Firebase
    runs-on: ubuntu-latest
    needs: build
    if: github.ref == 'refs/heads/develop'
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with:
          name: app-release-apk

      - name: Upload to Firebase App Distribution
        uses: wzieba/firebase-distribution-github-action@v1
        with:
          appId: ${{ secrets.FIREBASE_APP_ID }}
          serviceCredentialsFileContent: ${{ secrets.FIREBASE_CREDENTIAL }}
          groups: testers
          file: app-release.apk
          releaseNotes: |
            Commit: ${{ github.event.head_commit.message }}
            Branch: ${{ github.ref_name }}
            Author: ${{ github.actor }}

  # 发布到 Google Play
  deploy:
    name: Deploy to Google Play
    runs-on: ubuntu-latest
    needs: build
    if: startsWith(github.ref, 'refs/tags/v')
    steps:
      - uses: actions/checkout@v4
      - uses: actions/download-artifact@v4
        with:
          name: app-release-apk

      - name: Upload to Play Store
        uses: r0adkll/upload-google-play@v1
        with:
          serviceAccountJsonPlainText: ${{ secrets.PLAY_STORE_SERVICE_ACCOUNT }}
          packageName: com.example.myapp
          releaseFiles: app-release.apk
          track: production
          status: completed
          whatsNewDirectory: distribution/whatsnew

  # 构建 AAB
  build-aab:
    name: Build AAB
    runs-on: ubuntu-latest
    needs: [test, lint]
    if: startsWith(github.ref, 'refs/tags/v')
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: ${{ env.JAVA_VERSION }}
          distribution: 'temurin'
          cache: 'gradle'

      - name: Build AAB
        run: ./gradlew bundleRelease

      - name: Upload AAB
        uses: actions/upload-artifact@v4
        with:
          name: app-release-aab
          path: app/build/outputs/bundle/release/*.aab

Fastlane 自动化

# 安装
sudo gem install fastlane

# 初始化
fastlane init

# Fastfile
default_platform(:android)

platform :android do
  desc "Run tests"
  lane :test do
    gradle(task: "testDebugUnitTest")
  end

  desc "Build release APK"
  lane :build do
    gradle(
      task: "assemble",
      build_type: "Release",
      properties: {
        "android.injected.signing.store.file" => "release-key.jks",
        "android.injected.signing.store.password" => ENV["KEYSTORE_PASSWORD"],
        "android.injected.signing.key.alias" => ENV["KEY_ALIAS"],
        "android.injected.signing.key.password" => ENV["KEY_PASSWORD"]
      }
    )
  end

  desc "Submit to Play Store"
  lane :deploy do
    upload_to_play_store(
      track: "production",
      json_key: "play-store-key.json",
      apk: "app/build/outputs/apk/release/app-release.apk"
    )
  end

  desc "Distribute to Firebase"
  lane :beta do
    firebase_app_distribution(
      app: "1:123456789:android:abcdef",
      service_credentials_file: "firebase-key.json",
      groups: "testers",
      release_notes: "New features and bug fixes"
    )
  end

  desc "Full pipeline"
  lane :release do
    test
    build
    deploy
  end
end

# 运行
fastlane android test
fastlane android build
fastlane android deploy

Danger 代码审查

# Gemfile
gem 'danger'
gem 'danger-android_lint'
gem 'danger-jacoco'

# Dangerfile
danger.import_dangerfile(path: 'danger/commit')

# 检查 PR 大小
warn("PR 太大,请拆分") if git.lines_of_code > 500

# 检查未解决的 TODO
github.dismiss_out_of_range_messages
warn("有未解决的 TODO") if git.modified_files.any? { |f| File.read(f).include?("TODO") }

# Android Lint
android_lint.report_file = 'app/build/reports/lint-results-debug.xml'
android_lint.lint

# Jacoco 覆盖率
jacoco.report_path = 'app/build/reports/jacoco/jacocoTestReport/jacocoTestReport.xml'
jacoco.warn_if_coverage_less_than 80

40. 崩溃监控与分析

▼

Firebase Crashlytics

// 添加依赖
implementation platform('com.google.firebase:firebase-bom:32.7.1')
implementation 'com.google.firebase:firebase-crashlytics'
implementation 'com.google.firebase:firebase-analytics'

// 初始化(自动)
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()

        // 启用 Crashlytics(发布版)
        FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(!BuildConfig.DEBUG)
    }
}

// 记录非致命异常
try {
    // 可能出错的代码
} catch (e: Exception) {
    FirebaseCrashlytics.getInstance().recordException(e)

    // 添加自定义日志
    FirebaseCrashlytics.getInstance().log("User clicked button X")

    // 添加自定义键值
    FirebaseCrashlytics.getInstance().setCustomKey("user_id", userId)
    FirebaseCrashlytics.getInstance().setCustomKey("screen", "home")
    FirebaseCrashlytics.getInstance().setUserId(userId)
}

// 强制崩溃(测试)
FirebaseCrashlytics.getInstance().crash()

// 检查上次崩溃
val crashlytics = FirebaseCrashlytics.getInstance()
if (crashlytics.didCrashOnPreviousExecution()) {
    // 处理上次崩溃后的逻辑
}

// 启用 Analytics 集成
FirebaseAnalytics.getInstance(this).setUserId(userId)
FirebaseAnalytics.getInstance(this).logEvent("purchase", bundle)

自定义异常处理器

class CrashHandler private constructor(
    private val context: Context,
    private val defaultHandler: Thread.UncaughtExceptionHandler?
) : Thread.UncaughtExceptionHandler {

    override fun uncaughtException(thread: Thread, throwable: Throwable) {
        try {
            // 收集设备信息
            val deviceInfo = collectDeviceInfo()
            val stackTrace = Log.getStackTraceString(throwable)

            // 保存到本地
            saveCrashLog(stackTrace, deviceInfo)

            // 上传到服务器
            uploadCrashLog(stackTrace, deviceInfo)

            // 发送通知
            sendCrashNotification()

        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            // 交给系统默认处理(杀死进程)
            defaultHandler?.uncaughtException(thread, throwable)
            // 或退出应用
            android.os.Process.killProcess(android.os.Process.myPid())
            exitProcess(1)
        }
    }

    private fun collectDeviceInfo(): String {
        return """
            App Version: ${BuildConfig.VERSION_NAME}
            Android Version: ${Build.VERSION.RELEASE}
            SDK: ${Build.VERSION.SDK_INT}
            Device: ${Build.MANUFACTURER} ${Build.MODEL}
            Memory: ${getAvailableMemory()}MB
            Disk: ${getAvailableDisk()}MB
        """.trimIndent()
    }

    private fun saveCrashLog(stackTrace: String, deviceInfo: String) {
        val file = File(context.filesDir, "crash_${System.currentTimeMillis()}.log")
        file.writeText("$deviceInfo\n\n$stackTrace")
    }

    companion object {
        @Volatile
        private var instance: CrashHandler? = null

        fun init(context: Context) {
            instance ?: synchronized(this) {
                instance ?: CrashHandler(
                    context.applicationContext,
                    Thread.getDefaultUncaughtExceptionHandler()
                ).also {
                    Thread.setDefaultUncaughtExceptionHandler(it)
                }
            }
        }
    }
}

// Application 中初始化
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        CrashHandler.init(this)
    }
}

ANR 监控

// StrictMode 检测主线程违规
StrictMode.setThreadPolicy(
    StrictMode.ThreadPolicy.Builder()
        .detectAll()
        .penaltyLog()
        .penaltyDeath()  // 或 penaltyDialog()
        .build()
)

// 检测 ANR
class ANRDetector {
    private val handler = Handler(Looper.getMainLooper())
    private var isResponded = false

    fun startDetection(timeoutMs: Long = 5000) {
        isResponded = false
        handler.postDelayed({
            if (!isResponded) {
                // 发生 ANR
                val stackTrace = Thread.getAllStackTraces()
                logANR(stackTrace)
            }
        }, timeoutMs)
    }

    fun onResponse() {
        isResponded = true
    }
}

// 使用 Application.ActivityLifecycleCallbacks 监控
registerActivityLifecycleCallbacks(object : ActivityLifecycleCallbacks {
    override fun onActivityResumed(activity: Activity) {
        anrDetector.startDetection()
    }
    override fun onActivityPaused(activity: Activity) {
        anrDetector.onResponse()
    }
    // ... 其他方法
})

性能监控

// Firebase Performance
implementation 'com.google.firebase:firebase-perf'

// 自动监控
// 应用启动时间、屏幕渲染、网络请求等

// 自定义追踪
val trace = FirebasePerformance.startTrace("custom_trace")
// 执行操作
trace.putMetric("items_processed", items.size.toLong())
trace.stop()

// HTTP 网络监控(自动)
// OkHttp 集成
val client = OkHttpClient.Builder()
    .addNetworkInterceptor(FirebasePerformance.getOkHttpInterceptor())
    .build()

// App Startup 时间
class AppStartupLogger : Application.ActivityLifecycleCallbacks {
    private var startupStartTime = 0L
    private var firstActivityResumed = false

    override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
        if (startupStartTime == 0L) {
            startupStartTime = System.currentTimeMillis()
        }
    }

    override fun onActivityResumed(activity: Activity) {
        if (!firstActivityResumed) {
            firstActivityResumed = true
            val startupTime = System.currentTimeMillis() - startupStartTime
            FirebaseAnalytics.getInstance(activity)
                .logEvent("app_startup_time", bundleOf("time_ms" to startupTime))
        }
    }
}

日志收集与分析

// 自定义日志系统
class AppLogger(private val context: Context) {

    private val logFile = File(context.cacheDir, "app.log")
    private val maxFileSize = 5 * 1024 * 1024L  // 5MB

    fun log(level: LogLevel, tag: String, message: String, throwable: Throwable? = null) {
        val timestamp = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
            .format(Date())

        val logEntry = buildString {
            append("[$timestamp] ")
            append("[${level.name}] ")
            append("[$tag] ")
            append(message)
            throwable?.let { append("\n${Log.getStackTraceString(it)}") }
            append("\n")
        }

        logFile.appendText(logEntry)

        // 检查文件大小
        if (logFile.length() > maxFileSize) {
            rotateLogFile()
        }

        // 同时输出到 Logcat
        when (level) {
            LogLevel.DEBUG -> Log.d(tag, message)
            LogLevel.INFO -> Log.i(tag, message)
            LogLevel.WARN -> Log.w(tag, message, throwable)
            LogLevel.ERROR -> Log.e(tag, message, throwable)
        }
    }

    private fun rotateLogFile() {
        val oldFile = File(context.cacheDir, "app.log.old")
        if (oldFile.exists()) oldFile.delete()
        logFile.renameTo(oldFile)
        logFile.createNewFile()
    }

    fun uploadLogs() {
        // 压缩并上传到服务器
        val zipFile = File(context.cacheDir, "logs.zip")
        ZipOutputStream(zipFile.outputStream()).use { zos ->
            zos.putNextEntry(ZipEntry("app.log"))
            logFile.inputStream().copyTo(zos)
            zos.closeEntry()
        }
        // 上传 zipFile
    }
}

enum class LogLevel { DEBUG, INFO, WARN, ERROR }

用户反馈收集

// Shake to Feedback(摇一摇反馈)
class FeedbackHelper(private val activity: Activity) {
    private val shakeDetector = ShakeDetector(activity)

    fun enableShakeFeedback() {
        shakeDetector.start {
            showFeedbackDialog()
        }
    }

    private fun showFeedbackDialog() {
        val dialogView = LayoutInflater.from(activity)
            .inflate(R.layout.dialog_feedback, null)

        MaterialAlertDialogBuilder(activity)
            .setTitle("反馈问题")
            .setView(dialogView)
            .setPositiveButton("提交") { _, _ ->
                val feedback = dialogView.findViewById<EditText>(R.id.etFeedback).text.toString()
                val screenshot = takeScreenshot()
                submitFeedback(feedback, screenshot)
            }
            .setNegativeButton("取消", null)
            .show()
    }

    private fun takeScreenshot(): Bitmap? {
        val view = activity.window.decorView.rootView
        view.isDrawingCacheEnabled = true
        val bitmap = Bitmap.createBitmap(view.drawingCache)
        view.isDrawingCacheEnabled = false
        return bitmap
    }

    private fun submitFeedback(feedback: String, screenshot: Bitmap?) {
        // 上传到服务器
        val crashLog = getLastCrashLog()
        val deviceInfo = collectDeviceInfo()

        viewModelScope.launch {
            api.submitFeedback(FeedbackRequest(
                content = feedback,
                screenshot = bitmapToBase64(screenshot),
                crashLog = crashLog,
                deviceInfo = deviceInfo,
                appVersion = BuildConfig.VERSION_NAME
            ))
        }
    }
}

✅

最佳实践:

建立完整的监控体系:崩溃监控 + 性能监控 + 业务指标监控 + 用户反馈。

😔 没有找到相关内容,请尝试其他关键词

← 返回IT 技术 yicool 百科 · 📱 Android 开发教程

评论 0