iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
Software Development

Android 鐵人賽: 天命最高 - 陪伴大家一步步打造屬於自己的app系列 第 9 篇

Kotlin 協程進階與檔案處理——CoroutineScope、SupervisorJob、async 與 File I/O

  • 分享至 

  • xImage
  •  

上一篇我們認識了 Kotlin Coroutine(協程)的基本概念,知道可以使用 launch 啟動協程、透過 delay() 暫停工作。今天將繼續深入協程,學習多個 CoroutineScope、SupervisorJob、async/await、掛起函式與 Dispatcher 的使用方式,最後再加入 Kotlin 檔案建立、寫入與讀取操作。Kotlin Coroutines Guide


一、今天的學習目標

今天將會學習以下內容:

  1. 使用 CoroutineScope 建立獨立的協程作用域。
  2. 使用 Dispatchers.Default 執行背景運算工作。
  3. 了解 delay() 與 Thread.sleep() 的差異。
  4. 使用 SupervisorJob 隔離不同協程的例外。
  5. 使用 async 建立有回傳值的協程。
  6. 使用 await() 取得 Deferred 的執行結果。
  7. 建立與呼叫 suspend 掛起函式。
  8. 使用 withContext() 切換協程執行環境。
  9. 使用 File、FileWriter 與 FileReader 處理文字檔案。Kotlin Coroutine Basics

第一部分:Kotlin 多工協程

二、什麼是 CoroutineScope?

CoroutineScope 稱為「協程作用域」,主要用途是管理在這個作用域中啟動的協程。當我們使用 launch 或 async 啟動協程時,該協程會歸屬於特定的作用域。CoroutineScope API

建立協程作用域的基本語法如下:

val scope = CoroutineScope(Dispatchers.Default)

Dispatchers.Default 適合執行需要 CPU 運算的背景工作,例如資料排序、資料轉換或複雜計算。它會使用共享背景執行緒池執行工作。Coroutine Dispatchers


三、使用 launch 啟動第一個協程

下面建立第一個 CoroutineScope,並使用 launch 啟動協程:

var scope1 = CoroutineScope(Dispatchers.Default)

scope1.launch {
    repeat(3) {
        println("第一個 Scope - 協程 :$it")
        delay(300)
    }
}

repeat(3) 會執行三次,變數 it 的值依序為 0、1、2。每次輸出後,使用 delay(300) 將協程暫停 300 毫秒。Kotlin repeat

delay() 只會暫停目前的協程,不會阻塞協程所在的執行緒。因此,在這段協程等待期間,相同執行緒還能繼續執行其他協程工作。Kotlin delay API


四、建立第二個獨立協程作用域

接著建立第二個獨立的協程作用域,並在其中呼叫 sFun():

val job = CoroutineScope(Dispatchers.Default)

job.launch {
    sFun()
}

雖然這個變數命名為 job,但它實際儲存的是 CoroutineScope,不是 Job 物件。為了讓程式語意更加清楚,實務上可以將它改名為 scope2。CoroutineScope API

例如:

val scope2 = CoroutineScope(Dispatchers.Default)

scope2.launch {
    sFun()
}

五、掛起函式 suspend fun

sFun() 前面使用 suspend 修飾,代表它是一個「掛起函式」:

suspend fun sFun() {
    for (i in 1..5) {
        println("${(0x41 + i).toChar()}")
        Thread.sleep(500)
    }
}

掛起函式只能在另一個掛起函式或協程內呼叫。它可以在不阻塞整個執行緒的情況下,暫停目前協程並等待工作完成。Composing Suspending Functions

程式中的 0x41 是十六進位整數,對應 Unicode 字元 A。因為迴圈從 1 開始,所以實際輸出的字元如下:

B
C
D
E
F

0x41 + 1 等於 0x42,轉換為字元後就是 B;後面的字元以此類推。Kotlin toChar

不過,掛起函式中不建議使用 Thread.sleep(500),因為它會阻塞目前執行緒。較好的方式是改成 delay(500):

suspend fun sFun() {
    for (i in 1..5) {
        println("${(0x41 + i).toChar()}")

        // 使用 delay() 只暫停目前協程,不會阻塞背景執行緒。
        delay(500)
    }
}

資料來源:Kotlin delay API


六、主執行緒與協程同時運作

主程式中還有一個一般的 for 迴圈:

