---
# System prepended metadata

title: Kotlin / Ch03 - Android APP Fundamental

---

# Kotlin / Ch03 - Android APP Fundamental

## 前言

本學習筆記將著重於 Kotlin 與 Java 的差異、Android 平台的特殊性以及現代 Android 開發的最佳實踐。對於物件導向、設計模式等內容我就不多做贅述，覺得節奏太快的好朋朋可以回頭看看 Java Fundamental 或 Spring Framework。

## 第一章：Kotlin 的特殊性

### 1.1 語法的使用

#### 1.1.1 變數宣告與型別推斷

在 Kotlin 中，變數宣告使用 `val`（不可變，相當於 Java 的 `final`）和 `var`（可變）關鍵字。Kotlin 的型別推斷系統能夠自動推導變數型別，大幅減少冗餘的程式碼：

```kotlin=
// 不可變變數（推薦使用）
val name: String = "張三"
val age = 25  // 型別自動推斷為 Int

// 可變變數
var score = 90
score = 95  // 可以重新賦值

// 延遲初始化
lateinit var database: Database
```

**應該養成優先使用 `val` 的習慣**，這與 FP 的不可變性原則一致，也能避免許多潛在的錯誤。只有在確實需要修改變數值的情況下，才使用 `var`。

#### 1.1.2 Null Safety（空值安全）

Kotlin 最重要的特性之一是內建的 null safety 機制。在 Java 中，NullPointerException 是最常見的執行時期錯誤之一，Kotlin 透過型別系統在編譯時期就能夠防止大部分的空值問題：

```kotlin=
// 不可為 null 的型別
var name: String = "李四"
// name = null  // 編譯錯誤

// 可為 null 的型別（使用 ? 標記）
var nullableName: String? = "王五"
nullableName = null  // 合法

// 安全呼叫運算子
val length = nullableName?.length  // 如果 nullableName 為 null，結果為 null

// Elvis Operator（提供預設值）
val length2 = nullableName?.length ?: 0  // 如果為 null，回傳 0

// 非空斷言運算子（要確定不為 null 時使用，謹慎考慮）
val length3 = nullableName!!.length  // 如果為 null 會拋出例外
```

這個機制強制開發者在設計階段就明確處理可能為 null 的情況，大幅提升程式碼的穩定性。對於習慣使用 Optional 類別的 Java 開發者，Kotlin 的 null safety 提供了更直觀且語法更簡潔的解決方案。

#### 1.1.3 函數宣告

Kotlin 的函數宣告語法簡潔明瞭，並且支援預設參數和具名參數，您不需要像 Java 一樣透過 overloading 來實現：

```kotlin=
// 基本函數宣告
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(name = "陳先生", greeting = "早安")
greet(name = "林小姐")  // 使用預設的 greeting

// 可變參數
fun sum(vararg numbers: Int): Int {
    return numbers.sum()
}
```

函數是 Kotlin 的一等公民（first-class citizens），可以賦值給變數、作為參數傳遞，以及作為回傳值，也稱得上是 FP 的核心精神。

### 1.2 類別與物件

#### 1.2.1 類別宣告與建構子

Kotlin 會自動處理許多常見的 boilerplate code：

```kotlin=
// 基本類別宣告（主建構子）
class Person(val name: String, var age: Int) {
    // 屬性和方法
    fun introduce() {
        println("我是 $name，今年 $age 歲")
    }
}

// 使用次要建構子
class User(val username: String) {
    var email: String = ""
    
    constructor(username: String, email: String) : this(username) {
        this.email = email
    }
}

// 初始化區塊
class Account(val id: String) {
    init {
        println("帳號 $id 已建立")
    }
}
```

在 Kotlin 中，主建構子的參數如果使用 `val` 或 `var` 宣告，就會自動成為類別的屬性，no more `getter()` & `setter()` 👍。

#### 1.2.2 Data Class（資料類別）

Data class 是 Kotlin 的一個重要特性，專門用於存放資料的類別。編譯器會自動產生 `equals()`、`hashCode()`、`toString()`、`copy()` 等方法：

```kotlin=
data class Product(
    val id: Long,
    val name: String,
    val price: Double,
    var quantity: Int
)

// 使用範例
val product = Product(1, "筆記型電腦", 35000.0, 10)
println(product)  // 自動產生可讀的輸出

// copy() 方法可以複製並修改部分屬性
val discountProduct = product.copy(price = 30000.0)

// 解構宣告
val (id, name, price, quantity) = product
```

