Flow 的理解與運用

Kotlin Flow 可以用于替換 Rxjava,也可以用于替換 LiveData,功能十分強大,它是 Kotlin 協程庫提供的一部分功能,因此,如果我們項目中已經引用了 Kotlin 協程,則不需要額外引入 Flow 相關的依賴。

在協程中,掛起函數最多僅能返回一個值,而數據流 Flow 可按順序發出多個值,例如,我們可以通過數據流從數據庫中實時接收更新。數據流使用掛起函數通過異步方式生成和使用值,也就是說,數據流可安全地發出網絡請求以生成下一個值,而不會阻塞主線程。

數據流 Flow 包含三個重要角色:

數據提供方:生成數據,并添加到數據流中
中介(可選):可修改發送到數據流的值,或修正數據流本身
數據使用方:使用數據流中的值

創建數據流

flow 構建器函數會創建一個新數據流,然后我們可使用 emit 函數將新值發送到數據流中。

val latestNews: Flow<List<NewsData>> = flow {
    while (true) {
        val latestNews = newsApi.fetchLatestNews()
        emit(latestNews)
        delay(5000)
    }
}

修改數據流

中介可以利用中間運算符在不使用值的情況下修改數據流,例如:

  • filter : 對待操作的值進行過濾
  • map :對值進行加工后繼續向后傳遞
  • flatMapLatest:轉換成一個新的流,需要返回一個轉換后的新的流。(如果下個值來了,上一個值變換還沒結束,上一個值的轉換會被取消)
  • onEach:接收到的每一個值
val news: Flow<List<NewsData>> = myRemoteDataSource.latestNews
    // 先過濾列表數據大于3的數據(大于3才能通過)
    .filter {
        it.size >= 3
    }
    // 對結果進行加工后繼續向后傳遞
    .map { list ->
        // 調用 list.filter 進一步篩選出 id==1 的新聞
        list.filter { it.id == 1 }
    }
    // 轉換成一個新的流,需要返回一個轉換后的新的流。(如果下個值來了,上一個值變換還沒結束,上一個值的轉換會被取消)
    .flatMapLatest {
        flow {
            emit(it)
        }
    }
    .onEach {
        // todo 獲取到篩選后 新聞列表(結果) 數據
    }

收集數據流

只有在 收集數據流時 才會觸發 數據提供方 刷新最新數據。除非使用其他中間運算符指定流,否則數據流始終為冷流并延遲執行。

fun getNewsData() {
    viewModelScope.launch(Dispatchers.Main) {
        remoteRepository.news
            .catch {
                // todo 收集異常
            }
            .collect {
                // 收到到的數據
            }
    }
}

數據流收集可能會由于以下原因而停止:

收集數據的協程被取消,此操作也會讓 數據提供方 停止活動。
數據提供方完成發出數據項。在這種情況下,數據流將關閉,調用 collect 的協程則繼續執行。

捕獲異常

如需處理異常,可以使用 catch 運算符,如:

fun getNewsData() {
    viewModelScope.launch {
        remoteRepository.news
            .catch {
                // todo 在這里收集異常
            }
            .collect {
                newsData.value = it
            }
    }
}

另外,catch 還可執行 emit 操作,向數據流發出新的數據項,例如,如果我們在 上游 發現了異常,我們可以繼續調用 emit 函數發送新的數據(或者之前緩存的數據),如:

class MyRemoteRepository @Inject constructor(
    private val myRemoteDataSource: MyRemoteDataSource,
) {
    // 返回 id 等于 1 的新聞
    val news: Flow<List<NewsData>> = myRemoteDataSource.latestNews
        .map { news ->
            // 篩選出 id==1 的新聞
            news.filter { it.id == 1 }
        }
        .onEach {
            // todo 獲取到篩選后 新聞列表(結果) 數據
        }
        .catch {
            // 如果在 上游 收集到異常,我們可以繼續調用 emit 函數發送新的數據(或者之前緩存的數據)
            // 例如:emit(lastCachedNews())
        }
}

協程作用域切換

默認情況下,flow 上游數據提供方 會基于 下游數據收集方 的協程 CoroutineContext 執行,也就是說,默認情況,下游和上游會運行在同一個協程作用域中。