for (count in 1..10) {
    println("count = $count")
    Thread.sleep(400)
}

這段程式執行於主執行緒。每輸出一次 count,就使用 Thread.sleep(400) 阻塞主執行緒 400 毫秒。Java Thread.sleep

因為前面的協程使用 Dispatchers.Default 在背景執行緒池中工作,所以主執行緒、第一個協程及第二個協程可能交錯輸出:

count = 1
第一個 Scope - 協程 :0
B
第一個 Scope - 協程 :1
count = 2
C
第一個 Scope - 協程 :2
count = 3
D
count = 4
E
count = 5
F
count = 6
count = 7
count = 8
count = 9
count = 10

因為協程由執行緒排程器安排工作,每次執行的時間點可能略有不同,所以實際輸出順序不一定完全相同。Coroutine Context and Dispatchers


七、使用 SupervisorJob 隔離協程例外

接下來重新建立 scope1,並加入 SupervisorJob():

scope1 = CoroutineScope(
    Dispatchers.Default + SupervisorJob()
)

一般情況下,父協程或其中一個子協程發生例外時,可能會取消同一工作階層中的其他協程。SupervisorJob 的作用是隔離子協程的失敗,讓其中一個子協程失敗時,不會自動取消其他同層子協程。SupervisorJob API

第一個工作故意產生例外:

scope1.launch {
    delay(100)
    println("Job 1")
    error("job 1 fail")
}

error("job 1 fail") 會拋出 IllegalStateException,因此執行時會看到錯誤堆疊資訊。Kotlin error

第二個工作則正常執行:

scope1.launch {
    delay(200)
    println("job 2 is ok")
}

即使第一個協程發生例外,因為作用域中使用了 SupervisorJob,第二個協程仍然可以繼續執行並輸出:

Job 1
Exception in thread "DefaultDispatcher-worker-1"
java.lang.IllegalStateException: job 1 fail

job 2 is ok

這就是 SupervisorJob 最重要的功能:某個子工作失敗時,其他獨立工作仍能繼續執行。Kotlin Exception Handling


八、launch 與 async 的差異

launch 適合執行不需要回傳結果的工作,它會回傳一個 Job 物件;async 適合執行需要回傳結果的工作,它會回傳 Deferred<T> 物件。Composing Suspending Functions

建立方式 回傳型別 適合用途
launch Job 執行不需要回傳值的工作
async Deferred<T> 執行需要回傳結果的工作
await() T 等待並取得 async 的結果

資料來源:Kotlin async API


九、使用 async 與 await 取得結果

程式使用 runBlocking 建立一個協程作用域:

runBlocking {
    val value1 = async {
        delay(100)
        println("async job 1")
        100
    }

    val value2 = async {
        delay(200)
        println("async job 2")
        200
    }

    println("waiting 3")
    println("value1 = ${value1.await()}, value2 = ${value2.await()}")
    println("sum = ${value1.await() + value2.await()}")
}

runBlocking 會阻塞目前的執行緒,直到區塊中的所有協程完成,因此常用於一般 Kotlin 主程式、測試程式或教學範例。runBlocking API

第一個 async 等待 100 毫秒後回傳 100;第二個 async 等待 200 毫秒後回傳 200:

val value1: Deferred<Int> = async {
    delay(100)
    100
}

val value2: Deferred<Int> = async {
    delay(200)
    200
}

此時 value1 與 value2 並不是整數,而是包裝非同步結果的 Deferred<Int>。必須呼叫 await() 才能等待並取得真正的數字。Deferred API

val result1 = value1.await()
val result2 = value2.await()

println("result1 = $result1")
println("result2 = $result2")
println("sum = ${result1 + result2}")

輸出結果如下:

waiting 3
async job 1
async job 2
value1 = 100, value2 = 200
sum = 300

資料來源:Kotlin await API


十、為什麼不能只呼叫 toString()?

原本的範例使用:

println(
    "value1 = ${value1.toString()}, value2 = ${value2.toString()}"
)

這樣輸出的不是 100 與 200,而可能是:

value1 = DeferredCoroutine{Completed},
value2 = DeferredCoroutine{Completed}

原因是 toString() 只會把 Deferred 物件本身轉成文字,顯示它的狀態;它不會取得協程內部的運算結果。Deferred API

正確做法是使用:

println(
    "value1 = ${value1.await()}, value2 = ${value2.await()}"
)

資料來源:Kotlin await API


十一、使用 withContext 切換 Dispatcher

程式中另外定義了 doEvent() 與 doJob():

suspend fun doEvent() {
    withContext(Dispatchers.Default) {
        println("doEvent")
        doJob()
        delay(300)
        println("event end")
    }
}

withContext() 可以切換協程的執行環境,並等待區塊完成後再回到原來的協程環境。withContext API

doEvent() 使用 Dispatchers.Default,適合一般背景計算;它呼叫的 doJob() 則切換到 Dispatchers.IO:

suspend fun doJob() {
    withContext(Dispatchers.IO) {
        println("doJob")
        delay(100)
        println("job finished")
    }
}

Dispatchers.IO 適合檔案讀寫、網路存取及資料庫操作等可能需要等待輸入輸出的工作。Dispatchers.IO API

如果在 main() 中加入以下程式:

runBlocking {
    doEvent()
}

執行順序會是:

doEvent
doJob
job finished
event end

因為 doEvent() 必須等待 doJob() 完成,才能繼續執行後面的 delay(300) 與 println("event end")。withContext API


十二、MUTI_Task_2.kt 完整程式碼

以下是本次協程範例的完整程式碼:

import kotlinx.coroutines.*

fun main() {

    // =================================================================
    // 1. 建立第一個獨立的協程作用域(變數名稱為 scope1)
    // =================================================================
    var scope1 = CoroutineScope(Dispatchers.Default)

    // 在 scope1 中啟動第一個協程。
    scope1.launch {
        repeat(3) {
            println("第一個 Scope - 協程 :$it")

            // 暫停目前協程 300 毫秒,不阻塞背景執行緒。
            delay(300)
        }
    }

    // =================================================================
    // 2. 建立第二個獨立的協程作用域
    // =================================================================
    val job = CoroutineScope(Dispatchers.Default)

    job.launch {
        // 呼叫下方定義的掛起函式。
        sFun()
    }

    // =================================================================
    // 3. 主執行緒迴圈
    // =================================================================
    for (count in 1..10) {
        println("count = $count")

        // 阻塞主執行緒 400 毫秒。
        Thread.sleep(400)
    }

    // =================================================================
    // 4. 使用 SupervisorJob 隔離不同子協程的例外
    // =================================================================
    scope1 = CoroutineScope(
        Dispatchers.Default + SupervisorJob()
    )

    // 啟動工作 1,並故意引發例外。
    scope1.launch {
        delay(100)
        println("Job 1")

        // 故意拋出 IllegalStateException。
        error("job 1 fail")
    }

    // 啟動工作 2。
    // 因為使用 SupervisorJob,所以不會被工作 1 的失敗取消。
    scope1.launch {
        delay(200)
        println("job 2 is ok")
    }

    // 暫停主執行緒,等待上面的背景協程執行。
    Thread.sleep(1000)
    println("waiting 2")
    println()

    // =================================================================
    // 5. 使用 runBlocking、async 與 await
    // =================================================================
    runBlocking {

        // async 會回傳 Deferred<Int>。
        val value1 = async {
            delay(100)
            println("async job 1")

            // 此區塊的回傳結果為 100。
            100
        }

        // 第二個 async 同樣回傳 Deferred<Int>。
        val value2 = async {
            delay(200)
            println("async job 2")

            // 此區塊的回傳結果為 200。
            200
        }

        println("waiting 3")

        // 使用 await() 等待並取得真正的運算結果。
        val result1 = value1.await()
        val result2 = value2.await()

        println("value1 = $result1, value2 = $result2")
        println("sum = ${result1 + result2}")
        println()
    }

    println()

    // 暫停主執行緒,確認背景工作有足夠時間完成。
    Thread.sleep(1000)
    println("waiting 3")
}

/**
 * 自訂掛起函式。
 *
 * 使用 suspend 修飾後,只能從協程或其他掛起函式中呼叫。
 */
suspend fun sFun() {
    for (i in 1..5) {

        // 0x41 是字元 A。
        // 加上 1~5 後,依序輸出 B、C、D、E、F。
        println("${(0x41 + i).toChar()}")

        // 建議在掛起函式中使用 delay(),
        // 避免 Thread.sleep() 阻塞整個背景執行緒。
        delay(500)
    }
}