這對於 Spring 開發者來說應該很熟悉，類似於 Lombok 的 `@Data`，但 data class 是原生的，不需要額外套件。

### 1.3 物件導向 OOP

#### 1.3.1 繼承與介面

Kotlin 中的所有類別預設都是 final（不可繼承），這是一個有意的設計，理念是「實作抽象優於繼承」這個原則。如果您想讓類別可以被繼承，需要使用 `open` 關鍵字：

```kotlin=
// 開放類別供繼承
open class Animal(val name: String) {
    open fun makeSound() {
        println("某種聲音")
    }
}

// 繼承類別
class Dog(name: String, val breed: String) : Animal(name) {
    override fun makeSound() {
        println("$name 汪汪叫")
    }
}

// 介面定義
interface Drawable {
    fun draw()
    
    // 介面可以有預設實作
    fun getDescription(): String {
        return "可繪製的物件"
    }
}

// 實作多個介面
class Circle(val radius: Double) : Drawable, Comparable<Circle> {
    override fun draw() {
        println("繪製半徑為 $radius 的圓形")
    }
    
    override fun compareTo(other: Circle): Int {
        return this.radius.compareTo(other.radius)
    }
}
```

與 Java 不同， Kotlin 需要明確聲明可繼承性，有助於避免意外的繼承錯誤。對於習慣 Spring 框架的開發者，這與避免過度繼承、偏好實作抽象介面的最佳實踐是一致的。

#### 1.3.2 密封類別（Sealed Class）

Sealed class 是 Kotlin 的一個強大特性，用於表示受限的類別階層。它特別適合用於表示狀態或結果類型，這在 Android 開發中非常常見：

```kotlin=
// 定義密封類別表示網路請求結果
sealed class Result<out T> {
    data class Success<T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
    object Loading : Result<Nothing>()
}

// 使用密封類別搭配 when 表達式
fun handleResult(result: Result<String>) {
    when (result) {
        is Result.Success -> println("成功：${result.data}")
        is Result.Error -> println("錯誤：${result.exception.message}")
        Result.Loading -> println("載入中...")
    }
    // 編譯器會確保處理所有情況，不需要 else 分支
}
```

這個模式在處理非同步操作、狀態管理時非常實用，可以替代 Java 中需要使用多個類別或枚舉的場景。

#### 1.3.3 物件宣告與伴生物件

Kotlin 提供了三種特殊的物件宣告方式：Singleton object、Companion object 和 Anonymous object：

```kotlin=
// Singleton（單例模式）
object DatabaseConfig {
    const val DB_NAME = "app_database"
    var version = 1
    
    fun getConnectionString() = "jdbc:sqlite:$DB_NAME"
}

// Companion object（伴生物件，類似 Java 的靜態成員）
class User private constructor(val id: Long, val name: String) {
    companion object {
        private var nextId = 1L
        
        // Factory method
        fun create(name: String): User {
            return User(nextId++, name)
        }
        
        const val MAX_NAME_LENGTH = 50
    }
}

// 使用方式
val user = User.create("李小明")
println(User.MAX_NAME_LENGTH)

// Anonymous object（匿名物件，類似 Java 的匿名內部類別）
val clickListener = object : View.OnClickListener {
    override fun onClick(v: View?) {
        println("按鈕被點擊")
    }
}
```

伴生物件提供了類似 Java 靜態方法的功能，但更加靈活，因為它本質上是一個物件，可以實作介面、被繼承等。

### 1.4 函數導向 FP

#### 1.4.1 Lambda 表達式與高階函數

Kotlin 對 FP 有完整的支援，Lambda 表達式的語法簡潔且強大：

```kotlin=
// Lambda 表達式基本語法
val sum = { a: Int, b: Int -> a + b }
val result = sum(3, 5)  // 8

// 高階函數（接受函數作為參數）
fun calculate(a: Int, b: Int, operation: (Int, Int) -> Int): Int {
    return operation(a, b)
}

val addResult = calculate(10, 5) { x, y -> x + y }
val multiplyResult = calculate(10, 5) { x, y -> x * y }

// 如果 Lambda 是最後一個參數，可以移到括號外
listOf(1, 2, 3, 4, 5).filter { it % 2 == 0 }

// 函數引用
fun isEven(n: Int) = n % 2 == 0
val evenNumbers = listOf(1, 2, 3, 4, 5).filter(::isEven)
```

