使用 Kotlin Flow 優(yōu)化你的網(wǎng)絡(luò)請求框架馋没,減少模板代碼

目的

本文不涉及 Flow 很深的東西辟犀,即使不會 Flow 也可以上手使用械蹋。

話接上篇文章 兩種方式封裝Retrofit+協(xié)程出皇,實現(xiàn)優(yōu)雅快速的網(wǎng)絡(luò)請求

最近在獨立寫一個新的項目,用的是封裝二朝蜘,雖然幾行代碼就可以進行網(wǎng)絡(luò)請求,但是在使用過程中還是覺得有點遺憾涩金,寫起來也不是非称状迹快捷暇仲,存在模板代碼。

加上很多小伙伴想要一個Flow版本的副渴,忙里偷閑奈附,用kotlin Flow對這套框架進行了優(yōu)化,發(fā)現(xiàn)flow真香煮剧。

一斥滤、以前封裝的遺憾點

主要集中在如下2點上:

  • Loading的處理

  • 多余的LiveData

總而言之,就是需要寫很多模板代碼勉盅。

不必編寫模版代碼的一個最大好處就是: 寫的代碼越少佑颇,出錯的概率越小.

1.1 Loading的處理

對于封裝二,雖然解耦比封裝一更徹底草娜,但是關(guān)于Loading這里我覺得還是有遺憾挑胸。

試想一下:如果Activity中業(yè)務(wù)很多、邏輯復(fù)雜宰闰,存在很多個網(wǎng)絡(luò)請求茬贵,在需要網(wǎng)絡(luò)請求的地方都要手動去showLoading() ,然后在 observer() 中手動調(diào)用 stopLoading()

假如Activity中代碼業(yè)務(wù)復(fù)雜移袍,存在多個api接口解藻,這樣Activity中就存在很多個與loading有關(guān)的方法。

此外葡盗,如果一個網(wǎng)絡(luò)請求的showLoading()方法和dismissLoading()方法相隔很遠螟左。會導(dǎo)致一個順序流程的割裂。

請求開始前showLoading() ---> 請求網(wǎng)絡(luò) ---> 結(jié)束后stopLoading()戳粒,這是一個完整的流程路狮,代碼也應(yīng)該盡量在一起,一目了然蔚约,不應(yīng)該割裂存在奄妨。

如果代碼量一多,以后維護起來苹祟,萬一不小心刪除了某個showLoading()或者stopLoading()砸抛,也容易導(dǎo)致問題。

還有就是每次都要手動調(diào)用這兩個方法树枫,麻煩直焙。

1.2 重復(fù)的LiveData聲明

個人認為常用的網(wǎng)絡(luò)請求分為兩大類:

  • 用完即丟,只運行一次砂轻,返回一個結(jié)果

  • 需要監(jiān)聽數(shù)據(jù)變化奔誓,可以在一段時間內(nèi)發(fā)出多個值

舉個常見的例子,看下面這個頁面:

image.png

用戶一進入這個頁面搔涝,綠色框里面內(nèi)容基本不會變化厨喂,(不去糾結(jié)微信這個頁面是不是webview之類的)和措,這種ui其實是不需要設(shè)置一個LiveData去監(jiān)聽的,因為它幾乎不會再更新了蜕煌。

典型的還有:點擊登錄按鈕派阱,成功后就進去了下一個頁面。

但是紅色的框里面的ui不一樣斜纪,需要實時刷新數(shù)據(jù)贫母,也就用到LiveData監(jiān)聽,這種情況下觀察者訂閱者模式的好處才真正展示出來盒刚。并且從其他頁面過來腺劣,LiveData也會把最新的數(shù)據(jù)自動更新。

對于用完即丟的網(wǎng)絡(luò)請求伪冰,LoginViewModel會存在這種代碼:

// LoginViewModel.kt
val loginLiveData = MutableLiveData<User?>()
val logoutLiveData = MutableLiveData<Any?>()
val forgetPasswordLiveData = MutableLiveData<User?>()

并且對應(yīng)的Activity中也需要監(jiān)聽這3個LiveData誓酒。

