Kotlin Android开发入门教程

教程简介

零基础Kotlin Android开发完整教程,涵盖Kotlin语法基础、Android Studio安装、Activity生命周期、UI布局、RecyclerView、网络请求、Room数据库、Jetpack组件等核心技能,配有天气查询App实战项目。

Kotlin Android 开发入门教程

本教程面向零基础读者,从 Kotlin 语法到 Android 实战项目,手把手带你入门 Android 开发。


目录

  1. Kotlin 语言基础
  2. Android Studio 安装与配置
  3. 第一个 Android 项目
  4. Activity 生命周期
  5. UI 布局系统
  6. RecyclerView 列表展示
  7. 网络请求
  8. Room 数据库
  9. Jetpack 核心组件
  10. 实战项目:天气查询 App
  11. 学习路线与资源推荐

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 安装步骤

  1. 下载 Android Studio

    • 访问 Android Studio 官网:https://developer.android.com/studio
    • 下载最新稳定版
  2. 安装 JDK

    • Android Studio 自带 JBR(JetBrains Runtime),通常无需单独安装
    • 如需手动安装:推荐 JDK 17
  3. 首次启动配置

    • 选择 Standard 安装类型
    • SDK 会自动下载到默认目录
    • 等待 Gradle 同步和 SDK 组件下载完成
  4. 配置国内镜像(可选,加速下载)

    • gradle.properties 中添加:
    # 阿里云镜像
    systemProp.https.proxyHost=mirrors.aliyun.com
    
    • 或在项目 settings.gradle.ktspluginManagement 中添加镜像仓库

2.3 创建模拟器

  1. 打开 Tools → Device Manager
  2. 点击 Create Device
  3. 选择设备型号(推荐 Pixel 6)
  4. 下载系统镜像(推荐 API 34)
  5. 完成创建后点击启动按钮运行模拟器

3. 第一个 Android 项目

3.1 创建项目

  1. 打开 Android Studio → New Project
  2. 选择 Empty Views Activity(传统 View 体系)
  3. 配置:
    • Name: MyFirstApp
    • Package name: com.example.myfirstapp
    • Language: Kotlin
    • Minimum SDK: API 24(覆盖 99%+ 设备)
  4. 点击 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 后:

  1. 在搜索框输入城市名称(如"北京"),点击搜索
  2. 从搜索结果中选择城市
  3. 查看实时天气信息(温度、天气状况、湿度、风向)
  4. 点击收藏按钮保存常查城市
  5. 搜索历史自动记录,点击可快速查看

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 — 官方视频教程
  • Codelabdeveloper.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 开发学习顺利!🚀

内容声明

本文内容为AI技术学习教程,仅供学习参考。如涉及技术问题,欢迎通过 xurj005@163.com 与我们交流。

目录