對於習慣 Java 8+ Stream API 的開發者，對於 Kotlin 的 FP 應該是感覺跟喝水一樣自然。`it` 是單一參數 Lambda 的隱式名稱，可以讓程式碼更簡潔。

#### 1.4.2 擴充函數（Extension Function）

擴充函數是 Kotlin 的一個獨特特性，允許為現有的類別添加新方法，而無需繼承或修改原始程式碼：

```kotlin=
// 為 String 類別添加擴充函數
fun String.isValidEmail(): Boolean {
    return this.contains("@") && this.contains(".")
}

// 使用擴充函數
val email = "user@example.com"
if (email.isValidEmail()) {
    println("有效的 email 地址")
}

// 為泛型類別添加擴充函數
fun <T> List<T>.secondOrNull(): T? {
    return if (this.size >= 2) this[1] else null
}

val numbers = listOf(1, 2, 3)
val second = numbers.secondOrNull()  // 2

// 可空接收者的擴充函數
fun String?.orDefault(default: String): String {
    return this ?: default
}

val name: String? = null
println(name.orDefault("訪客"))  // 輸出：訪客
```

擴充函數在 Android 開發中廣泛使用，特別是在簡化常見操作時。例如，Android KTX（Kotlin Extensions）套件提供了大量實用的擴充函數。

> 那麼這時候您可能會問，該如何在使用擴充函數與實作抽象介面之間做選擇呢？
> - 只是補工具 / 語法糖、對既有類別做方便呼叫 => 擴充函數
> - 需要低耦合 & 高內聚的實踐 => 實作抽象介面

## 第二章：Android 平台

### 2.1 開發環境設置

#### 2.1.1 Android Studio 安裝

Android Studio 是 Google 官方提供的整合開發環境（IDE），基於 IntelliJ IDEA 建構而成。由於 Kotlin 也是 JetBrains 的產品，Android Studio 對 Kotlin 的支援非常完善，包括程式碼補全、重構、除錯等功能。

下載並安裝最新版本的 Android Studio 後，您需要安裝 Android SDK、模擬器映像檔（Emulator images）以及相關的建置工具。初次安裝時，Android Studio 會引導您完成這些設定。建議安裝至少一個最新的 Android 版本 SDK，以及一個較舊但仍被廣泛使用的版本（例如 Android 8.0 或更高）以確保相容性測試。

#### 2.1.2 建立第一個 Kotlin 專案

在 Android Studio 中建立新專案時，選擇「Empty Views Activity」範本，並確保選擇 Kotlin 作為程式語言。專案建立後，您會看到一個標準的 Android 專案結構，主要包含：
- `app/src/main/java`（存放 Kotlin 原始碼）
- `app/src/main/res`（存放資源檔案）
- `app/build.gradle`（專案建置設定檔）

與 Maven 或 Gradle 在 Spring 專案中的角色類似，Android 專案使用 Gradle 作為建置系統。您會看到兩個 `build.gradle` 檔案：

- 一個在專案根目錄（project-level）
- 一個在 `app` 模組中（module-level）

大部分的相依性管理和建置設定會在 module-level 的`build.gradle` 中進行。

### 2.2 Android 應用程式架構

#### 2.2.1 應用程式組件

Android 應用程式由四種主要組件構成，每種組件都有特定的用途和生命週期：

- **Activity（活動）**：應用程式中單一畫面的表示，相當於使用者介面的容器。每個 Activity 都是一個獨立的入口點，使用者可以透過它與應用程式互動。例如，電子郵件應用程式中可能有一個 Activity 顯示新郵件清單，另一個 Activity 用於撰寫郵件，還有一個 Activity 用於閱讀郵件。

- **Service（服務）**：在背景執行的組件，用於執行長時間執行的操作或遠端處理作業，無需使用者介面。例如，音樂播放器可以在背景播放音樂，即使使用者切換到其他應用程式也能繼續播放。

- **Broadcast Receiver（廣播接收器）**：用於接收系統或應用程式發出的廣播訊息。例如，當裝置電量低、網路連線狀態變更或收到簡訊時，系統會發出廣播，應用程式可以透過 Broadcast Receiver 接收這些事件並做出回應。

- **Content Provider（內容提供者）**：用於管理和分享應用程式資料。它提供了一個標準介面，讓其他應用程式可以查詢或修改資料（需要設定適當權限）。例如，聯絡人資料就是透過 Content Provider 提供給其他應用程式使用。