這種模板代碼讓我寫的很煩。

用了Flow優(yōu)化后贮聂,完美的解決這2個痛點靠柑。

“Talk is cheap. Show me the code.”

二、集成Flow之后的用法

2.1 請求自帶Loading&&不需要監(jiān)聽數(shù)據(jù)變化

需求:

  • 不需要監(jiān)聽數(shù)據(jù)變化吓懈,對應(yīng)上面的用完即丟

  • 不需要在ViewModel中聲明LiveData成員對象

  • 發(fā)起請求之前自動showLoading()歼冰,請求結(jié)束后自動stopLoading()

  • 類似于點擊登錄按鈕,finish 當(dāng)前頁面耻警,跳轉(zhuǎn)到下一個頁面

TestActivity 中示例代碼:

// TestActivity.kt
private fun login() {
    launchWithLoadingAndCollect({mViewModel.login("username", "password")}) {
        onSuccess = { data->
            showSuccessView(data)
        }
        onFailed = { errorCode, errorMsg ->
            showFailedView(code, msg)
        }
        onError = {e ->
            e.printStackTrace()
        }
    }
}

TestViewModel 中代碼:

// TestViewModel中代碼
suspend fun login(username: String, password: String): ApiResponse<User?> {
    return repository.login(username, password)
}

2.2 請求不帶Loading&&不需要聲明LiveData

需求:

  • 不需要監(jiān)聽數(shù)據(jù)變化

  • 不需要在ViewModel中聲明LiveData成員對象

  • 不需要Loading的展示

// TestActivity.kt
private fun getArticleDetail() {
    launchAndCollect({ mViewModel.getArticleDetail() }) {
            onSuccess = {
                showSuccessView()
            }
            onFailed = { errorCode, errorMsg ->
                showFailedView(code, msg)
            }
            onDataEmpty = {
                showEmptyView()
            }
        }
}

TestViewModel 中代碼和上面一樣隔嫡,這里就不寫了。

是不是非常簡單甘穿,一個方法搞定腮恩,將Loading的邏輯都隱藏了,再也不需要手動寫 showLoading()stopLoading()温兼。

并且請求的結(jié)果直接在回調(diào)里面接收秸滴,直接處理,這樣請求網(wǎng)絡(luò)和結(jié)果的處理都在一起募判,看起來一目了然荡含,再也不需要在 Activity 中到處找在哪監(jiān)聽的 LiveData

同樣届垫,它跟 LiveData 一樣释液,也會監(jiān)聽 Activity 的生命周期,不會造成內(nèi)存泄露装处。因為它是運行在ActivitylifecycleScope 協(xié)程作用域中的误债。

2.3 需要監(jiān)聽數(shù)據(jù)變化

需求:

  • 需要監(jiān)聽數(shù)據(jù)變化,要實時更新數(shù)據(jù)

  • 需要在 ViewModel 中聲明 LiveData 成員對象

  • 例如實時獲取最新的配置、最新的用戶信息等

TestActivity 中示例代碼:

// TestActivity.kt
class TestActivity : AppCompatActivity(R.layout.activity_api) {

    private fun initObserver() {
        mViewModel.wxArticleLiveData.observeState(this) {
        
            onSuccess = { data: List<WxArticleBean>? ->
                showSuccessView(data)
            }

            onDataEmpty = { showEmptyView() }

            onFailed = { code, msg -> showFailedView(code, msg) }

            onError = { showErrorView() }
        }
    }

    private fun requestNet() {
        // 需要Loading
        launchWithLoading {
            mViewModel.requestNet()
        }
    }
}

ViewModel 中示例代碼:

class ApiViewModel : ViewModel() {

    private val repository by lazy { WxArticleRepository() }

    val wxArticleLiveData = StateMutableLiveData<List<WxArticleBean>>()

    suspend fun requestNet() {
        wxArticleLiveData.value = repository.fetchWxArticleFromNet()
    }
}

