1. Android 开发概述
▼
什么是 Android
Android 是由 Google 主导开发的移动操作系统,基于 Linux 内核构建。它是全球市场份额最大的移动平台,覆盖手机、平板、手表、电视、汽车等多种设备形态。
Android 版本发展
| 版本 | API Level | 代号 | 重要特性 |
|---|---|---|---|
| 16 | 36 | Baklava | AI 集成、隐私沙盒增强 |
| 15 | 35 | VanillaIceCream | 预测性返回手势、卫星通信扩展 |
| 14 | 34 | Upside Down Cake | 卫星通信、照片选择器增强 |
| 13 | 33 | Tiramisu | 通知权限、主题图标 |
| 12 | 31 | Snow Cone | Material You 动态主题 |
| 11 | 30 | Red Velvet Cake | 一次性权限、对话气泡 |
| 10 | 29 | Queen Cake | 深色模式、手势导航 |
开发语言选择
- Kotlin(推荐):Google 官方首推语言,简洁、安全、现代化,空指针安全、扩展函数、协程支持
- Java:传统 Android 开发语言,生态成熟,资料丰富
- Flutter / Dart:跨平台方案,一套代码同时构建 iOS 和 Android
- React Native:使用 JavaScript/TypeScript,可复用 Web 技能
- Kotlin Multiplatform:共享业务逻辑代码,UI 各平台原生实现
💡
建议:
新项目优先使用 Kotlin,Google 官方文档和 Jetpack 库均以 Kotlin 为主。
Android 架构层次
- 应用层 (Applications):系统应用与第三方应用
- 应用框架层 (Application Framework):Activity Manager、Window Manager、Content Provider、View System
- 原生库层 (Native Libraries):SQLite、OpenGL、Webkit、libc
- Android Runtime:ART 虚拟机(替代旧版 Dalvik),采用 AOT 编译
- HAL (硬件抽象层):为硬件提供标准接口
- Linux 内核层:驱动、内存管理、进程管理、网络协议栈
- Jetpack:Google 官方组件库,遵循最佳实践
- Firebase:后端服务(分析、崩溃、推送、认证)
- Google Play Services:地图、支付、登录等核心服务
- AndroidX:替代旧版 Support Library,持续更新
- Kotlin Coroutines:现代异步编程模型
- 访问官网
developer.android.com/studio下载最新版 - 运行安装程序,选择标准安装(Standard)
- 首次启动时,会自动下载 Android SDK、Gradle 等工具
- 建议分配至少 4GB 内存给 IDE:Help → Change Memory Settings
- 勾选最新的稳定版 SDK Platform(如 API 34/35)
- 在 SDK Tools 标签页中勾选:
Android SDK Build-Tools、Android Emulator、Android SDK Platform-Tools - 安装
Google USB Driver(用于真机调试) - 安装
Intel HAXM或启用Hyper-V(模拟器加速) - 选择设备类型(如 Pixel 7、Pixel Fold)
- 选择系统镜像(推荐 x86_64 版本,支持硬件加速)
- 设置 RAM 大小、内部存储容量
- 启用 HAXM 或 Hyper-V 加速以提升模拟器性能
- 可选启用冷启动/快照模式
- 手机进入
设置 → 关于手机,连续点击版本号7 次开启开发者模式 - 进入
设置 → 开发者选项,开启USB 调试 - 连接电脑后,在手机上确认调试授权对话框
- 无线调试(Android 11+):开发者选项 → 无线调试 → 使用配对码配对
- 在 Android Studio 工具栏选择目标设备(模拟器或真机)
- 点击绿色的 ▶ Run 按钮或按
Shift + F10 - 首次运行需要构建 Gradle,耐心等待
- 应用会自动安装并启动
- Logcat:查看应用日志(底部面板 → Logcat)
- Layout Inspector:实时查看 UI 层级(Tools → Layout Inspector)
- Database Inspector:查看 Room 数据库(App Inspection 面板)
- Apply Changes:无需重启应用即可应用代码/资源改动(⚡ 图标)
- 显式 Intent:明确指定目标组件(Activity/Service)
- 隐式 Intent:描述要执行的动作,由系统匹配合适的组件
- 前台服务:显示通知,用户可见(如音乐播放、下载)
- 后台服务:不可见,Android 8.0+ 受严格限制
- 绑定服务:提供客户端-服务器接口供组件绑定
- 标准广播:异步发送,所有接收器同时收到
- 有序广播:同步依次传递,可被截断、修改
- 本地广播:只在应用内传播,更安全高效
- 粘性广播:已废弃(API 28+),不建议使用
- Retrofit:类型安全的 REST 客户端(推荐)
- OkHttp:底层 HTTP 客户端
- Ktor Client:Kotlin 跨平台网络库
- Volley:轻量级请求库(Google 出品)
- 使用 App Startup 库延迟初始化第三方 SDK
- 避免在 Application.onCreate() 中执行耗时操作
- 使用
Trace.beginSection()追踪启动阶段 - 启用 R8 代码压缩,减小 APK 体积
- 使用 Baseline Profile 优化 ART 编译
- 使用
<include>复用布局 - 使用
<merge>减少根节点 - 使用
ViewStub延迟加载不常用视图 - 扁平化布局,减少嵌套层数
- 使用 Layout Inspector 工具分析布局
- 避免过度绘制(overdraw)
- RecyclerView 使用
setHasFixedSize(true) - 使用
DiffUtil或ListAdapter精准刷新 - 图片加载使用 Glide / Coil,开启缓存
- 避免在
onBindViewHolder中创建对象或设置监听器 - 设置
RecycledViewPool跨 RecyclerView 共享 - 使用
setItemViewCacheSize(20)增加缓存数量 - 预加载
Prefetch提前准备下一个 item - 启用
minifyEnabled true和 R8 压缩 - 启用
shrinkResources true移除未使用资源 - 使用 WebP 格式替代 PNG(体积减少 30%+)
- 使用 Android App Bundle(AAB)按需下发资源
- 删除未使用的资源:Analyze → Inspect Code → Unused Resources
- 使用
resConfigs限制语言资源 - 检查并删除无用依赖
- F8:单步跳过(Step Over)
- F7:单步进入(Step Into)
- Shift+F8:跳出(Step Out)
- F9:运行到下一个断点
- Alt+F9:运行到光标处
- 条件断点:右键断点设置条件,仅在满足时触发
- 日志断点:不暂停,仅输出日志
- 异常断点:在抛出异常时暂停
- 注册 Google Play 开发者账号(一次性 25 美元)
- 在 Google Play Console 创建应用
- 填写应用信息:
- 标题(30 字符内)、简短描述(80 字符)、详细描述(4000 字符)
- 应用图标 512x512 PNG
- 特色图片 1024x500 PNG
- 截图:手机(最少 2 张)、7 寸平板、10 寸平板
- 设置内容分级(IARC 问卷)
- 填写隐私政策 URL(强制)
- 设置定价和分发地区
- 声明广告、目标受众
- 上传 AAB 文件
- 创建发布轨道:内部测试 → 封闭测试 → 开放测试 → 正式版
- 提交审核(一般 1-7 天)
- 软件著作权登记证书(中国)
- ICP 备案(有网站)
- 隐私政策(详细)
- 用户协议
- 应用截图(高清、带文案)
- 应用图标(各尺寸)
- 代码解耦,易于测试
- 自动管理依赖的生命周期
- 与 Jetpack 组件深度集成
- 编译时校验,避免运行时错误
- 在 Firebase Console 创建项目
- 添加 Android 应用,填写包名和 SHA-1
- 下载
google-services.json放到 app/ 目录 - 添加依赖
- 华为 Push:HMS Core Push Kit
- 小米 Push:MiPush
- 极光推送:JPush(统一集成多家)
- 个推:Getui
- 阿里云推送:EMAS Push
- Deep Links:自定义 scheme(如
myapp://path) - Web Links:http/https 链接
- Android App Links:验证的 https 链接(自动打开,无需选择)
- 服务视障、听障、认知障碍、运动障碍用户
- 法律要求(美国 ADA、欧盟 EAA)
- 提升所有用户体验
- Google Play 推荐
Android 开发生态
2. 开发环境搭建
▼
系统要求
| 项目 | 最低要求 | 推荐配置 |
|---|---|---|
| 操作系统 | Windows 10/11、macOS 10.14+、Linux | 最新版 64 位系统 |
| RAM | 8 GB | 16 GB+ |
| 磁盘空间 | 16 GB | 32 GB+(SSD) |
| 屏幕分辨率 | 1280 × 800 | 1920 × 1080+ |
安装 Android Studio
Android Studio 是 Google 官方的集成开发环境(IDE),基于 IntelliJ IDEA 平台。
配置 SDK
打开 Settings → Languages & Frameworks → Android SDK:
配置模拟器 (AVD)
打开 Tools → Device Manager,创建虚拟设备:
真机调试配置
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_*.xml | activity_main.xml, item_user.xml |
| drawable | ic_.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!"
}
}
}
运行应用
常用调试技巧
✅
技巧:
使用
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()
// 从停止状态重新启动
}
}
生命周期场景
| 场景 | 回调顺序 |
|---|---|
| 启动 Activity | onCreate → onStart → onResume |
| 按 Home 键 | onPause → onStop |
| 从后台返回 | onRestart → onStart → onResume |
| 按返回键 | onPause → onStop → onDestroy |
| 屏幕旋转 | onPause → onStop → onDestroy → onCreate → onStart → onResume |
| 弹出对话框 | onPause(Activity 仍可见但不可交互) |
| A 启动 B,B 返回到 A | A.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 类型
显式跳转 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 类型
创建 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 广播
▼
广播类型
接收系统广播
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 完整示例
添加依赖
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 库
| 库 | 特点 | 推荐度 |
|---|---|---|
| Gson | Google 出品,简单易用 | ⭐⭐⭐⭐ |
| Moshi | Square 出品,性能优、Kotlin 友好 | ⭐⭐⭐⭐⭐ |
| kotlinx.serialization | Kotlin 官方,编译时生成,跨平台 | ⭐⭐⭐⭐⭐ |
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.IO | IO 线程池(64线程) | 网络、数据库、文件 |
| Dispatchers.Default | CPU 密集线程池(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
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 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.apply {
setHasFixedSize(true)
setItemViewCacheSize(20)
recycledViewPool.setMaxRecycledViews(0, 30)
// 嵌套 RecyclerView 共享 ViewPool
setRecycledViewPool(sharedPool)
}
APK 瘦身
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 Profiler | CPU / 内存 / 网络 / 能耗监控 |
| 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")
断点调试
单元测试(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-3 天 | 华为手机预装 |
| 小米应用商店 | 免费 | 1-3 天 | MIUI 生态 |
| OPPO 软件商店 | 免费 | 1-3 天 | ColorOS 生态 |
| VIVO 应用商店 | 免费 | 1-3 天 | FuntouchOS |
| 应用宝(腾讯) | 免费 | 1-3 天 | 微信/QQ 生态 |
| 360 手机助手 | 免费 | 1-3 天 | 老牌市场 |
上架必备资料:
多渠道打包
// 使用 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. 架构模式
▼
常见架构对比
| 架构 | 特点 | 适用场景 |
|---|---|---|
| MVC | Controller 臃肿,难测试 | 小项目,不推荐 |
| MVP | Presenter 持有 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。
优势
添加依赖
// 项目 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 | 生命周期 | 对应注解 |
|---|---|---|
| SingletonComponent | Application | @Singleton |
| ActivityComponent | Activity | @ActivityScoped |
| FragmentComponent | Fragment | @FragmentScoped |
| ViewComponent | View | @ViewScoped |
| ServiceComponent | Service | @ServiceScoped |
| ViewModelComponent | ViewModel | @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+)
| 权限组 | 权限 |
|---|---|
| CALENDAR | READ_CALENDAR, WRITE_CALENDAR |
| CAMERA | CAMERA |
| CONTACTS | READ_CONTACTS, WRITE_CONTACTS |
| LOCATION | ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION |
| MICROPHONE | RECORD_AUDIO |
| PHONE | CALL_PHONE, READ_CALL_LOG |
| SENSORS | BODY_SENSORS |
| SMS | SEND_SMS, READ_SMS |
| STORAGE | READ/WRITE_EXTERNAL_STORAGE(API 28+) |
| NOTIFICATIONS | POST_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 提供的免费跨平台消息推送服务。
接入步骤
// 项目 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
)
}
}
国内推送替代方案
31. 深度链接
▼
深度链接类型
配置深度链接
<!-- 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. 图片加载库
▼
常用图片库对比
| 库 | 语言 | 特点 |
|---|---|---|
| Glide | Java/Kotlin | Google 推荐,功能全面 |
| Coil | Kotlin | Kotlin 优先,Compose 友好 |
| Picasso | Java | 简单易用,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. 无障碍适配
▼
为什么需要无障碍
核心原则
| 原则 | 实践 |
|---|---|
| 可感知 | 提供文字替代、高对比度、字幕 |
| 可操作 | 支持键盘、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 AccessibilityChecks | UI 测试集成 |
// 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 |
| 陀螺仪 | 旋转检测、AR | TYPE_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 Actions | GitHub 集成、免费额度大 | 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
))
}
}
}
✅
最佳实践:
建立完整的监控体系:崩溃监控 + 性能监控 + 业务指标监控 + 用户反馈。
😔 没有找到相关内容,请尝试其他关键词