并且,它無法從不同協程作用域對值執行 emit 操作。

如果需要更改數據流的的協程作用域,可以使用中間運算符 flowOn 運算符。

flowOn 會更改上游數據流的 CoroutineContext,但不會影響到下游數據流的作用域。

如果有多個 flowOn 運算符,每個運算符都會更改當前位置的上游數據流。

// 上游數據流代碼,上游數據流將會在 Dispatchers.IO 作用域上執行:

class MyRemoteRepository @Inject constructor(
    private val myRemoteDataSource: MyRemoteDataSource,
) {
    val news: Flow<List<NewsData>> = myRemoteDataSource.latestNews
        .flowOn(Dispatchers.IO)
        .catch { }
}

// 下游數據流代碼,下游數據流將會在 Dispatchers.Main 作用域上執行
fun getNewsData() {
    viewModelScope.launch(Dispatchers.Main) {
        remoteRepository.news.collect { }
    }
}

Flow Demo 演示

下面的代碼將演示通過 Flow 不斷獲取最新的新聞列表。

步驟一:創建 News 類,定義新聞格式

data class NewsData(var id: Int, var content: String)
1
步驟二:創建 NewsApi 接口,用于請求最新的新聞列表

interface NewsApi {
    /**
     * 請求最新的新聞列表
     */
    suspend fun fetchLatestNews(): List<NewsData>

    companion object {
        fun create(): NewsApi {
            return NewsApiImpl()
        }
    }
}

步驟三:創建 DataSource 類,內部有一個 latestNews 變量,作為 Flow 的上游數據提供者,每隔 5 秒,通過 newsApi 請求新聞數據,并調用 emit 方法將新聞數據發出

class MyRemoteDataSource @Inject constructor(
    private val newsApi: NewsApi,
) {
    val latestNews: Flow<List<NewsData>> = flow {
        while (true) {
            val latestNews = newsApi.fetchLatestNews()
            emit(latestNews)
            delay(5000)
        }
    }
}

步驟四:創建 RemoteRepository 類,內部有一個 news 變量,作為 Flow 的數據中間處理者,篩選數據,切換上游作用域,收集上游異常等都可以在這里處理。

class MyRemoteRepository @Inject constructor(
    private val myRemoteDataSource: MyRemoteDataSource,
) {
    // 返回 id 等于 1 的新聞
    val news: Flow<List<NewsData>> = myRemoteDataSource.latestNews
        .map { news ->
            // 篩選出 id==1 的新聞
            news.filter { it.id == 1 }
        }
        .onEach {
            // todo 獲取到篩選后 新聞列表(結果) 數據
        }
        .flowOn(Dispatchers.IO) // 上游數據流將會在 Dispatchers.IO 作用域上執行
        .catch {
            // 如果在 上游 收集到異常,我們可以繼續調用 emit 函數發送新的數據(或者之前緩存的數據)
            // 例如:emit(lastCachedNews())
        }
}

步驟五:創建 ViewModel 類,定義了一個成員方法 getNewsData() ,作為 Flow 的下游數據接收者,另外,還定義了一個 LiveData 變量,監聽最新的新聞數據。

@HiltViewModel
class MainViewModel @Inject constructor(
    private val remoteRepository: MyRemoteRepository,
) : ViewModel() {
    
    val newsData = MutableLiveData<List<NewsData>>()
    
    fun getNewsData() {
        viewModelScope.launch(Dispatchers.Main) {
            remoteRepository.news
                .catch {
                    // todo 收集異常
                }
                .collect {
                    newsData.value = it
                }
        }
    }
}

步驟六:編寫 MainActivity 代碼,接受最新的新聞數據并打印出來

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private val mMainViewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initObserve()
    }

    private fun initObserve() {
        mMainViewModel.newsData.observe(this) {
            println("newsData=${Gson().toJson(it)}")
        }
    }

    fun click(view: View) {
        mMainViewModel.getNewsData()
    }

}

步驟七:創建 NewsApi 的實現類

class NewsApiImpl : NewsApi {
    override suspend fun fetchLatestNews(): List<NewsData> {
        val list = ArrayList<NewsData>()
        list.add(NewsData(1, "news 1"))
        list.add(NewsData(2, "news 2"))
        list.add(NewsData(3, "news 3"))
        return list
    }
}