本質(zhì)上是通過FLow來調(diào)用LiveDatasetValue()方法,還是LiveData的使用。雖然可以完全用 Flow 來實現(xiàn)辜窑,但是我覺得這里用 Flow 的方式麻煩旺垒,不容易懂,還是怎么簡單怎么來形帮。

這種方式其實跟上篇文章中的封裝二差不多槽惫,區(qū)別就是不需要手動調(diào)用Loading有關(guān)的方法。

用2張流程圖來對比下上面的方式:

[圖片上傳失敗...(image-25b035-1638674349543)]

三辩撑、拆封裝

如果不抽取通用方法是這樣寫的:

// TestActivity.kt
private fun login() {
    lifecycleScope.launch {
        flow {
            emit(mViewModel.login("username", "password"))
        }.onStart {
            showLoading()
        }.onCompletion {
            dismissLoading()
        }.collect { response ->
            when (response) {
                is ApiSuccessResponse -> showSuccessView(response.data)
                is ApiEmptyResponse -> showEmptyView()
                is ApiFailedResponse -> showFailedView(response.errorCode, response.errorMsg)
                is ApiErrorResponse -> showErrorView(response.error)
            }
        }
    }
}

簡單介紹下Flow

Flow類似于RxJava,操作符都跟Rxjava差不多界斜,但是比Rxjava簡單很多,kotlin通過flow來實現(xiàn)順序流和鏈?zhǔn)骄幊獭?/p>

flow關(guān)鍵字大括號里面的是方法的執(zhí)行合冀,結(jié)果通過emit發(fā)送給下游各薇。

onStart表示最開始調(diào)用方法之前執(zhí)行的操作,這里是展示一個 loading ui君躺;

onCompletion表示所有執(zhí)行完成峭判,不管有沒有異常都會執(zhí)行這個回調(diào)。

collect表示執(zhí)行成功的結(jié)果回調(diào)棕叫,就是emit()方法發(fā)送的內(nèi)容林螃,flow必須執(zhí)行collect才能有結(jié)果。因為是冷流俺泣,對應(yīng)的還有熱流疗认。

更多的Flow知識點可以參考其他博客和官方文檔。

這里可以看出伏钠,通過Flow完美的解決了loading的顯示與隱藏横漏。

我這里是在Activity中都調(diào)用flow的流程,這樣我們擴展BaseActivity即可熟掂。

為什么擴展的是BaseActivity?

因為startLoading()stopLoading()BaseActivity中缎浇。??

3.1 解決 flow 的 Loading 模板代碼

fun <T> BaseActivity.launchWithLoadingGetFlow(block: suspend () -> ApiResponse<T>): Flow<ApiResponse<T>> {
    return flow {
        emit(block())
    }.onStart {
        showLoading()
    }.onCompletion {
        dismissLoading()
    }
}

這樣每次調(diào)用launchWithLoadingGetFlow方法,里面就實現(xiàn)了 Loading 的展示與隱藏打掘,并且會返回一個 FLow 對象华畏。

下一步就是處理 flow 結(jié)果collect里面的模板代碼。

3.2 聲明結(jié)果回調(diào)類

class ResultBuilder<T> {
    var onSuccess: (data: T?) -> Unit = {}
    var onDataEmpty: () -> Unit = {}
    var onFailed: (errorCode: Int?, errorMsg: String?) -> Unit = { _, _ -> }
    var onError: (e: Throwable) -> Unit = { e -> }
    var onComplete: () -> Unit = {}
}

各種回調(diào)按照項目特性刪減即可尊蚁。

3.3 對ApiResponse對象進行解析

private fun <T> parseResultAndCallback(response: ApiResponse<T>, 
                                       listenerBuilder: ResultBuilder<T>.() -> Unit) {
    val listener = ResultBuilder<T>().also(listenerBuilder)
    when (response) {
        is ApiSuccessResponse -> listener.onSuccess(response.response)
        is ApiEmptyResponse -> listener.onDataEmpty()
        is ApiFailedResponse -> listener.onFailed(response.errorCode, response.errorMsg)
        is ApiErrorResponse -> listener.onError(response.throwable)
    }
    listener.onComplete()
}