/**
 * 示範使用 Dispatchers.Default 執行一般背景工作。
 */
suspend fun doEvent() {
    withContext(Dispatchers.Default) {
        println("doEvent")

        // 呼叫另一個掛起函式,並等待它執行完成。
        doJob()

        delay(300)
        println("event end")
    }
}

/**
 * 示範使用 Dispatchers.IO 執行輸入輸出工作。
 */
suspend fun doJob() {
    withContext(Dispatchers.IO) {
        println("doJob")
        delay(100)
        println("job finished")
    }
}

資料來源:kotlinx.coroutines API


第二部分:Kotlin 檔案處理

十三、取得目前執行路徑

Kotlin 可以使用 Java 的 File 類別處理檔案與資料夾:

val dir = File("").absolutePath
println("current path: $dir")

File("") 代表目前的工作目錄,absolutePath 會取得它的絕對路徑。Java File API


十四、建立資料夾

以下程式會在 Windows 的 C:\ 建立 kotlin 資料夾:

val newDir = File("c:\\kotlin")
val result = newDir.mkdir()

println("result = $result")

因為 Kotlin 字串中的反斜線 \ 是跳脫字元,所以 Windows 路徑必須寫成 c:\\kotlin。mkdir() 成功建立資料夾時回傳 true;如果資料夾已存在或建立失敗,則回傳 false。Java File.mkdir

也可以使用原始字串,讓 Windows 路徑更容易閱讀:

val newDir = File("""C:\kotlin""")

Kotlin 的三個雙引號可以建立原始字串,裡面的反斜線不需要再次跳脫。Kotlin Strings


十五、建立與寫入文字檔案

使用 File.apply 可以在建立物件後,連續執行多個檔案操作:

val file = File("c:\\kotlin\\data1.txt").apply {
    createNewFile()
    writeText("Hello Mary \n")
    appendText("Good morning !\n")
    appendText("This is kotlin class. \n")
}

各個函式的用途如下:

函式 用途
createNewFile() 建立新檔案
writeText() 寫入文字,原有內容會被覆蓋
appendText() 將文字附加在檔案末端
readText() 一次讀取整個文字檔案
readLines() 將每一行讀成字串串列

資料來源:Kotlin File Extensions


十六、一次讀取整個檔案

使用 readText() 可以一次取得檔案內的全部文字:

val data = file.readText()
println(data)

輸出如下:

Hello Mary
Good morning !
This is kotlin class.

readText() 適合讀取容量較小的文字檔案。如果檔案非常大,應改用緩衝串流或逐行讀取,以免一次占用大量記憶體。Kotlin readText


十七、逐行讀取檔案

使用 readLines() 可以將檔案內容轉成 List<String>:

val lines = file.readLines()

lines.forEach { line ->
    println("data = $line")
}

輸出如下:

data = Hello Mary
data = Good morning !
data = This is kotlin class.

資料來源:Kotlin readLines


十八、使用 FileWriter 寫入檔案

除了 Kotlin 提供的 writeText(),也可以使用 Java 的 FileWriter:

val newFile = FileWriter(
    "c:\\kotlin\\data2.txt",
    false
)

第二個參數表示是否採用附加模式:

參數 寫入方式
false 覆蓋原有內容
true 將內容附加在檔案末端

資料來源:Java FileWriter API


十九、使用 use 自動關閉檔案

檔案寫入完成後必須關閉 FileWriter,否則可能造成資料尚未完整寫入,或檔案持續被程式鎖定。Kotlin 可以使用 use 自動關閉資源:

newFile.use {
    it.write(0x31)
    it.write(0x38)
    it.write(0x41)
    it.write("\n")

    val data = charArrayOf(
        'H', 'e', 'l', 'l', 'o', '\n'
    )

    it.write(data)
    it.write("It is sunny day\n")
}

use 的概念與 Java 的 try-with-resources 類似。不論區塊正常完成或發生例外,都會自動呼叫資源的 close()。Kotlin use

其中:

it.write(0x31)
it.write(0x38)
it.write(0x41)

寫入的是 Unicode 字元值:

十六進位 十進位 字元
0x31 49 1
0x38 56 8
0x41 65 A

資料來源:Unicode Latin Basic

因此,data2.txt 的內容會是:

18A
Hello
It is sunny day

二十、使用 FileReader 與字元緩衝區