步驟八:編寫 依賴注入類(di)

@InstallIn(SingletonComponent::class)
@Module
class MainModule {
    @Singleton
    @Provides
    fun provideAppDatabase(@ApplicationContext context: Context): NewsApi {
        return NewsApi.create()
    }
}
// 另外,別忘了在 Application 中加上 @HiltAndroidApp 注解
@HiltAndroidApp
class MainApplication : Application()

步驟九:由于 demo 使用到了 Hilt ,因此我們需要加上如下依賴:

// Project-build.gradle
buildscript {
    ext {
        hiltVersion = '2.41'
    }
    dependencies {
        classpath "com.google.dagger:hilt-android-gradle-plugin:$hiltVersion"
    }
}

// app-build.gradle
plugins {
    id 'kotlin-kapt'
    id 'dagger.hilt.android.plugin'
}
dependencies {
    kapt "com.google.dagger:hilt-android-compiler:$rootProject.hiltVersion"
    implementation "com.google.dagger:hilt-android:$rootProject.hiltVersion"
}

步驟十:為了能在 activity 或 fragment 中使用 by viewModels() api,我們還需額外引入以下 依賴:

// app-build.gradle
dependencies {
    implementation "androidx.activity:activity-ktx:1.4.0"
    implementation "androidx.fragment:fragment-ktx:1.4.1"
}

StateFlw

StateFlow 是一個狀態容器式的可觀察數據流,可以向其收集器發出當前狀態更新和新狀態更新。還可通過其 value 屬性讀取當前狀態值。

StateFlow 非常適合需要讓可變狀態保持可觀察的類。

Flow 是冷數據流,而 StateFlow 是熱數據流,熱流有如下特性:

  • 調用 StateFlow.collect 收集數據不會觸發任何 數據提供方(上游) 的代碼
  • 上游數據流 如果已經處于活躍(發送)狀態,即使沒有任何地方調用 StateFlow.collect ,上游流仍會持續活躍(沒有 Gc Root 引用自然也會被回收)
  • 它允許被多個觀察者共用 (因此是共享的數據流)
    當一個 新的數據接收方 開始從數據流中 collect 數據時,它將接收到信息流中的最近一個狀態及任何后續狀態。

注意:如果 StateFlow.value 接收的新數據和前一個舊數據一樣時,下游并不會接收到數據的更新通知。

StateFlow 和 LiveData

StateFlow 和 LiveData 具有相似之處,兩者都是可觀察的數據容器類。

但也存在不同之處:

  • StateFlow 需要將初始狀態傳遞給構造函數,而 LiveData 不需要
  • 當 View 進入 STOPPED 狀態時,LiveData.observe() 會自動取消注冊使用方,但是從 StateFlow 或任何其他數據流收集數據的操作并不會自動停止,如需實現與 LiveData 相同的行為,則需從 Lifecycle.repeatOnLifecycle 塊中收集數據流
    StateFlow 的簡單用法如下:
// ViewModel: (_uiState.value更新的地方屬于上游)
@HiltViewModel
class MainViewModel @Inject constructor(
    private val remoteRepository: MyRemoteRepository,
) : ViewModel() {

  // 定義一個私有的 MutableStateFlow 變量(可變)
    private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))

    // UI 從此 StateFlow 收集以獲取其狀態更新
    val uiState: StateFlow<LatestNewsUiState> = _uiState

    fun getNewsData() {
        viewModelScope.launch {
            remoteRepository.news.collect {
                // 接受到最新的新聞列表數據后,將數據賦值給 StateFlow 的 value
                _uiState.value = LatestNewsUiState.Success(it)
            }
        }
    }
}

// Activity: (mMainViewModel.uiState.collect的地方屬于下游)
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private val mMainViewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        initObserve()
    }

    private fun initObserve() {
        mMainViewModel.viewModelScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                mMainViewModel.uiState.collect {
                    // 注意:如果 StateFlow.value 接收的新數據和前一個舊數據一樣時,下游并不會接收到數據的更新通知
                    when (it) {
                        is LatestNewsUiState.Success -> {
                            println("獲取新聞成功,news=${Gson().toJson(it.news)}")
                        }
                        is LatestNewsUiState.Error -> {
                            println("獲取新聞失敗,error=${Gson().toJson(it.exception)}")
                        }
                    }
                }
            }
        }
    }

    fun click(view: View) {
        mMainViewModel.getNewsData()
    }

}