上篇文章這里的處理用的是繼承LiveDataObserver亡笑,這里就不需要了,畢竟繼承能少用就少用横朋。

3.4 最終抽取方法

將上面的步驟連起來如下:

fun <T> BaseActivity.launchWithLoadingAndCollect(block: suspend () -> ApiResponse<T>, 
                                                listenerBuilder: ResultBuilder<T>.() -> Unit) {
    lifecycleScope.launch {
        launchWithLoadingGetFlow(block).collect { response ->
            parseResultAndCallback(response, listenerBuilder)
        }
    }
}

3.5 將Flow轉(zhuǎn)換成LiveData對象

獲取到的是Flow對象仑乌,如果想要變成LiveDataFlow原生就支持將Flow對象轉(zhuǎn)換成不可變的LiveData對象。

val loginFlow: Flow<ApiResponse<User?>> =
    launchAndGetFlow(requestBlock = { mViewModel.login("UserName", "Password") })
val loginLiveData: LiveData<ApiResponse<User?>> = loginFlow.asLiveData()

調(diào)用的是 Flow 的asLiveData()方法晰甚,原理也很簡單,就是用了livedata的擴展函數(shù):

@JvmOverloads
fun <T> Flow<T>.asLiveData(
    context: CoroutineContext = EmptyCoroutineContext,
    timeoutInMs: Long = DEFAULT_TIMEOUT
): LiveData<T> = liveData(context, timeoutInMs) {
    collect {
        emit(it)
    }
}

這里返回的是LiveData<ApiResponse<User?>>對象衙传,如果想要跟上篇文章一樣用StateLiveData,在observe的回調(diào)里面監(jiān)聽不同狀態(tài)的callback

以前的方式是繼承厕九,有如下缺點:

  • 必須要用StateLiveData蓖捶,不能用原生的LiveData,侵入性很強
  • 不只是繼承LiveData,還要繼承Observer扁远,麻煩
  • 為了實現(xiàn)這個俊鱼,寫了一堆的代碼

這里用 Kotlin 擴展實現(xiàn),直接擴展 LiveData

@MainThread
inline fun <T> LiveData<ApiResponse<T>>.observeState(
    owner: LifecycleOwner,
    listenerBuilder: ResultBuilder<T>.() -> Unit
) {
    val listener = ResultBuilder<T>().also(listenerBuilder)
    observe(owner) { apiResponse ->
        when (apiResponse) {
            is ApiSuccessResponse -> listener.onSuccess(apiResponse.response)
            is ApiEmptyResponse -> listener.onDataEmpty()
            is ApiFailedResponse -> listener.onFailed(apiResponse.errorCode, apiResponse.errorMsg)
            is ApiErrorResponse -> listener.onError(apiResponse.throwable)
        }
        listener.onComplete()
    }
}

感謝Flywith24開源庫提供的思路畅买,感覺自己有時候還是在用Java的思路在寫Kotlin并闲。

3.6 進一步完善

很多網(wǎng)絡(luò)請求的相關(guān)并不是只有 loading 狀態(tài),還需要在請求前和結(jié)束后處理一些特定的邏輯谷羞。

這里的方式是:直接在封裝方法的參數(shù)加 callback帝火,默認用是 loading 的實現(xiàn)。

fun <T> BaseActivity.launchAndCollect(
    requestBlock: suspend () -> ApiResponse<T>,
    startCallback: () -> Unit = { showLoading() },
    completeCallback: () -> Unit = { dismissLoading() },
    listenerBuilder: ResultBuilder<T>.() -> Unit
)

四湃缎、針對多數(shù)據(jù)來源

雖然項目中大部分都是單一數(shù)據(jù)來源犀填,但是也偶爾會出現(xiàn)多數(shù)據(jù)來源,多數(shù)據(jù)源結(jié)合Flow的操作符嗓违,也非常的方便宏浩。

示例

假如同一份數(shù)據(jù)可以從數(shù)據(jù)庫獲取,可以從網(wǎng)絡(luò)請求獲取靠瞎,TestRepository的代碼如下:

// TestRepository.kt
suspend fun fetchDataFromNet(): Flow<ApiResponse<List<WxArticleBean>>> {
    val response =  executeHttp { mService.getWxArticle() }
    return flow { emit(response) }.flowOn(Dispatchers.IO)
}

suspend fun fetchDataFromDb(): Flow<ApiResponse<List<WxArticleBean>>> {
    val response =  getDataFromRoom()
    return flow { emit(response) }.flowOn(Dispatchers.IO)
}

Repository中的返回不再直接返回實體類比庄,而是返回flow包裹的實體類對象。

為什么要這么做乏盐?

為了用神奇的flow操作符來處理佳窑。

flow組合操作符

  • combine、combineTransform
    combine操作符可以連接兩個不同的Flow父能。

  • merge
    merge操作符用于將多個流合并神凑。

  • zip
    zip操作符會分別從兩個流中取值,當(dāng)一個流中的數(shù)據(jù)取完何吝,zip過程就完成了溉委。

關(guān)于 Flow 的基礎(chǔ)操作符,徐醫(yī)生大神的這篇文章已經(jīng)寫的很棒了爱榕,這里就不多余的寫了瓣喊。

根據(jù)操作符的示例可以看出,就算返回的不是同一個對象黔酥,也可以用操作符進行處理藻三。

幾年前剛開始學(xué)RxJava時洪橘,好幾次都是入門到放棄,操作符太多了棵帽,搞的也很懵逼熄求,F(xiàn)low 真的比它簡單太多了。

五逗概、flow的奇淫技巧

flowWithLifecycle

需求:
Activity 的 onResume() 方法中請求最新的地理位置信息弟晚。

以前的寫法:

// TestActivity.kt
override fun onResume() {
    super.onResume()
    getLastLocation()
}

override fun onDestory() {
    super.onDestory()
    // 釋放獲取定位的代碼,防止內(nèi)存泄露
}

這種寫法沒問題逾苫,也很正常指巡,但是用了 Flow 之后,有一種新的寫法隶垮。

用了 flow 的寫法:

// TestActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    getLastLocation()
}

@ExperimentalCoroutinesApi
@SuppressLint("MissingPermission")
private fun getLastLocation() {
    if (LocationPermissionUtils.isLocationProviderEnabled() && LocationPermissionUtils.isLocationPermissionGranted()) {
        lifecycleScope.launch {
           NetWorkLocationHelper(this)
            .getNetLocationFlow()
            .flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
            .collect { location ->
                Log.i(TAG, "最新的位置是:$location")
            }
        }
    }
}

onCreate中書寫該函數(shù),然后 flow 的鏈?zhǔn)秸{(diào)用中加入:

.flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)

flowWithLifecycle能監(jiān)聽 Activity 的生命周期秘噪,在 Activity 的onResume開始請求位置信息狸吞,onStop 時自動停止,不會導(dǎo)致內(nèi)存泄露指煎。

flowWithLifecycle 會在生命周期進入和離開目標(biāo)狀態(tài)時發(fā)送項目和取消內(nèi)部的生產(chǎn)者蹋偏。

這個api需要引入 androidx.lifecycle:lifecycle-runtime-ktx:2.4.0-rc01依賴庫。

callbackFlow

有沒有發(fā)現(xiàn)5.1中調(diào)用獲取位置信息的代碼很簡單至壤?

NetWorkLocationHelper(this)
    .getNetLocationFlow()
    .flowWithLifecycle(lifecycle, Lifecycle.State.RESUMED)
    .collect { location -> 
         Log.i(TAG, "最新的位置是:$location")
    }

幾行代碼解決獲取位置信息威始,并且任何地方都直接調(diào)用,不要寫一堆代碼像街。

這里就是用到callbackFlow黎棠,簡而言之,callbackFlow就是將callback回調(diào)代碼變成同步的方式來寫镰绎。

這里直接上NetWorkLocationHelper的代碼脓斩,具體細節(jié)自行 Google,因為這就不是網(wǎng)絡(luò)框架的內(nèi)容畴栖。

這里附上主要的代碼:

suspend fun getNetLocationFlow(context: Context): Flow<Location?> {
    return callbackFlow<Location?> {
        val locationManager: LocationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            val consumer: java.util.function.Consumer<Location> = java.util.function.Consumer<Location> { location -> offer(location) }
            locationManager.getCurrentLocation(LocationManager.NETWORK_PROVIDER, null, context.mainExecutor, consumer)
            awaitClose()
        } else {
            val locationListener = LocationListener { location -> offer(location) }
            locationManager.requestSingleUpdate(LocationManager.NETWORK_PROVIDER, locationListener, Looper.getMainLooper())
            awaitClose {
                locationManager.removeUpdates(locationListener)
            }
        }
    }
}

詳細代碼見Github

總結(jié)

上一篇文章# 兩種方式封裝Retrofit+協(xié)程随静,實現(xiàn)優(yōu)雅快速的網(wǎng)絡(luò)請求

加上這篇的 flow 網(wǎng)絡(luò)請求封裝,一共是三種對Retrofit+協(xié)程的網(wǎng)絡(luò)封裝方式吗讶。

對比下三種封裝方式:

  • 封裝一 (對應(yīng)分支oneWay) 傳遞ui引用燎猛,可按照項目進行深度ui定制,方便快速照皆,但是耦合高

  • 封裝二 (對應(yīng)分支master) 耦合低重绷,依賴的東西很少,但是寫起來模板代碼偏多

  • 封裝三 (對應(yīng)分支dev) 引入了新的flow流式編程(雖然出來很久膜毁,但是大部分人應(yīng)該還沒用到)论寨,鏈?zhǔn)秸{(diào)用星立,loading 和網(wǎng)絡(luò)請求以及結(jié)果處理都在一起,很多時候甚至都不要聲明 LiveData 對象葬凳。

第二種封裝我在公司的商業(yè)項目App中用了很長時間了绰垂,涉及幾十個接口,暫時沒遇到什么問題火焰。

第三種是我最近才折騰出來的劲装,在公司的新項目中(還沒上線)使用,也暫時沒遇到什么問題昌简。

如果某位大神看到這篇文章占业,有不同意見,或者發(fā)現(xiàn)封裝三有漏洞纯赎,歡迎指出谦疾,不甚感謝!

項目地址

FastJetpack

項目持續(xù)更新...

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末犬金,一起剝皮案震驚了整個濱河市念恍,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌晚顷,老刑警劉巖峰伙,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異该默,居然都是意外死亡瞳氓,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門栓袖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來匣摘,“玉大人,你說我怎么就攤上這事裹刮×滴郑” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵必指,是天一觀的道長囊咏。 經(jīng)常有香客問我,道長塔橡,這世上最難降的妖魔是什么梅割? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮葛家,結(jié)果婚禮上户辞,老公的妹妹穿的比我還像新娘。我一直安慰自己癞谒,他們只是感情好底燎,可當(dāng)我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布刃榨。 她就那樣靜靜地躺著,像睡著了一般双仍。 火紅的嫁衣襯著肌膚如雪枢希。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天朱沃,我揣著相機與錄音苞轿,去河邊找鬼。 笑死逗物,一個胖子當(dāng)著我的面吹牛搬卒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播翎卓,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼契邀,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了失暴?” 一聲冷哼從身側(cè)響起坯门,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎锐帜,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體畜号,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡缴阎,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了简软。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片蛮拔。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖痹升,靈堂內(nèi)的尸體忽然破棺而出建炫,到底是詐尸還是另有隱情,我是刑警寧澤疼蛾,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布肛跌,位于F島的核電站,受9級特大地震影響察郁,放射性物質(zhì)發(fā)生泄漏衍慎。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一皮钠、第九天 我趴在偏房一處隱蔽的房頂上張望稳捆。 院中可真熱鬧,春花似錦麦轰、人聲如沸乔夯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽末荐。三九已至侧纯,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間鞠评,已是汗流浹背茂蚓。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留剃幌,地道東北人聋涨。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像负乡,于是被迫代替她去往敵國和親牍白。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內(nèi)容