下面建立長度為 50 的 CharArray,作為檔案讀取緩衝區:

val chBuff = CharArray(50)
var len = 0
var myFile = FileReader("c:\\kotlin\\data2.txt")

接著使用 read(chBuff) 把文字讀入字元陣列:

myFile.use {
    len = it.read(chBuff)
    println("len = $len")
}

read() 會回傳實際讀到的字元數。如果已經到達檔案結尾,則會回傳 -1。Java Reader.read


二十一、將字元陣列轉成 String

讀取完成後,可以將緩衝區中的有效範圍轉成字串:

println(String(chBuff, 0, len))

三個參數分別代表:

  1. chBuff:來源字元陣列。
  2. 0:開始位置。
  3. len:要轉換的字元數量。

資料來源:Java String Constructor

也可以使用 for 迴圈逐字輸出:

for (i in 0 until len) {
    print(chBuff[i])
}

println()

0 until len 會建立從 0 到 len - 1 的整數範圍,可避免存取到字元陣列有效內容以外的位置。Kotlin Ranges


二十二、FileControl.kt 完整程式碼

import java.io.File
import java.io.FileReader
import java.io.FileWriter

fun main() {

    // ================================================================
    // 1. 取得並輸出目前專案的執行路徑(工作目錄)
    // ================================================================
    val dir = File("").absolutePath
    println("current path: $dir")

    // ================================================================
    // 2. 在 Windows 的 C 槽建立 kotlin 資料夾
    // ================================================================
    val newDir = File("c:\\kotlin")

    // mkdir() 成功建立資料夾時回傳 true。
    // 若資料夾已存在或建立失敗,則回傳 false。
    val result = newDir.mkdir()
    println("result = $result")

    // ================================================================
    // 3. 使用 Kotlin 擴充函式操作 data1.txt
    // ================================================================
    val file = File("c:\\kotlin\\data1.txt").apply {

        // 建立新檔案。
        createNewFile()

        // 寫入文字;如果檔案原本有內容,會覆蓋舊內容。
        writeText("Hello Mary \n")

        // 將文字附加到檔案末端。
        appendText("Good morning !\n")

        // 再次附加一行文字。
        appendText("This is kotlin class. \n")
    }

    println()

    // ================================================================
    // 4. 使用 readText() 一次讀取完整內容
    // ================================================================
    val data = file.readText()
    println(data)

    println()

    // ================================================================
    // 5. 使用 readLines() 按行讀取內容
    // ================================================================
    val lines = file.readLines()

    lines.forEach { line ->
        println("data = $line")
    }

    println()

    // ================================================================
    // 6. 使用 FileWriter 寫入 data2.txt
    // ================================================================

    // FileWriter 的第二個參數代表附加模式:
    // false:覆蓋寫入。
    // true:附加寫入。
    val newFile = FileWriter(
        "c:\\kotlin\\data2.txt",
        false
    )

    // use 相當於 Java 的 try-with-resources。
    // 區塊結束後會自動關閉 FileWriter,釋放檔案資源。
    newFile.use { writer ->

        // write(Int) 會將數值當成 Unicode 字元碼寫入。
        writer.write(0x31) // 字元 1
        writer.write(0x38) // 字元 8
        writer.write(0x41) // 字元 A
        writer.write("\n") // 換行

        // 建立字元陣列。
        val charData = charArrayOf(
            'H',
            'e',
            'l',
            'l',
            'o',
            '\n'
        )

        // 寫入完整字元陣列。
        writer.write(charData)

        // 直接寫入字串。
        writer.write("It is sunny day\n")
    }

    // ================================================================
    // 7. 使用 readText() 快速讀取 data2.txt
    // ================================================================
    val readData = File(
        "c:\\kotlin\\data2.txt"
    ).readText()

    println(readData)

    // ================================================================
    // 8. 使用 FileReader 與字元陣列讀取檔案
    // ================================================================

    // 建立可存放 50 個字元的緩衝區。
    val chBuff = CharArray(50)

    // 記錄實際讀取到的字元數量。
    var len = 0

    var myFile = FileReader(
        "c:\\kotlin\\data2.txt"
    )

    myFile.use { reader ->

        // 將資料讀入 chBuff。
        // 回傳值是實際讀取到的字元數量。
        len = reader.read(chBuff)
        println("len = $len")
    }

    // ================================================================
    // 9. 輸出字元陣列的有效內容
    // ================================================================

    // 方法 A:
    // 從索引 0 開始,將 len 個字元轉換為 String。
    if (len > 0) {
        println(String(chBuff, 0, len))
    }

    println()

    // 方法 B:
    // 使用迴圈逐字輸出。
    if (len > 0) {
        for (i in 0 until len) {
            print(chBuff[i])
        }
    }

    println()

    // ================================================================
    // 10. 使用 FileReader 的 readLines() 按行讀取
    // ================================================================
    myFile = FileReader(
        "c:\\kotlin\\data2.txt"
    )

    myFile.use { reader ->

        // readLines() 是 Kotlin 為 Reader 提供的擴充函式。
        val dataList = reader.readLines()

        for (lineData in dataList) {
            println(lineData)
        }
    }
}