如下代碼中,負責更新 MutableStateFlow 的類是 數據提供方(上游) ,從 StateFlow.collect 的類是 數據使用方(下游)。

另外,repeatOnLifecycle 能使界面處于活躍狀態下才會更新界面,要使用該 api,還需引入以下依賴:

// Project-build.gradle
buildscript {
    ext {
        lifecycleVersion = '2.4.1'
    }
}

// app-build.gradle
dependencies {
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:$rootProject.lifecycleVersion"
}

Flow 轉為 StateFlow

如需將任何數據流轉換為 StateFlow ,可以使用 stateIn 中間運算符。

stateIn 有兩個重載函數,一般我們用第二個:

// 函數1:
public suspend fun <T> Flow<T>.stateIn(scope: CoroutineScope): StateFlow<T>

// 函數2:
public fun <T> Flow<T>.stateIn(
    scope: CoroutineScope,
    started: SharingStarted,
    initialValue: T
): StateFlow<T>

其中函數1是一個掛起函數,且僅需要傳一個 scope 參數既可,函數2是非掛起函數,需要傳遞三個參數,三個參數的含義如下:

1、scope :共享流開始時所在的協程作用域范圍

2、started :控制共享的開始和結束的策略

3、initialValue: 流的初始值

而 started 有三種取值可選:

  • SharingStarted.Eagerly :立即啟動上游數據流,且在 scope 指定的作用域被結束時終止上游流
  • SharingStarted.Lazily :在第一個訂閱者出現后開始啟動上游數據流,且在 scope 指定的作用域被結束時終止上游流
  • SharingStarted.WhileSubscribed(stopTimeoutMillis) :在第一個訂閱者出現后開始啟動上游數據流,沒有下游收集的情況下,指定時間后(默認是0)會取消上游數據流
    stateIn 使用如下:
// ViewModel:
@HiltViewModel
class MainViewModel @Inject constructor(
    private val remoteRepository: MyRemoteRepository,
) : ViewModel() {
    // 將 news(Flow冷流) 轉為 StateFlow 熱流
    val mStateFlow: StateFlow<List<NewsData>> = remoteRepository.news
        .stateIn(
            scope = viewModelScope,
            started = WhileSubscribed(5000),
            initialValue = emptyList()
        )
}

// Activity:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private val mMainViewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    // 點擊開始收集上游數據
    fun click(view: View) {
        mMainViewModel.viewModelScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                mMainViewModel.mStateFlow.collect {
                    println("收集到數據,it=${Gson().toJson(it)}")
                }
            }
        }
    }
}

例子中的 ViewModel,定義了一個變量,將 Flow 冷流通過 stateIn 操作符轉為 StateFlow ,該 StateFlow 在第一個訂閱者出現后開始啟動上游數據流,沒有下游收集的情況下,會在 5秒 后取消上游數據流,另外,initialValue 初始值設置為 一個空列表 。在 MainActivity 的代碼中,點擊按鈕將會觸發 上游流開始發送數據,同時下游流也開始接收數據。

WhileSubscribed 傳入了 5000 ,是為了實現等待5 秒后仍然沒有訂閱者存在就終止協程的功能,這個方法有以下功能:

應用轉至后臺運行后,5 秒鐘后所有來自其他層的數據更新會停止,這樣可以節省電量
在屏幕旋轉時,因為重新訂閱的時間在5s內,因此上游流不會中止
SharedFlow
SharedFlow 配置更為靈活,支持配置replay,緩沖區大小等,StateFlow是SharedFlow的特化版本,replay固定為1,緩沖區大小默認為0

我們可使用 shareIn 函數會返回一個熱數據流 SharedFlow, SharedFlow 會 向從其所有的 數據接收方(下游) 發出數據。

我們先看下 ShareFlow 的構造函數:

public fun <T> MutableSharedFlow(
    replay: Int = 0,
    extraBufferCapacity: Int = 0,
    onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND
): MutableSharedFlow<T>