#### 2.2.2 應用程式清單檔（AndroidManifest.xml）

每個 Android 應用程式都必須在根目錄包含一個 AndroidManifest.xml 檔案，這個檔案向 Android 系統描述應用程式的基本資訊：

```xml=
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.myapp">

    <!-- 權限宣告 -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

    <application
        android:name=".MyApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.MyApp">
        
        <!-- Activity 宣告 -->
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        
        <!-- Service 宣告 -->
        <service
            android:name=".MusicService"
            android:exported="false" />
    </application>
</manifest>
```

這個檔案類似於 Spring Boot 的 `application.properties` 或 `web.xml`，但功能更廣泛，包含了應用程式的所有組件宣告、權限需求、最低 SDK 版本等資訊。

### 2.3 Activity 與生命週期

#### 2.3.1 Activity 生命週期

Activity 的生命週期是 Android 開發中最重要的概念之一。理解生命週期對於正確管理資源、儲存狀態和避免記憶體洩漏至關重要：

```mermaid
graph TD
    A[Activity 啟動] --> B[onCreate]
    B --> C[onStart]
    C --> D[onResume]
    D --> E{Activity 執行中}
    E --> F[onPause]
    F --> G{使用者返回?}
    G -->|是| D
    G -->|否| H[onStop]
    H --> I{重新啟動?}
    I -->|是| J[onRestart]
    J --> C
    I -->|否| K[onDestroy]
    K --> L[Activity 結束]
```

每個生命週期方法都有特定的用途：

- **`onCreate()`**：在 Activity 第一次建立時呼叫，這是進行初始化工作的地方。**您應該在這裡設定 UI、綁定資料、初始化 ViewModel 等。** 這個方法只會在 Activity 的整個生命週期中呼叫一次。
- **`onStart()`**：當 Activity 變為使用者可見時呼叫。在這個階段，Activity 準備進入前景並變為可互動狀態。
- **`onResume()`**：當 Activity 開始與使用者互動時呼叫。在這個狀態下，Activity 位於 Activity 堆疊的頂部，正在接收使用者輸入。
- **`onPause()`**：當系統準備啟動或恢復另一個 Activity 時呼叫。這是保存資料、停止動畫或暫停耗費 CPU 資源操作的好時機。這個方法執行必須非常快速，因為下一個 Activity 要等到這個方法執行完畢才會繼續。
- **`onStop()`**：當 Activity 對使用者不再可見時呼叫。這可能是因為 Activity 正在被銷毀、新的 Activity 正在啟動，或者現有 Activity 正在進入恢復狀態並覆蓋這個 Activity。
- **`onDestroy()`** 在 Activity 被銷毀前呼叫。這是釋放資源的最後機會。

#### 2.3.2 實作 Activity

以下是一個典型的 Activity 實作範例：

```kotlin=
class MainActivity : AppCompatActivity() {
    
    private lateinit var binding: ActivityMainBinding
    private val viewModel: MainViewModel by viewModels()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        // 使用 ViewBinding
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        setupUI()
        observeViewModel()
        
        // 恢復儲存的狀態
        savedInstanceState?.let {
            val savedText = it.getString("user_input")
            binding.editText.setText(savedText)
        }
    }
    
    private fun setupUI() {
        binding.button.setOnClickListener {
            val input = binding.editText.text.toString()
            viewModel.processInput(input)
        }
    }
    
    private fun observeViewModel() {
        viewModel.result.observe(this) { result ->
            when (result) {
                is Result.Success -> showSuccess(result.data)
                is Result.Error -> showError(result.exception.message)
                Result.Loading -> showLoading()
            }
        }
    }
    
    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        // 儲存狀態以應對配置變更（如螢幕旋轉）
        outState.putString("user_input", binding.editText.text.toString())
    }
    
    override fun onPause() {
        super.onPause()
        // 暫停動畫或其他活動
    }
    
    override fun onDestroy() {
        super.onDestroy()
        // 清理資源
    }
}
```

對於 Spring 開發者，Activity 類似於 Bean的生命週期，但更複雜，因為需要處理使用者互動和系統資源限制。

### 2.4 Intent 與 Activity 導航

#### 2.4.1 Intent 介紹

Intent 是 Android 組件之間通訊的訊息物件。它可以用來啟動 Activity、啟動 Service 或發送 Broadcast：

