Kotlin語言(十三):Flow

注:本文中使用 runBlocking 是為了方便測試,業務開發中禁止使用

一、Flow 的基本使用

1、Sequence 序列生成器

(1)取出序列生成器中的值,需要迭代序列生成器;
(2)是同步調用,是阻塞的,無法調用其它的掛起函數。

fun sequenceFun() {
    val sequence = sequence<Int> {
        Thread.sleep(1000)
        yield(1)
        Thread.sleep(1000)
        yield(2)
        Thread.sleep(1000)
        yield(3)
    }
    sequence.forEach {
        println(it)
    }
    println("Done!")

    // 1
    // 2
    // 3
    // Done!
}

2、Flow 的簡單使用

(1)flow{ ... } 內部可以調用 suspend 函數;
(2)使用 emit() 方法來發射數據;
(3)使用 collect() 方法來收集結果。

fun flowFun() = runBlocking {
    val flow = flow {
        delay(1000)
        emit(1)
        delay(1000)
        emit(2)
        delay(1000)
        emit(3)
    }
    flow.collect {
        println(it)
    }
    println("Done!")

    // 1
    // 2
    // 3
    // Done!
}

3、創建 Flow 的常用方式

(1)flow{...} 需要顯示調用 emit() 發射數據;
(2)flowOf() 一個發射固定值集的流, 不需要顯示調用 emit() 發射數據;
(3)asFlow() 擴展函數,可以將各種集合與序列轉換為流,也不需要顯示調用 emit() 發射數據。

fun createFlowFun() = runBlocking {
    val flow1 = flow {
        delay(1000)
        emit(1)
    }
    val flow2 = flowOf(2, 3).onEach {
        delay(1000)
    }
    val flow3 = listOf(4, 5).asFlow().onEach {
        delay(1000)
    }
    flow1.collect {
        println(it)
    }
    flow2.collect {
        println(it)
    }
    flow3.collect {
        println(it)
    }
    println("Done!")

    // 1
    // 2
    // 3
    // 4
    // 5
    // Done!
}

4、Flow 是冷流(惰性的)

如同 Sequences 一樣, Flow 也是惰性的,即在調用末端流操作符( collect 是其中之一)之前,flow{ ... } 中的代碼不會執行。我們稱之為 -- 冷流

fun coldFlowFun() = runBlocking {
    val flow = flowOf(1, 2, 3)
        .onEach {
            delay(1000)
        }
    println("calling collect...")
    flow.collect {
        println(it)
    }
    println("calling collect again...")
    flow.collect {
        println(it)
    }

    // calling collect...
    // 1
    // 2
    // 3
    // calling collect again...
    // 1
    // 2
    // 3
}

5、Flow 的取消

流采用了與協程同樣的協助取消,取消 Flow 只需要取消它所在的 協程 即可。

fun cancelFlowFun() = runBlocking {
    val flow = flow {
        for (i in 1..3) {
            delay(100)
            println("Emitting $i")
            emit(i)
        }
    }
    withTimeoutOrNull(250) {
        flow.collect {
            println(it)
        }
    }
    println("Done!")

    // Emitting 1
    // 1
    // Emitting 2
    // 2
    // Done!
}


二、Flow 的常用操作符

1、末端流操作符 collect 、reduce 、fold、toxxx 等

fun terminalFlowOptFun() = runBlocking {
    val flow = (1..3).asFlow().onEach { delay(200) }
    flow.collect { println(it) }
    // 1
    // 2
    // 3

    val reduceSum = flow.reduce { a, b -> a + b }
    println("reduce: sum = $reduceSum")
    // reduce: sum = 6

    val foldSum = flow.fold(100) { a, b -> a + b }
    println("fold: sum = $foldSum")
    // fold: sum = 106

    val list = flow.toList()
    val set = flow.toSet()
    println("list: $list")
    println("set: $set")
    // list: [1, 2, 3]
    // set: [1, 2, 3]

    val flow2 = flowOf("one", "two").onEach { delay(200) }
    flow.onEach { println(it) }.launchIn(this)
    flow2.onEach { println(it) }.launchIn(this)
    // 1
    // one
    // 2
    // two
    // 3
}

2、流啟動時 onStart

fun startFlowFun() = runBlocking {
    (1..3).asFlow()
        .onEach { delay(1000) }
        .onStart { println("onStart") }
        .collect { println(it) }

    // onStart
    // 1
    // 2
    // 3
}

