Kotlin Android 开发入门教程
本教程面向零基础读者,从 Kotlin 语法到 Android 实战项目,手把手带你入门 Android 开发。
目录
- Kotlin 语言基础
- Android Studio 安装与配置
- 第一个 Android 项目
- Activity 生命周期
- UI 布局系统
- RecyclerView 列表展示
- 网络请求
- Room 数据库
- Jetpack 核心组件
- 实战项目:天气查询 App
- 学习路线与资源推荐
1. Kotlin 语言基础
Kotlin 是 JetBrains 开发的现代编程语言,自 2019 年起成为 Android 官方推荐语言。相比 Java,Kotlin 更简洁、更安全、更易读。
1.1 变量与数据类型
// 不可变变量(推荐使用 val)
val name: String = "张三"
val age: Int = 25
val height: Double = 1.75
val isStudent: Boolean = false
// 可变变量
var score: Int = 90
score = 95 // 可以重新赋值
// 类型推断 —— 编译器自动推断类型,无需显式声明
val city = "北京" // 推断为 String
val temperature = 28.5 // 推断为 Double
基本数据类型:
Int/Long— 整数Float/Double— 浮点数String— 字符串Boolean— 布尔值Char— 单个字符
1.2 字符串模板
val name = "小明"
val age = 20
// 使用 $ 引用变量
println("我叫$name,今年${age}岁")
// 表达式需要用花括号包裹
println("明年我就${age + 1}岁了")
// 多行字符串
val text = """
第一行
第二行
第三行
""".trimIndent()
1.3 条件判断
// if-else 表达式(Kotlin 中 if 是表达式,有返回值)
val score = 85
val grade = if (score >= 90) {
"优秀"
} else if (score >= 80) {
"良好"
} else if (score >= 60) {
"及格"
} else {
"不及格"
}
println(grade) // 输出:良好
// when 表达式(替代 switch,更强大)
val day = 3
val dayName = when (day) {
1 -> "星期一"
2 -> "星期二"
3 -> "星期三"
4 -> "星期四"
5 -> "星期五"
6, 7 -> "周末"
else -> "无效"
}
1.4 循环
// for 循环 —— 遍历范围
for (i in 1..5) {
println(i) // 输出 1 2 3 4 5
}
// until 不包含结束值
for (i in 0 until 5) {
println(i) // 输出 0 1 2 3 4
}
// step 步长
for (i in 0..10 step 2) {
println(i) // 输出 0 2 4 6 8 10
}
// 遍历集合
val fruits = listOf("苹果", "香蕉", "橘子")
for (fruit in fruits) {
println(fruit)
}
// 带索引遍历
for ((index, fruit) in fruits.withIndex()) {
println("$index: $fruit")
}
// while 循环
var count = 0
while (count < 5) {
println(count)
count++
}
1.5 函数
// 基本函数
fun add(a: Int, b: Int): Int {
return a + b
}
// 单表达式函数(简写)
fun multiply(a: Int, b: Int) = a * b
// 默认参数
fun greet(name: String, greeting: String = "你好") {
println("$greeting, $name!")
}
greet("小明") // 输出:你好, 小明!
greet("小明", "早上好") // 输出:早上好, 小明!
// 命名参数
greet(greeting = "嗨", name = "小红")
1.6 集合操作
// List(不可变)
val numbers = listOf(1, 2, 3, 4, 5)
// MutableList(可变)
val mutableNumbers = mutableListOf(1, 2, 3)
mutableNumbers.add(4)
mutableNumbers.remove(2)
// Map
val userMap = mapOf("name" to "张三", "age" to 25)
val mutableMap = mutableMapOf<String, Any>()
mutableMap["email"] = "test@example.com"
// 集合操作(链式调用,非常常用)
val result = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
.filter { it % 2 == 0 } // 筛选偶数:[2, 4, 6, 8, 10]
.map { it * it } // 平方:[4, 16, 36, 64, 100]
.sum() // 求和:220
1.7 空安全
Kotlin 的类型系统区分可空和非空类型,这是 Kotlin 最重要的安全特性之一。
// 非空类型
var name: String = "张三"
// name = null // 编译错误!
// 可空类型(加 ?)
var nullableName: String? = null
// 安全调用操作符 ?.
println(nullableName?.length) // null,不会崩溃
// Elvis 操作符 ?:
val length = nullableName?.length ?: 0 // 如果为 null,使用默认值 0
// 非空断言 !!(谨慎使用,可能抛出 NPE)
// val length2 = nullableName!!.length // 如果为 null 会崩溃
// 安全转换 as?
val obj: Any = "Hello"
val str: String? = obj as? String // 转换成功
val num: Int? = obj as? Int // 转换失败,返回 null
1.8 类与数据类
// 普通类
class Person(val name: String, var age: Int) {
fun introduce() {
println("我叫$name,今年${age}岁")
}
}
val person = Person("小明", 20)
person.introduce()
// 数据类 —— 自动生成 equals、hashCode、toString、copy 等方法
data class User(val id: Int, val name: String, val email: String)
val user1 = User(1, "张三", "zhangsan@example.com")
val user2 = user1.copy(name = "李四") // 复制并修改部分字段
println(user1) // User(id=1, name=张三, email=zhangsan@example.com)
// 解构声明
val (id, name, email) = user1
println("$id, $name, $email")
1.9 Lambda 表达式与高阶函数
// Lambda 表达式
val square: (Int) -> Int = { x -> x * x }
println(square(5)) // 25
// 单参数时可用 it
val double: (Int) -> Int = { it * 2 }
// 高阶函数 —— 接受函数作为参数
fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
return operation(a, b)
}
val sum = calculate(10, 5) { x, y -> x + y } // 15
val product = calculate(10, 5) { x, y -> x * y } // 50
// 常用高阶函数
val numbers = listOf(1, 2, 3, 4, 5)
numbers.filter { it > 2 } // [3, 4, 5]
numbers.map { it * 10 } // [10, 20, 30, 40, 50]
numbers.forEach { println(it) } // 逐个打印
numbers.reduce { acc, i -> acc + i } // 15
2. Android Studio 安装与配置
2.1 系统要求
| 平台 | 最低要求 |
|---|---|
| Windows | 64 位,8GB RAM(推荐 16GB),至少 8GB 可用磁盘空间 |
| macOS | macOS 10.14+,8GB RAM(推荐 16GB) |
| Linux | 64 位 GNOME 或 KDE,8GB RAM |
2.2 安装步骤
下载 Android Studio
- 访问 Android Studio 官网:
https://developer.android.com/studio - 下载最新稳定版
- 访问 Android Studio 官网:
安装 JDK
- Android Studio 自带 JBR(JetBrains Runtime),通常无需单独安装
- 如需手动安装:推荐 JDK 17
首次启动配置
- 选择 Standard 安装类型
- SDK 会自动下载到默认目录
- 等待 Gradle 同步和 SDK 组件下载完成
配置国内镜像(可选,加速下载)
- 在
gradle.properties中添加:
# 阿里云镜像 systemProp.https.proxyHost=mirrors.aliyun.com- 或在项目
settings.gradle.kts的pluginManagement中添加镜像仓库
- 在
2.3 创建模拟器
- 打开 Tools → Device Manager
- 点击 Create Device
- 选择设备型号(推荐 Pixel 6)
- 下载系统镜像(推荐 API 34)
- 完成创建后点击启动按钮运行模拟器
3. 第一个 Android 项目
3.1 创建项目
- 打开 Android Studio → New Project
- 选择 Empty Views Activity(传统 View 体系)
- 配置:
- Name:
MyFirstApp - Package name:
com.example.myfirstapp - Language: Kotlin
- Minimum SDK: API 24(覆盖 99%+ 设备)
- Name:
- 点击 Finish,等待 Gradle 同步完成
3.2 项目结构
app/
├── src/
│ ├── main/
│ │ ├── java/com/example/myfirstapp/ ← Kotlin 代码
│ │ │ └── MainActivity.kt
│ │ ├── res/ ← 资源文件
│ │ │ ├── layout/ ← 布局文件
│ │ │ │ └── activity_main.xml
│ │ │ ├── values/ ← 值资源
│ │ │ │ ├── strings.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── themes.xml
│ │ │ ├── drawable/ ← 图片/图形
│ │ │ └── mipmap/ ← 应用图标
│ │ └── AndroidManifest.xml ← 应用清单
│ └── test/ ← 单元测试
├── build.gradle.kts ← 模块构建配置
└── ...
build.gradle.kts ← 项目构建配置
settings.gradle.kts ← 项目设置
3.3 Hello World
activity_main.xml(布局文件):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/tvHello"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello, Android!"
android:textSize="24sp" />
<Button
android:id="@+id/btnClick"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="点击我"
android:layout_marginTop="16dp" />
</LinearLayout>
MainActivity.kt(主活动):
package com.example.myfirstapp
import android.os.Bundle
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 获取控件引用
val tvHello = findViewById<TextView>(R.id.tvHello)
val btnClick = findViewById<Button>(R.id.btnClick)
// 设置点击事件
btnClick.setOnClickListener {
tvHello.text = "你点击了按钮!"
Toast.makeText(this, "Hello!", Toast.LENGTH_SHORT).show()
}
}
}
4. Activity 生命周期
Activity 是 Android 应用的核心组件,每个界面通常对应一个 Activity。理解生命周期对开发至关重要。
4.1 生命周期方法
onCreate() → Activity 被创建(初始化布局、数据)
↓
onStart() → Activity 变为可见
↓
onResume() → Activity 获得焦点,可交互
↓
onPause() → Activity 失去焦点(被部分遮挡)
↓
onStop() → Activity 完全不可见
↓
onDestroy() → Activity 被销毁
4.2 生命周期实战
class MainActivity : AppCompatActivity() {
private val TAG = "MainActivity"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
Log.d(TAG, "onCreate: Activity 创建")
// 恢复保存的状态
savedInstanceState?.let {
val savedText = it.getString("key_text", "")
Log.d(TAG, "恢复状态: $savedText")
}
}
override fun onStart() {
super.onStart()
Log.d(TAG, "onStart: Activity 可见")
}
override fun onResume() {
super.onResume()
Log.d(TAG, "onResume: Activity 可交互")
// 适合开始动画、监听传感器等
}
override fun onPause() {
super.onPause()
Log.d(TAG, "onPause: Activity 暂停")
// 适合保存草稿、暂停动画
}
override fun onStop() {
super.onStop()
Log.d(TAG, "onStop: Activity 不可见")
// 适合释放资源
}
override fun onDestroy() {
super.onDestroy()
Log.d(TAG, "onDestroy: Activity 销毁")
}
// 保存状态 —— 在 Activity 可能被系统销毁前调用
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString("key_text", "一些需要保存的数据")
Log.d(TAG, "onSaveInstanceState: 状态已保存")
}
}
4.3 Activity 间跳转与传参
// 从 MainActivity 跳转到 DetailActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
findViewById<Button>(R.id.btnGoDetail).setOnClickListener {
val intent = Intent(this, DetailActivity::class.java).apply {
putExtra("EXTRA_TITLE", "文章标题")
putExtra("EXTRA_ID", 42)
}
startActivity(intent)
}
}
}
// DetailActivity 接收参数
class DetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_detail)
val title = intent.getStringExtra("EXTRA_TITLE") ?: ""
val id = intent.getIntExtra("EXTRA_ID", 0)
findViewById<TextView>(R.id.tvTitle).text = title
Log.d("Detail", "接收到 ID: $id")
}
}
注意: 别忘了在
AndroidManifest.xml中注册新 Activity。
5. UI 布局系统
Android 使用 XML 定义界面布局。掌握常用布局和控件是开发的基础。
5.1 常用布局
LinearLayout(线性布局)—— 按行或列排列子控件
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="用户名" />
<EditText
android:id="@+id/etUsername"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入用户名" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="密码" />
<EditText
android:id="@+id/etPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="请输入密码"
android:inputType="textPassword" />
<Button
android:id="@+id/btnLogin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="24dp"
android:text="登录" />
</LinearLayout>
RelativeLayout(相对布局)—— 通过相对位置定位子控件
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp">
<ImageView
android:id="@+id/ivAvatar"
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@drawable/ic_launcher_foreground"
android:layout_centerVertical="true" />
<TextView
android:id="@+id/tvName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/ivAvatar"
android:layout_marginStart="16dp"
android:text="张三"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvDesc"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/tvName"
android:layout_toEndOf="@id/ivAvatar"
android:layout_marginStart="16dp"
android:layout_marginTop="4dp"
android:text="Android 开发者"
android:textColor="#666666" />
</RelativeLayout>
ConstraintLayout(约束布局)—— 最灵活、性能最好的布局
<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/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="标题"
android:textSize="24sp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_marginTop="32dp" />
<Button
android:id="@+id/btnAction"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="操作"
android:layout_margin="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
5.2 常用控件
<!-- ImageView 图片 -->
<ImageView
android:layout_width="100dp"
android:layout_height="100dp"
android:src="@drawable/sample"
android:scaleType="centerCrop" />
<!-- CheckBox 复选框 -->
<CheckBox
android:id="@+id/cbAgree"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="同意协议" />
<!-- RadioGroup + RadioButton 单选 -->
<RadioGroup
android:id="@+id/rgGender"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<RadioButton android:id="@+id/rbMale" android:text="男" />
<RadioButton android:id="@+id/rbFemale" android:text="女" />
</RadioGroup>
<!-- Spinner 下拉选择 -->
<Spinner
android:id="@+id/spinnerCity"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
5.3 Kotlin 中操作 UI 控件
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 方式一:findViewById(传统方式)
val tvTitle = findViewById<TextView>(R.id.tvTitle)
tvTitle.text = "新的标题"
tvTitle.setTextColor(Color.BLUE)
// 方式二:ViewBinding(推荐,类型安全)
// 需在 build.gradle.kts 中启用 viewBinding
val binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
binding.tvTitle.text = "新的标题"
binding.btnAction.setOnClickListener {
// 处理点击
}
}
}
启用 ViewBinding(在 build.gradle.kts 中):
android {
buildFeatures {
viewBinding = true
}
}
6. RecyclerView 列表展示
RecyclerView 是 Android 中展示列表数据的核心组件,取代了旧的 ListView。
6.1 添加依赖
在 app/build.gradle.kts 中:
dependencies {
implementation("androidx.recyclerview:recyclerview:1.3.2")
}
6.2 创建数据类
data class Article(
val id: Int,
val title: String,
val summary: String,
val date: String
)
6.3 创建列表项布局
res/layout/item_article.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<TextView
android:id="@+id/tvTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvSummary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="#666666"
android:maxLines="2"
android:ellipsize="end" />
<TextView
android:id="@+id/tvDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="12sp"
android:textColor="#999999" />
</LinearLayout>
6.4 创建 Adapter
class ArticleAdapter(
private val articles: List<Article>,
private val onClick: (Article) -> Unit = {}
) : RecyclerView.Adapter<ArticleAdapter.ViewHolder>() {
// ViewHolder 持有列表项中的控件引用
class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
val tvTitle: TextView = view.findViewById(R.id.tvTitle)
val tvSummary: TextView = view.findViewById(R.id.tvSummary)
val tvDate: TextView = view.findViewById(R.id.tvDate)
}
// 创建 ViewHolder(加载布局)
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.item_article, parent, false)
return ViewHolder(view)
}
// 绑定数据到 ViewHolder
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val article = articles[position]
holder.tvTitle.text = article.title
holder.tvSummary.text = article.summary
holder.tvDate.text = article.date
holder.itemView.setOnClickListener { onClick(article) }
}
// 返回列表项总数
override fun getItemCount() = articles.size
}
6.5 在 Activity 中使用
class ArticleListActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_article_list)
// 准备数据
val articles = listOf(
Article(1, "Kotlin 入门指南", "学习 Kotlin 的基础知识...", "2024-01-15"),
Article(2, "Android 14 新特性", "最新 Android 版本带来了...", "2024-01-20"),
Article(3, "Jetpack Compose 实战", "使用声明式 UI 构建界面...", "2024-02-01")
)
// 配置 RecyclerView
val rvArticles = findViewById<RecyclerView>(R.id.rvArticles)
rvArticles.layoutManager = LinearLayoutManager(this)
rvArticles.adapter = ArticleAdapter(articles) { article ->
Toast.makeText(this, "点击了: ${article.title}", Toast.LENGTH_SHORT).show()
}
}
}
activity_article_list.xml:
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/rvArticles"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp" />
7. 网络请求
7.1 添加依赖(Retrofit + OkHttp)
dependencies {
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")
}
添加网络权限(AndroidManifest.xml):
<uses-permission android:name="android.permission.INTERNET" />
7.2 定义数据模型
data class WeatherResponse(
val city: String,
val temperature: Double,
val description: String,
val humidity: Int
)
data class ApiResponse<T>(
val code: Int,
val message: String,
val data: T?
)
7.3 定义 API 接口
interface ApiService {
@GET("weather")
suspend fun getWeather(
@Query("city") city: String,
@Query("key") apiKey: String
): WeatherResponse
@POST("users/login")
suspend fun login(@Body request: LoginRequest): ApiResponse<User>
@GET("articles")
suspend fun getArticles(
@Query("page") page: Int = 1,
@Query("size") size: Int = 20
): ApiResponse<List<Article>>
}
data class LoginRequest(val username: String, val password: String)
data class User(val id: Int, val name: String, val token: String)
7.4 创建 Retrofit 实例
object RetrofitClient {
private const val BASE_URL = "https://api.example.com/"
private val okHttpClient = OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
val apiService: ApiService by lazy {
Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
.create(ApiService::class.java)
}
}
7.5 发起网络请求
class WeatherActivity : AppCompatActivity() {
// 使用生命周期感知的协程作用域
private val lifecycleScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_weather)
fetchWeather("北京")
}
private fun fetchWeather(city: String) {
lifecycleScope.launch {
try {
// 显示加载状态
showLoading(true)
// 切到 IO 线程执行网络请求
val weather = withContext(Dispatchers.IO) {
RetrofitClient.apiService.getWeather(city, "your_api_key")
}
// 回到主线程更新 UI
findViewById<TextView>(R.id.tvCity).text = weather.city
findViewById<TextView>(R.id.tvTemp).text = "${weather.temperature}°C"
findViewById<TextView>(R.id.tvDesc).text = weather.description
} catch (e: Exception) {
Toast.makeText(
this@WeatherActivity,
"请求失败: ${e.message}",
Toast.LENGTH_LONG
).show()
} finally {
showLoading(false)
}
}
}
private fun showLoading(show: Boolean) {
findViewById<ProgressBar>(R.id.progressBar).visibility =
if (show) View.VISIBLE else View.GONE
}
override fun onDestroy() {
super.onDestroy()
lifecycleScope.cancel()
}
}
8. Room 数据库
Room 是 Jetpack 提供的本地数据库库,对 SQLite 进行了封装,提供了编译时 SQL 校验。
8.1 添加依赖
dependencies {
val roomVersion = "2.6.1"
implementation("androidx.room:room-runtime:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion") // 协程支持
kapt("androidx.room:room-compiler:$roomVersion") // 注解处理器
}
在 build.gradle.kts 顶部启用 kapt:
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
}
8.2 创建实体(Entity)
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "cities")
data class City(
@PrimaryKey(autoGenerate = true)
val id: Int = 0,
val name: String,
val province: String,
val population: Long,
val isFavorite: Boolean = false
)
8.3 创建 DAO(数据访问对象)
import androidx.room.*
@Dao
interface CityDao {
@Query("SELECT * FROM cities ORDER BY name ASC")
suspend fun getAllCities(): List<City>
@Query("SELECT * FROM cities WHERE isFavorite = 1")
suspend fun getFavoriteCities(): List<City>
@Query("SELECT * FROM cities WHERE name LIKE '%' || :keyword || '%'")
suspend fun searchCities(keyword: String): List<City>
@Query("SELECT * FROM cities WHERE id = :cityId")
suspend fun getCityById(cityId: Int): City?
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertCity(city: City)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertCities(cities: List<City>)
@Update
suspend fun updateCity(city: City)
@Delete
suspend fun deleteCity(city: City)
@Query("DELETE FROM cities")
suspend fun deleteAll()
}
8.4 创建数据库
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
@Database(entities = [City::class], version = 1, exportSchema = false)
abstract class AppDatabase : RoomDatabase() {
abstract fun cityDao(): CityDao
companion object {
@Volatile
private var INSTANCE: AppDatabase? = null
fun getInstance(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
AppDatabase::class.java,
"weather_app.db"
).build()
INSTANCE = instance
instance
}
}
}
}
8.5 使用数据库
class CityRepository(context: Context) {
private val cityDao = AppDatabase.getInstance(context).cityDao()
suspend fun getAllCities() = cityDao.getAllCities()
suspend fun addCity(name: String, province: String) {
cityDao.insertCity(City(name = name, province = province, population = 0))
}
suspend fun toggleFavorite(city: City) {
cityDao.updateCity(city.copy(isFavorite = !city.isFavorite))
}
suspend fun search(keyword: String) = cityDao.searchCities(keyword)
}
9. Jetpack 核心组件
Jetpack 是 Android 官方的组件库,帮助开发者遵循最佳实践、减少样板代码。
9.1 ViewModel
ViewModel 在配置变更(如屏幕旋转)时保持数据不丢失。
class WeatherViewModel : ViewModel() {
// 使用 StateFlow 管理 UI 状态
private val _weatherState = MutableStateFlow<WeatherState>(WeatherState.Loading)
val weatherState: StateFlow<WeatherState> = _weatherState.asStateFlow()
private val _city = MutableStateFlow("北京")
val city: StateFlow<String> = _city.asStateFlow()
fun updateCity(newCity: String) {
_city.value = newCity
fetchWeather(newCity)
}
private fun fetchWeather(city: String) {
viewModelScope.launch {
_weatherState.value = WeatherState.Loading
try {
val response = withContext(Dispatchers.IO) {
RetrofitClient.apiService.getWeather(city, "your_key")
}
_weatherState.value = WeatherState.Success(response)
} catch (e: Exception) {
_weatherState.value = WeatherState.Error(e.message ?: "未知错误")
}
}
}
}
// UI 状态密封类
sealed class WeatherState {
object Loading : WeatherState()
data class Success(val data: WeatherResponse) : WeatherState()
data class Error(val message: String) : WeatherState()
}
在 Activity 中使用:
class WeatherActivity : AppCompatActivity() {
private val viewModel: WeatherViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_weather)
// 使用 lifecycleScope 收集状态
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.weatherState.collect { state ->
when (state) {
is WeatherState.Loading -> showLoading()
is WeatherState.Success -> showWeather(state.data)
is WeatherState.Error -> showError(state.message)
}
}
}
}
}
// 搜索按钮
findViewById<Button>(R.id.btnSearch).setOnClickListener {
val city = findViewById<EditText>(R.id.etCity).text.toString()
viewModel.updateCity(city)
}
}
}
9.2 LiveData(传统响应式方案)
class UserViewModel : ViewModel() {
private val _user = MutableLiveData<User>()
val user: LiveData<User> = _user
private val _error = MutableLiveData<String>()
val error: LiveData<String> = _error
fun loadUser(userId: Int) {
viewModelScope.launch {
try {
val result = withContext(Dispatchers.IO) {
// 模拟网络请求
User(userId, "张三", "zhangsan@example.com")
}
_user.value = result
} catch (e: Exception) {
_error.value = e.message
}
}
}
}
// 在 Activity 中观察
class UserActivity : AppCompatActivity() {
private val viewModel: UserViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel.user.observe(this) { user ->
// 数据更新时自动调用
findViewById<TextView>(R.id.tvName).text = user.name
}
viewModel.error.observe(this) { errorMsg ->
Toast.makeText(this, errorMsg, Toast.LENGTH_SHORT).show()
}
viewModel.loadUser(1)
}
}
9.3 Navigation(导航组件)
添加依赖:
dependencies {
implementation("androidx.navigation:navigation-fragment-ktx:2.7.7")
implementation("androidx.navigation:navigation-ui-ktx:2.7.7")
}
创建导航图 res/navigation/nav_graph.xml:
<?xml version="1.0" encoding="utf-8"?>
<navigation
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/nav_graph"
app:startDestination="@id/homeFragment">
<fragment
android:id="@+id/homeFragment"
android:name="com.example.app.HomeFragment"
android:label="首页">
<action
android:id="@+id/action_home_to_detail"
app:destination="@id/detailFragment" />
</fragment>
<fragment
android:id="@+id/detailFragment"
android:name="com.example.app.DetailFragment"
android:label="详情">
<argument
android:name="itemId"
app:argType="integer" />
</fragment>
</navigation>
在 Activity 中设置 NavHost:
<!-- activity_main.xml -->
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />
在代码中导航:
// 从 HomeFragment 跳转到 DetailFragment
findNavController().navigate(
R.id.action_home_to_detail,
bundleOf("itemId" to 42)
)
// 在 DetailFragment 中获取参数
val itemId = arguments?.getInt("itemId") ?: 0
9.4 DataStore(键值对存储,替代 SharedPreferences)
dependencies {
implementation("androidx.datastore:datastore-preferences:1.0.0")
}
// 创建 DataStore
val Context.settingsDataStore by preferencesDataStore(name = "settings")
class SettingsRepository(private val context: Context) {
companion object {
val KEY_CITY = stringPreferencesKey("default_city")
val KEY_DARK_MODE = booleanPreferencesKey("dark_mode")
val KEY_FONT_SIZE = intPreferencesKey("font_size")
}
// 读取
val defaultCity: Flow<String> = context.settingsDataStore.data
.map { prefs -> prefs[KEY_CITY] ?: "北京" }
// 写入
suspend fun setDefaultCity(city: String) {
context.settingsDataStore.edit { prefs ->
prefs[KEY_CITY] = city
}
}
}
10. 实战项目:天气查询 App
下面我们将综合运用前面学到的知识,构建一个完整的天气查询 App。
10.1 项目功能
- 搜索城市天气
- 显示当前温度、天气描述、湿度
- 保存搜索历史到本地数据库
- 收藏常用城市
- 使用 ViewModel 管理状态
10.2 添加依赖
// app/build.gradle.kts
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")
// Lifecycle + ViewModel
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
// Room
val roomVersion = "2.6.1"
implementation("androidx.room:room-runtime:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion")
kapt("androidx.room:room-compiler:$roomVersion")
// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
// RecyclerView
implementation("androidx.recyclerview:recyclerview:1.3.2")
}
10.3 数据层
天气 API 接口:
interface WeatherApiService {
@GET("v2/city/lookup")
suspend fun searchCity(
@Query("location") location: String,
@Query("key") key: String
): CityLookupResponse
@GET("v7/weather/now")
suspend fun getCurrentWeather(
@Query("location") location: String,
@Query("key") key: String
): WeatherNowResponse
}
data class CityLookupResponse(val location: List<LocationInfo>)
data class LocationInfo(val id: String, val name: String, val country: String)
data class WeatherNowResponse(
val now: WeatherNow,
val updateTime: String
)
data class WeatherNow(
val temp: String,
val feelsLike: String,
val text: String,
val humidity: String,
val windDir: String,
val windSpeed: String
)
Room 实体与 DAO:
@Entity(tableName = "search_history")
data class SearchHistory(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val cityName: String,
val locationId: String,
val searchedAt: Long = System.currentTimeMillis()
)
@Entity(tableName = "favorite_cities")
data class FavoriteCity(
@PrimaryKey val locationId: String,
val cityName: String,
val lastTemp: String = "",
val lastWeather: String = ""
)
@Dao
interface WeatherDao {
@Query("SELECT * FROM search_history ORDER BY searchedAt DESC LIMIT 20")
suspend fun getRecentSearches(): List<SearchHistory>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertSearch(history: SearchHistory)
@Query("DELETE FROM search_history")
suspend fun clearHistory()
@Query("SELECT * FROM favorite_cities ORDER BY cityName ASC")
suspend fun getFavorites(): List<FavoriteCity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun addFavorite(city: FavoriteCity)
@Delete
suspend fun removeFavorite(city: FavoriteCity)
}
10.4 Repository 层
class WeatherRepository(
private val api: WeatherApiService,
private val dao: WeatherDao,
private val apiKey: String
) {
// 搜索城市
suspend fun searchCity(query: String): List<LocationInfo> {
return api.searchCity(query, apiKey).location
}
// 获取实时天气
suspend fun getCurrentWeather(locationId: String): WeatherNowResponse {
return api.getCurrentWeather(locationId, apiKey)
}
// 保存搜索记录
suspend fun saveSearch(cityName: String, locationId: String) {
dao.insertSearch(SearchHistory(cityName = cityName, locationId = locationId))
}
// 获取搜索历史
suspend fun getSearchHistory() = dao.getRecentSearches()
// 收藏管理
suspend fun getFavorites() = dao.getFavorites()
suspend fun addFavorite(cityName: String, locationId: String) {
dao.addFavorite(FavoriteCity(locationId = locationId, cityName = cityName))
}
suspend fun removeFavorite(city: FavoriteCity) = dao.removeFavorite(city)
}
10.5 ViewModel
class WeatherViewModel(application: Application) : AndroidViewModel(application) {
private val repository: WeatherRepository
private val _searchResults = MutableStateFlow<List<LocationInfo>>(emptyList())
val searchResults: StateFlow<List<LocationInfo>> = _searchResults
private val _currentWeather = MutableStateFlow<WeatherNowResponse?>(null)
val currentWeather: StateFlow<WeatherNowResponse?> = _currentWeather
private val _searchHistory = MutableStateFlow<List<SearchHistory>>(emptyList())
val searchHistory: StateFlow<List<SearchHistory>> = _searchHistory
private val _favorites = MutableStateFlow<List<FavoriteCity>>(emptyList())
val favorites: StateFlow<List<FavoriteCity>> = _favorites
private val _isLoading = MutableStateFlow(false)
val isLoading: StateFlow<Boolean> = _isLoading
private val _error = MutableStateFlow<String?>(null)
val error: StateFlow<String?> = _error
init {
// 初始化 Repository
val db = AppDatabase.getInstance(application)
val retrofit = Retrofit.Builder()
.baseUrl("https://geoapi.qweather.com/")
.addConverterFactory(GsonConverterFactory.create())
.build()
val api = retrofit.create(WeatherApiService::class.java)
repository = WeatherRepository(api, db.weatherDao(), "your_api_key")
// 加载初始数据
loadHistory()
loadFavorites()
}
fun searchCity(query: String) {
if (query.isBlank()) return
viewModelScope.launch {
_isLoading.value = true
_error.value = null
try {
_searchResults.value = withContext(Dispatchers.IO) {
repository.searchCity(query)
}
} catch (e: Exception) {
_error.value = "搜索失败: ${e.message}"
} finally {
_isLoading.value = false
}
}
}
fun selectCity(locationId: String, cityName: String) {
viewModelScope.launch {
_isLoading.value = true
try {
val weather = withContext(Dispatchers.IO) {
repository.getCurrentWeather(locationId)
}
_currentWeather.value = weather
// 保存搜索记录
withContext(Dispatchers.IO) {
repository.saveSearch(cityName, locationId)
}
loadHistory()
} catch (e: Exception) {
_error.value = "获取天气失败: ${e.message}"
} finally {
_isLoading.value = false
}
}
}
fun addToFavorite(cityName: String, locationId: String) {
viewModelScope.launch(Dispatchers.IO) {
repository.addFavorite(cityName, locationId)
loadFavorites()
}
}
private fun loadHistory() {
viewModelScope.launch(Dispatchers.IO) {
_searchHistory.value = repository.getSearchHistory()
}
}
private fun loadFavorites() {
viewModelScope.launch(Dispatchers.IO) {
_favorites.value = repository.getFavorites()
}
}
}
10.6 主界面布局
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<!-- 搜索栏 -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<EditText
android:id="@+id/etSearch"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:hint="输入城市名称"
android:inputType="text"
android:paddingStart="12dp" />
<Button
android:id="@+id/btnSearch"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:text="搜索" />
</LinearLayout>
<!-- 天气信息卡片 -->
<androidx.cardview.widget.CardView
android:id="@+id/cardWeather"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:visibility="gone"
app:cardCornerRadius="12dp"
app:cardElevation="4dp"
xmlns:app="http://schemas.android.com/apk/res-auto">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="20dp">
<TextView
android:id="@+id/tvCityName"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/tvTemperature"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="48sp"
android:textColor="#FF6B35" />
<TextView
android:id="@+id/tvWeatherText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textSize="18sp"
android:textColor="#666666" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="horizontal">
<TextView
android:id="@+id/tvHumidity"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="湿度: --" />
<TextView
android:id="@+id/tvWind"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="风向: --" />
</LinearLayout>
<Button
android:id="@+id/btnFavorite"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="⭐ 收藏" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<!-- 加载指示器 -->
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="32dp"
android:visibility="gone" />
<!-- 搜索结果列表 -->
<TextView
android:id="@+id/tvSearchResultsTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="搜索结果"
android:textStyle="bold"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvSearchResults"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp" />
<!-- 搜索历史 -->
<TextView
android:id="@+id/tvHistoryTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="搜索历史"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rvHistory"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp" />
</LinearLayout>
10.7 MainActivity 完整代码
class MainActivity : AppCompatActivity() {
private val viewModel: WeatherViewModel by viewModels()
private var currentLocationId: String? = null
private var currentCityName: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val etSearch = findViewById<EditText>(R.id.etSearch)
val btnSearch = findViewById<Button>(R.id.btnSearch)
val cardWeather = findViewById<View>(R.id.cardWeather)
val progressBar = findViewById<ProgressBar>(R.id.progressBar)
val rvResults = findViewById<RecyclerView>(R.id.rvSearchResults)
val rvHistory = findViewById<RecyclerView>(R.id.rvHistory)
val tvResultsTitle = findViewById<TextView>(R.id.tvSearchResultsTitle)
// RecyclerView 配置
rvResults.layoutManager = LinearLayoutManager(this)
rvHistory.layoutManager = LinearLayoutManager(this)
// 搜索按钮
btnSearch.setOnClickListener {
val query = etSearch.text.toString().trim()
viewModel.searchCity(query)
}
// 观察状态
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
// 加载状态
launch {
viewModel.isLoading.collect { loading ->
progressBar.visibility = if (loading) View.VISIBLE else View.GONE
}
}
// 搜索结果
launch {
viewModel.searchResults.collect { results ->
if (results.isNotEmpty()) {
tvResultsTitle.visibility = View.VISIBLE
rvResults.adapter = CityAdapter(results) { location ->
currentLocationId = location.id
currentCityName = location.name
viewModel.selectCity(location.id, location.name)
}
}
}
}
// 天气数据
launch {
viewModel.currentWeather.collect { weather ->
weather?.let {
cardWeather.visibility = View.VISIBLE
findViewById<TextView>(R.id.tvCityName).text =
currentCityName ?: "未知"
findViewById<TextView>(R.id.tvTemperature).text =
"${it.now.temp}°C"
findViewById<TextView>(R.id.tvWeatherText).text =
it.now.text
findViewById<TextView>(R.id.tvHumidity).text =
"湿度: ${it.now.humidity}%"
findViewById<TextView>(R.id.tvWind).text =
"风向: ${it.now.windDir}"
}
}
}
// 搜索历史
launch {
viewModel.searchHistory.collect { history ->
rvHistory.adapter = HistoryAdapter(history) { item ->
currentLocationId = item.locationId
currentCityName = item.cityName
viewModel.selectCity(item.locationId, item.cityName)
}
}
}
// 错误处理
launch {
viewModel.error.collect { error ->
error?.let {
Toast.makeText(this@MainActivity, it, Toast.LENGTH_LONG).show()
}
}
}
}
}
// 收藏按钮
findViewById<Button>(R.id.btnFavorite).setOnClickListener {
currentLocationId?.let { id ->
currentCityName?.let { name ->
viewModel.addToFavorite(name, id)
Toast.makeText(this, "已收藏 $name", Toast.LENGTH_SHORT).show()
}
}
}
}
}
10.8 运行效果
运行 App 后:
- 在搜索框输入城市名称(如"北京"),点击搜索
- 从搜索结果中选择城市
- 查看实时天气信息(温度、天气状况、湿度、风向)
- 点击收藏按钮保存常查城市
- 搜索历史自动记录,点击可快速查看
11. 学习路线与资源推荐
11.1 学习路线
第一阶段:基础(2-4 周)
├── Kotlin 语法基础
├── Android 四大组件
├── UI 布局与控件
└── 事件处理
第二阶段:进阶(4-6 周)
├── RecyclerView 高级用法
├── 网络请求(Retrofit + OkHttp)
├── 本地存储(Room / DataStore)
├── ViewModel + LiveData / StateFlow
└── Fragment 与 Navigation
第三阶段:高级(6-8 周)
├── Jetpack Compose(声明式 UI)
├── MVVM / MVI 架构
├── 依赖注入(Hilt)
├── 协程深入(Flow / Channel)
└── 性能优化
第四阶段:实战(持续)
├── 完整项目开发
├── 发布到 Google Play
├── 阅读开源项目源码
└── 持续跟进 Android 新版本特性
11.2 推荐资源
- 官方文档:
developer.android.com— 最权威的学习资料 - Kotlin 官方文档:
kotlinlang.org/docs— Kotlin 语法参考 - Android Developers YouTube — 官方视频教程
- Codelab:
developer.android.com/courses— 交互式教程 - 开源项目参考:在 GitHub 上搜索 Android 开源项目学习实际代码
11.3 常见问题
Q:选 Kotlin 还是 Java? A:强烈推荐 Kotlin。它是 Google 官方推荐语言,语法简洁、空安全、协程支持好,新项目几乎都用 Kotlin。
Q:需要买实体 Android 手机吗? A:初期用模拟器就够了。模拟器性能已大幅提升,支持绝大多数功能。后期如需测试 NFC、摄像头等硬件功能,再考虑真机。
Q:多久能独立开发 App? A:因人而异。坚持每天学习 1-2 小时,大约 2-3 个月可以开发简单的 App,6 个月左右可以独立完成中等复杂度的项目。
Q:学完这个教程下一步学什么? A:建议学习 Jetpack Compose(Android 新一代 UI 框架),它代表了 Android UI 开发的未来方向。
附录:build.gradle.kts 完整配置参考
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
}
android {
namespace = "com.example.weatherapp"
compileSdk = 34
defaultConfig {
applicationId = "com.example.weatherapp"
minSdk = 24
targetSdk = 34
versionCode = 1
versionName = "1.0"
}
buildTypes {
release {
isMinifyEnabled = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
buildFeatures {
viewBinding = true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
}
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.recyclerview:recyclerview:1.3.2")
implementation("androidx.cardview:cardview:1.0.0")
// Lifecycle
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
// Room
val roomVersion = "2.6.1"
implementation("androidx.room:room-runtime:$roomVersion")
implementation("androidx.room:room-ktx:$roomVersion")
kapt("androidx.room:room-compiler:$roomVersion")
// Retrofit
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
}
📝 提示:本教程中的天气 API 示例需要替换为你自己申请的 API Key。推荐使用和风天气(QWeather)等免费天气 API 服务。实际开发中请注意 API Key 不要硬编码在代码中,建议通过
local.properties或 BuildConfig 注入。
祝你 Android 开发学习顺利!🚀