```kotlin=
// Explicit Intent（指定目標組件）
val intent = Intent(this, DetailActivity::class.java)
intent.putExtra("user_id", userId)
intent.putExtra("user_name", userName)
startActivity(intent)

// 在目標 Activity 中接收資料
class DetailActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        val userId = intent.getLongExtra("user_id", -1)
        val userName = intent.getStringExtra("user_name")
    }
}

// Implicit Intent（基於動作和資料類型）
val phoneIntent = Intent(Intent.ACTION_DIAL).apply {
    data = Uri.parse("tel:0912345678")
}
startActivity(phoneIntent)

// 啟動 Activity 並等待結果（Activity Result API）
private val startForResult = registerForActivityResult(
    ActivityResultContracts.StartActivityForResult()
) { result ->
    if (result.resultCode == Activity.RESULT_OK) {
        val data = result.data?.getStringExtra("result")
        // 處理回傳的資料
    }
}

fun openCamera() {
    val intent = Intent(this, CameraActivity::class.java)
    startForResult.launch(intent)
}
```

> 更完整的資訊可以參考官方文件：
>https://developer.android.com/guide/components/intents-filters?hl=en

#### 2.4.2 Navigation Component

對於複雜的應用程式導航，Google 推薦使用 Navigation Component，它提供了統一的導航架構：

```kotlin
// 在 build.gradle 中添加依賴
dependencies {
    implementation("androidx.navigation:navigation-fragment-ktx:2.7.0")
    implementation("androidx.navigation:navigation-ui-ktx:2.7.0")
}
```
```kotlin=
// 導航圖定義（res/navigation/nav_graph.xml）
// 在程式碼中使用導航
class HomeFragment : Fragment() {
    private fun navigateToDetail(userId: Long) {
        val action = HomeFragmentDirections
            .actionHomeToDetail(userId)
        findNavController().navigate(action)
    }
    
    private fun navigateWithArgs() {
        val bundle = Bundle().apply {
            putLong("user_id", 123)
            putString("user_name", "張三")
        }
        findNavController().navigate(R.id.detailFragment, bundle)
    }
}
```

Navigation Component 提供了型別安全的參數傳遞、深層連結支援、以及與 ViewModel 的良好整合，是現代 Android 開發的標準導航解決方案。

### 2.5 Fragment

#### 2.5.1 Fragment 介紹

Fragment 代表 Activity 中的可重複使用的部分，擁有自己的生命週期、接收自己的輸入事件，並且可以在 Activity 執行時新增或移除：

```kotlin=
class UserListFragment : Fragment() {
    
    private var _binding: FragmentUserListBinding? = null
    private val binding get() = _binding!!
    
    private val viewModel: UserListViewModel by viewModels()
    
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentUserListBinding.inflate(inflater, container, false)
        return binding.root
    }
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        setupRecyclerView()
        observeViewModel()
    }
    
    private fun setupRecyclerView() {
        binding.recyclerView.apply {
            layoutManager = LinearLayoutManager(context)
            adapter = UserAdapter { user ->
                navigateToDetail(user.id)
            }
        }
    }
    
    private fun observeViewModel() {
        viewModel.users.observe(viewLifecycleOwner) { users ->
            (binding.recyclerView.adapter as UserAdapter).submitList(users)
        }
    }
    
    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null  // 避免記憶體洩漏
    }
}
```

Fragment 的生命週期比 Activity 更複雜，因為它依賴於宿主 Activity 的生命週期。重要的是要在 `onDestroyView()` 中清理 view binding，避免記憶體洩漏。

#### 2.5.2 Fragment 通訊

**Fragment 之間不應該直接通訊**，而應該透過共享的 ViewModel 或透過 Activity 作為媒介：

```kotlin=
// 使用共享 ViewModel
class SharedViewModel : ViewModel() {
    private val _selectedUser = MutableLiveData<User>()
    val selectedUser: LiveData<User> = _selectedUser
    
    fun selectUser(user: User) {
        _selectedUser.value = user
    }
}

// Fragment A
class ListFragment : Fragment() {
    private val sharedViewModel: SharedViewModel by activityViewModels()
    
    private fun onUserClick(user: User) {
        sharedViewModel.selectUser(user)
    }
}

// Fragment B
class DetailFragment : Fragment() {
    private val sharedViewModel: SharedViewModel by activityViewModels()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        sharedViewModel.selectedUser.observe(viewLifecycleOwner) { user ->
            displayUserDetails(user)
        }
    }
}
```