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