資料來源:Kotlin I/O API


二十三、協程搭配檔案讀寫

當檔案內容比較大,或檔案位於速度較慢的儲存裝置時,讀寫操作不應放在主執行緒執行。可以使用 withContext(Dispatchers.IO) 將檔案操作切換到 I/O 執行緒池。Dispatchers.IO API

以下是協程搭配檔案寫入的完整範例:

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.File

fun main() = runBlocking {

    // 呼叫掛起函式並等待檔案寫入完成。
    saveData()

    // 呼叫掛起函式並取得檔案內容。
    val content = readData()

    println("檔案內容:")
    println(content)
}

/**
 * 使用 Dispatchers.IO 寫入檔案。
 */
suspend fun saveData() {
    withContext(Dispatchers.IO) {

        val directory = File("c:\\kotlin")

        // 如果資料夾不存在,就建立資料夾及必要的上層目錄。
        if (!directory.exists()) {
            directory.mkdirs()
        }

        val file = File(
            directory,
            "coroutine_data.txt"
        )

        // writeText() 會覆蓋原本的檔案內容。
        file.writeText(
            "Kotlin Coroutine\n" +
                "File I/O with Dispatchers.IO\n"
        )

        println("檔案寫入完成:${file.absolutePath}")
    }
}

/**
 * 使用 Dispatchers.IO 讀取檔案。
 */
suspend fun readData(): String {
    return withContext(Dispatchers.IO) {

        val file = File(
            "c:\\kotlin\\coroutine_data.txt"
        )

        // 先判斷檔案是否存在,避免直接讀取不存在的檔案。
        if (file.exists()) {
            file.readText()
        } else {
            "找不到檔案:${file.absolutePath}"
        }
    }
}

資料來源:Coroutine Context and Dispatchers


二十四、今天的重點整理

今天的範例同時涵蓋 Kotlin 協程與檔案操作。CoroutineScope 用來管理協程生命週期;launch 適合沒有回傳值的工作;async 適合需要回傳結果的工作,而結果必須透過 await() 取得。Kotlin Coroutines Guide

SupervisorJob 可以避免單一子協程失敗時取消其他同層協程,適合多個彼此獨立的背景工作。例如 App 同時更新使用者資料、載入圖片與取得通知時,其中一個工作失敗,不一定要讓其他工作全部停止。SupervisorJob API

在掛起函式中,等待時間應優先使用 delay(),避免使用會阻塞執行緒的 Thread.sleep()。如果需要處理檔案、網路或資料庫,則可以透過 withContext(Dispatchers.IO) 切換到適合 I/O 工作的執行緒池。Kotlin delay API

檔案處理方面,Kotlin 提供 writeText()、appendText()、readText() 與 readLines() 等簡潔的擴充函式;若需要更細部地控制讀寫,也可以使用 FileWriter、FileReader 和字元緩衝區。Kotlin File API

透過今天的練習,我們已經能夠同時執行多個背景工作、隔離協程例外、取得非同步運算結果,並將資料寫入檔案。下一步就可以把這些技術運用在 Android App,例如背景下載、API 資料取得、Room 資料庫操作,以及本機檔案儲存。Android Kotlin Coroutines


上一篇
Kotlin Coroutine 入門:協程、CoroutineScope、SupervisorJob 與 async 非同步程式設計
下一篇
Android 畫面設計入門——ConstraintLayout、View Binding 與 Fragment 切換
系列文
Android 鐵人賽: 天命最高 - 陪伴大家一步步打造屬於自己的app 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言