其主要有3個參數:

  • replay:有新的訂閱者Collect時,發送幾個已經發送過的數據給它,默認為0
  • extraBufferCapacity:除了 replay,SharedFlow 還緩存多少數據,默認為0
  • onBufferOverflow:表示緩存策略,即緩沖區滿了之后 ShareFlow 如何處理,默認為掛起
    StateFolw 和 SharedFlow 的區別
    StateFolw 和 SharedFlow 都屬于熱流。

StateFlow 本質上是一個 replay 為 1,且沒有緩沖區的 SharedFlow,因此第一次訂閱時會先獲得默認值。

StateFlow 僅在值已更新,并且值發生了變化時才會返回,也就是說如果更新后的值沒有變化,Collect 方法不會回調,但是 ShareFlow 是會回調的。

下面舉個簡單的使用 SharedFlow 的例子:

// ViewModel:
@HiltViewModel
class MainViewModel @Inject constructor(
    private val remoteRepository: MyRemoteRepository,
) : ViewModel() {
    // 定義一個私有的 MutableSharedFlow 變量(可變),當有新的訂閱者時,會先發送1個之前發送過的數據給訂閱者
    private val mMutableSharedFlow = MutableSharedFlow<List<NewsData>>(replay = 1)
    // 不可變的 shareFlow
    val shareFlow: SharedFlow<List<NewsData>> = mMutableSharedFlow
    
    fun getNewsData() {
        viewModelScope.launch {
            remoteRepository.news.collect {
                mMutableSharedFlow.emit(it)
            }
        }
    }
}

// Activity:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private val mMainViewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    // ShareFlow 開始發送數據  
    fun click(view: View) {
        mMainViewModel.getNewsData()
    }

    // 收集 ShareFlow 發送的數據
    fun click2(view: View) {
        mMainViewModel.viewModelScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                mMainViewModel.shareFlow.collect {
                    println("獲取到的新聞,news=${Gson().toJson(it)}")
                }
            }
        }
    }

}

該例子中,ViewModel 中定義了一個 ShareFlow,并將其 replay 參數設置為 1 ,即當有新的訂閱者時,會先發送 1 個之前發送過的數據給訂閱者。在Activity 中,有兩個按鈕,按鈕 1 觸發 ShareFlow 上游開始發送數據, 按鈕 2 觸發 下游收集數據,按鈕 2 按下后,下游會先收集到 1 個之前發送過的數據。

Flow 轉為 SharedFlow
如需將任何數據流轉換為 SharedFlow ,可以使用 ShareIn 中間運算符:

public fun <T> Flow<T>.shareIn(
    scope: CoroutineScope,
    started: SharingStarted,
    replay: Int = 0
): SharedFlow<T>

ShareIn 函數有三個參數:

  • scope:共享流開始時所在的協程作用域范圍
  • started:控制共享的開始和結束的策略
  • replay:有新的訂閱者Collect時,發送幾個已經發送過的數據給它,默認為0
    舉個簡單的例子(跟 StateIn 的例子很像):
// ViewModel:
@HiltViewModel
class MainViewModel @Inject constructor(
    private val remoteRepository: MyRemoteRepository,
) : ViewModel() {
    // 將 news(Flow冷流) 轉為 SharedFlow 熱流
    val mSharedFlow: SharedFlow<List<NewsData>> = remoteRepository.news
        .shareIn(
            scope = viewModelScope,
            started = WhileSubscribed(5000),
            replay = 1
        )
}

// Activity:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private val mMainViewModel: MainViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    fun click(view: View) {
        // 點擊開始收集上游數據
        mMainViewModel.viewModelScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                mMainViewModel.mSharedFlow.collect {
                    println("收集到的新聞數據,news=${Gson().toJson(it)}")
                }
            }
        }
    }
}

例子中的 ViewModel,定義了一個變量,將 Flow 冷流通過 shareIn 操作符轉為 SharedFlow ,該 SharedFlow 在第一個訂閱者出現后開始啟動上游數據流,沒有下游收集的情況下,會在 5秒 后取消上游數據流,另外,replay設置為 1 ,當有下游收集者時,會將之前發過的最近一個值發給下游收集者。在 MainActivity 的代碼中,點擊按鈕將會觸發 上游流開始發送數據,同時下游流也開始接收數據。

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

推薦閱讀更多精彩內容