3、流完成時 onCompletion

(1)使用 try ... finally 實現;
(2)通過 onCompletion 函數實現。

fun completionFlowFun() = runBlocking {
    try {
        flow {
            for (i in 1..3) {
                delay(1000)
                emit(i)
            }
        }.collect {
            println(it)
        }
    } finally {
        println("Done!")
    }
    // 1
    // 2
    // 3
    // Done!

    flow {
        for (i in 1..3) {
            delay(1000)
            emit(i)
        }
    }.onCompletion { println("Done!") }
        .collect { println(it) }
    // 1
    // 2
    // 3
    // Done!
}

4、背壓 Backpressure

Backpressure 是響應式編程的功能之一,Flow 的 Backpressure 是通過 suspend 函數實現的。
(1)buffer 緩沖(這里要注意的是,buffer 的容量是從 0 開始計算的
?? - SUSPEND 設置緩沖區,如果溢出了,則將當前協程掛起,直到有消費了緩沖區中的數據;
?? - DROP_LATEST 設置緩沖區,如果溢出了,丟棄最新的數據;
?? - DROP_OLDEST 設置緩沖區,如果溢出了,丟棄最老的數據。
(2)conflate 合并
?? - 不設緩沖區,也就是緩沖區大小為 0,采取 DROP_OLDEST 策略,等價于 buffer(0, BufferOverflow.DROP_OLDEST) 。

fun bufferFlowFun() = runBlocking {
    val cosTime = measureTimeMillis {
        (1..5).asFlow()
            .onEach {
                delay(100)
                println("produce data: $it")
            }
            .buffer(1, BufferOverflow.SUSPEND)
            .collect {
                delay(500)
                println("collect: $it")
            }
    }
    println("cosTime: $cosTime")

    // produce data: 1
    // produce data: 2
    // produce data: 3
    // collect: 1
    // produce data: 4
    // collect: 2
    // produce data: 5
    // collect: 3
    // collect: 4
    // collect: 5
    // cosTime: 2742
}
fun conflateFlowFun() = runBlocking {
    val cosTime = measureTimeMillis {
        (1..5).asFlow()
            .onEach {
                delay(100)
                println("produce data: $it")
            }
            .conflate()
            .collect {
                delay(500)
                println("collect: $it")
            }
    }
    println("cosTime: $cosTime")

    // produce data: 1
    // produce data: 2
    // produce data: 3
    // produce data: 4
    // produce data: 5
    // collect: 1
    // collect: 5
    // cosTime: 1223
}

5、Flow 異常處理 catch、retry、retryWhen

(1)catch 操作符捕獲上游異常
?? - onCompletion 用來處理 Flow 是否收集完成,即使是遇到異常也會執行;
?? - onCompletion 有一個參數可以用來判斷上游是否出現異常;上游出現異常,不為 null,未出現異常,則為 null;
?? - onCompletion 只能判斷是否出現了異常,并不能捕獲異常;
?? - 捕獲異常使用 catch 操作符;
?? - 如果把 onCompletion 和 catch 交換一下位置,則 catch 操作捕獲到異常之后,不會再影響下游;
?? - catch 操作符用于實現異常透明化處理, catch 只是中間操作符不能捕獲下游的異常;
?? - catch 操作符內,可以使用 throw 再次拋出異常、可以使用 emit() 轉換為發射值、可以用于打印或者其他業務邏輯的處理等等。
(2)retryretryWhen 操作符重試
?? - 如果上游遇到了異常,并且 retry 方法返回 true 則會進行重試,最多重試 retries 指定的次數;
?? - retry 最終調用的是 retryWhen 操作符。

fun catchFlowFun() = runBlocking {
    (1..5).asFlow()
        .onEach {
            if (it == 4) {
                throw Exception("test exception")
            }
            delay(100)
            println("produce data: $it")
        }
        /*.catch { e ->
            println("catch exception: $e")
        }*/
        .onCompletion { e ->
            if (null == e) {
                println("onCompletion")
            } else {
                println("onCompletion: $e")
            }
        }
        .catch { e ->
            println("catch exception: $e")
        }
        .collect {
            println("collect: $it")
        }

    // produce data: 1
    // collect: 1
    // produce data: 2
    // collect: 2
    // produce data: 3
    // collect: 3
    // onCompletion: java.lang.Exception: test exception
    // catch exception: java.lang.Exception: test exception
}
fun retryFlowFun() = runBlocking {
    (1..5).asFlow()
        .onEach {
            if (it == 2) {
                throw Exception("test exception")
            }
            delay(100)
            println("produce data: $it")
        }
        .retry(1) {
            it.message == "test exception"
        }
        /*.retryWhen { cause, attempt ->
            cause.message == "test exception" && attempt < 1
        }*/
        .catch { ex ->
            println("catch exception: ${ex.message}")
        }
        .collect {
            println("collect: $it")
        }

    // produce data: 1
    // collect: 1
    // produce data: 1
    // collect: 1
    // catch exception: test exception
}

6、Flow 線程切換 flowOn

(1)響應線程是由 CoroutineContext 決定的,比如,在 Main 線程中執行 collect, 那么響應線程就是 Dispatchers.Main;
(2)Flow 通過 flowOn 方法來切換線程,多次調用,都會影響到它上游的代碼。

fun switchThreadFlowFun() = runBlocking {
    val myDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
    (1..2).asFlow()
        .onEach {
            printlnWithThread("produce data: $it")
        }
        .flowOn(Dispatchers.IO)
        .onEach {
            printlnWithThread("IO data: $it")
        }
        .flowOn(myDispatcher)
        .onEach {
            printlnWithThread("my data: $it")
        }
        .flowOn(Dispatchers.Default)
        .onCompletion {
            myDispatcher.close()
        }
        .collect {
            printlnWithThread("collect: $it")
        }

    // Thread -> id: 12, name: DefaultDispatcher-worker-2, produce data: 1
    // Thread -> id: 12, name: DefaultDispatcher-worker-2, produce data: 2
    // Thread -> id: 13, name: pool-1-thread-1, IO data: 1
    // Thread -> id: 13, name: pool-1-thread-1, IO data: 2
    // Thread -> id: 11, name: DefaultDispatcher-worker-1, my data: 1
    // Thread -> id: 11, name: DefaultDispatcher-worker-1, my data: 2
    // Thread -> id: 1, name: main, collect: 1
    // Thread -> id: 1, name: main, collect: 2
}

7、Flow 的中間轉換操作符

(1)map 操作符用于將流中的每個元素進行轉換后再發射出來

fun mapFlowFun() = runBlocking {
    (1..2).asFlow()
        .map {
            "map -> $it"
        }
        .collect {
            println(it)
        }

    // map -> 1
    // map -> 2
}

(2)transform 操作符,可以任意多次調用 emit ,這是 transform 跟 map 最大的區別

fun transformFlowFun() = runBlocking {
    (1..2).asFlow()
        .transform {
            emit("transform1 -> $it")
            delay(100)
            emit("transform2 -> $it")
        }
        .collect {
            println(it)
        }

    // transform1 -> 1
    // transform2 -> 1
    // transform1 -> 2
    // transform2 -> 2
}

(3)onEach 遍歷

fun onEachFlowFun() = runBlocking {
    (1..3).asFlow()
        .onEach { println("onEach: $it") }
        .collect { println(it) }

    // onEach: 1
    // 1
    // onEach: 2
    // 2
    // onEach: 3
    // 3
}

(4)filter 條件過濾

fun filterFlowFun() = runBlocking {
    (1..5).asFlow()
        .filter { it % 2 == 0 }
        .collect { println(it) }

    // 2
    // 4
}

(5)drop 過濾掉 前 N 個 元素

fun dropFlowFun() = runBlocking {
    (1..5).asFlow()
        .drop(3)
        .collect { println(it) }

    // 4
    // 5
}

(6)dropWhile 過濾 滿足條件前 N 個 元素,一旦條件不滿足則不再過濾后續元素

fun dropWhileFlowFun() = runBlocking {
    listOf(1, 3, 4, 2, 5).asFlow()
        .dropWhile { it < 4 }
        .collect { println(it) }
    // 4
    // 2
    // 5

    listOf(1, 3, 4, 2, 5).asFlow()
        .dropWhile { it % 2 == 1 }
        .collect { println(it) }
    // 4
    // 2
    // 5
}

(7)take 只取 前 N 個 emit 發射的值

fun takeFlowFun() = runBlocking {
    (1..5).asFlow()
        .take(2)
        .collect { println(it) }

    // 1
    // 2
}

(8)takeWhile 只取 滿足條件前 N 個 元素,一旦條件不滿足則不再獲取后續元素

fun takeWhileFlowFun() = runBlocking {
    (5 downTo 1).asFlow()
        .takeWhile { it > 3 }
        .collect { println(it) }

    // 5
    // 4

    listOf(5, 2, 4, 1).asFlow()
        .takeWhile { it > 3 }
        .collect { println(it) }

    // 5
}

(9)zip 是可以將2個 flow 進行合并的操作符
?? - 即使 flowB 中的每一個 item 都使用了 delay() 函數,在合并過程中也會等待 delay() 執行完后再進行合并;
?? - 如果 flowA 和 flowB 中 item 個數不一致,則合并后新的 flow item 個數,等于較小的 item 個數。

fun zipFlowFun() = runBlocking {
    val flowA = (1..6).asFlow()
    val flowB = flowOf("one", "two", "three").onEach { delay(200) }
    flowA.zip(flowB) { a, b -> "$a and $b" }
        .collect { println(it) }

    // 1 and one
    // 2 and two
    // 3 and three
}

(10)combine 合并時,組合每個流最新發出的元素

fun combineFlowFun() = runBlocking {
    val flowA = (1..5).asFlow().onEach { delay(100) }
    val flowB = flowOf("one", "two", "three", "four", "five").onEach { delay(200) }
    flowA.combine(flowB) { a, b -> "$a and $b" }.collect { println(it) }

    // 1 and one
    // 2 and one
    // 3 and one
    // 3 and two
    // 4 and two
    // 5 and two
    // 5 and three
    // 5 and four
    // 5 and five
}

(11)flattenConcat 將給定流按順序展平為單個流,而不交錯嵌套流

fun flattenConcatFlowFun() = runBlocking {
    val flowA = (1..3).asFlow()
    val flowB = flowOf("a", "b", "c").onEach { delay(1000) }
    flowOf(flowA, flowB).flattenConcat().collect { println(it) }

    // 1
    // 2
    // 3
    // a
    // b
    // c
}

(12)fattenMerge 有一個參數,并發限制,默認 16;參數必須大于0,為 1 時,等價于 flattenConcat

fun flattenMergeFlowFun() = runBlocking {
    val flowA = (1..3).asFlow().onEach { delay(1000) }
    val flowB = flowOf("a", "b", "c").onEach { delay(2000) }
    flowOf(flowA, flowB).flattenMerge(8).collect { println(it) }

    // 1
    // a
    // 2
    // 3
    // b
    // c
}

(13)flatMapContact 由 map、flattenConcat 操作符實現,收集新值之前會等待 flatMapConcat 內部的 flow 完成

fun flatMapContactFlowFun() = runBlocking {
    (1..2).asFlow()
        .flatMapConcat {
            flow {
                emit(it)
                delay(1000)
                emit("string: $it")
            }
        }
        .collect {
            println(it)
        }

    // 1
    // string: 1
    // 2
    // string: 2
}

(14)flatMapMerge 由 map、flattenMerge 操作符實現,不會等待內部的 flow 完成

fun flatMapMergeFlowFun() = runBlocking {
    (1..2).asFlow()
        .flatMapMerge {
            flow {
                emit(it)
                delay(1000)
                emit("string: $it")
            }
        }
        .collect {
            println(it)
        }

    // 1
    // 2
    // string: 1
    // string: 2
}

(15)flatMapLatest 當發射了新值之后,上個 flow 就會被取消

fun flatMapLatestFlowFun() = runBlocking {
    (1..3).asFlow()
        .onEach { delay(100) }
        .flatMapLatest {
            flow {
                println("begin flatMapLatest: $it")
                delay(200)
                emit("string: $it")
                println("end flatMapLatest: $it")
            }
        }
        .collect { println(it) }
}


三、StateFlow 和 SharedFlow

StateFlowSharedFlow 是用來替代 BroadcastChannel 的新的 API。用于上游發射數據,能同時被 多個訂閱者 收集數據。

1、StateFlow

(1)StateFlow 是一個狀態容器式可觀察數據流,可以向其收集器發出當前狀態更新和新狀態更新;還可通過其 value 屬性讀取當前狀態值;
(2)StateFlow 有兩種類型: StateFlow(只讀) 和 MutableStateFlow(可以改變 value 的值);
(3)StateFlow 的狀態由其值表示,任何對值的更新都會反饋新值到所有流的接收器中;
(4)StateFlow 發射的數據可以被在不同的協程中的多個接受者同時收集;
(5)StateFlow 是熱流,只要數據發生變化,就會發射數據;
(6)StateFlow 調用 collect 收集數據后不會停止,需要手動取消訂閱者的協程;
(7)StateFlow 只會發射最新的數據給訂閱者。

class StateFlowTest {
    private val _state = MutableStateFlow("unKnow")
    val state: StateFlow<String> get() = _state

    fun getApi1(scope: CoroutineScope) {
        scope.launch {
            delay(1000)
            _state.value = "hello StateFlow"
        }
    }

    fun getApi2(scope: CoroutineScope) {
        scope.launch {
            delay(1000)
            _state.value = "hello Kotlin"
        }
    }
}

fun stateFlowFun() = runBlocking {
    val test = StateFlowTest()
    test.getApi1(this)
    delay(1000)
    test.getApi2(this)

    val job1 = launch(Dispatchers.IO) {
        delay(5000)
        test.state.collect {
            printlnWithThread(it)
        }
    }
    val job2 = launch(Dispatchers.IO) {
        delay(5000)
        test.state.collect {
            printlnWithThread(it)
        }
    }

    delay(7000)
    job1.cancel()
    job2.cancel()

    // Thread -> id: 11, name: DefaultDispatcher-worker-1, hello Kotlin
    // Thread -> id: 13, name: DefaultDispatcher-worker-3, hello Kotlin
}

2、SharedFlow

(1)SharedFlow 管理一系列狀態更新(即事件流),而非管理當前狀態;
(2)SharedFlow 也有兩種類型:SharedFlowMutableSharedFlow
?? - SharedFlow 包含可用作原子快照的 replayCache,每個新的訂閱者會先從 replay cache 中獲取值,然后才收到新發出的值;
?? - MutableSharedFlow 可用于從掛起或非掛起的上下文中發射值,顧名思義,可以重置 replayCache,而且還將訂閱者的數量作為 Flow 暴露出來。
(3)MutableSharedFlow 具有 subscriptionCount 屬性,其中包含處于活躍狀態的收集器的數量;
(4)MutableSharedFlow 包含一個 resetReplayCache 函數,在不想重放已向數據流發送的最新信息的情況下使用;
(5)使用 sharedIn 方法可以將 Flow 轉換為 SharedFlow

class SharedFlowTest {
    private val _state = MutableSharedFlow<Int>(
        replay = 2,                               // 當新的訂閱者 Collect 時,發送幾個已經發送過的數據給它
        extraBufferCapacity = 3,                  // 減去 replay 還緩存多少數據(即此處總緩存為5)
        onBufferOverflow = BufferOverflow.SUSPEND // 緩存溢出時的處理策略,三種 丟掉最新值、丟掉最舊值和掛起
    )
    val state: SharedFlow<Int> get() = _state

    fun getApi(scope: CoroutineScope) {
        scope.launch {
            for (i in 0..5) {
                delay(200)
                _state.emit(i)
                printlnWithThread("send data: $i")
            }
        }
    }
}

fun sharedFlowFun() = runBlocking {
    val test = SharedFlowTest()
    test.getApi(this)

    val job = launch(Dispatchers.IO) {
        delay(1000)
        test.state.collect {
            printlnWithThread("collect data: $it")
        }
    }

    delay(5000)
    job.cancel()

    // 總緩存為5,訂閱時先發送2個舊數據,然后再收集新數據
    // Thread -> id: 1, name: main, send data: 0
    // Thread -> id: 1, name: main, send data: 1
    // Thread -> id: 1, name: main, send data: 2
    // Thread -> id: 1, name: main, send data: 3
    // Thread -> id: 11, name: DefaultDispatcher-worker-1, collect data: 2
    // Thread -> id: 11, name: DefaultDispatcher-worker-1, collect data: 3
    // Thread -> id: 1, name: main, send data: 4
    // Thread -> id: 11, name: DefaultDispatcher-worker-1, collect data: 4
    // Thread -> id: 1, name: main, send data: 5
    // Thread -> id: 11, name: DefaultDispatcher-worker-1, collect data: 5
}
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
禁止轉載,如需轉載請通過簡信或評論聯系作者。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,892評論 2 372

推薦閱讀更多精彩內容