# 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)
}
}
}
```