Android MVVM架構(gòu)實(shí)踐,單Activity+Kotlin+DataBinding+Jetpack+協(xié)程(附完整項(xiàng)目)

前言

關(guān)于android開發(fā)架構(gòu)這方面的文章雖然網(wǎng)上非常多,但是大多數(shù)給出的實(shí)例都是demo級別,而并不足以解決在實(shí)際開發(fā)中遇到的一些問題,本文將帶你從頭構(gòu)建mvvm項(xiàng)目框架,并一步步在開發(fā)中完善。本文所有代碼都為Kotlin編寫,不太了解的同學(xué)也不要太在意細(xì)節(jié),明白大概意思就行。完整項(xiàng)目地址在這里,有些地方我可能說得比較簡單需要自行翻閱代碼。

什么是mvvm?主要是運(yùn)用數(shù)據(jù)驅(qū)動(dòng)的思想,將View(視圖,android中的xml布局),ViewModel(數(shù)據(jù)模型,android中裝載視圖所需的數(shù)據(jù)類的實(shí)例)綁定在一起,通過改變ViewModel的數(shù)據(jù)自動(dòng)更新視圖。在android開發(fā)中,就要借助DataBinding來實(shí)現(xiàn)數(shù)據(jù)綁定,如果你還不太了解它,建議先去看官方文檔熟悉一下基本用法。這里是傳送門

1. 抽象基類

根據(jù)MVVM的思路,我們將一個(gè)頁面拆分成四個(gè)部分

  • xml 布局文件,類似這樣

    <layout xmlns:android="http://schemas.android.com/apk/res/android"
      xmlns:app="http://schemas.android.com/apk/res-auto">
    
      <data>
    
          <import type="com.lyj.fakepixiv.module.login.WallpaperViewModel" />
    
          <variable
              name="vm"
              type="WallpaperViewModel" />
      </data>
    
      <RelativeLayout
          android:layout_width="match_parent"
          android:layout_height="match_parent"
          android:orientation="vertical">
    
      </RelativeLayout>
    
    
  • activity/fragment:它的主要作用是做一些綁定操作以及對生命周期進(jìn)行管理。

    abstract class BaseActivity<V : ViewDataBinding, VM : BaseViewModel?> : AppCompatActivity() {
      protected lateinit var mBinding: V
      protected abstract val mViewModel: VM
      protected var mToolbar: Toolbar? = null
    
      override fun onCreate(savedInstanceState: Bundle?) {
          super.onCreate(savedInstanceState)
          mViewModel?.let {
              // 綁定生命周期
              lifecycle.addObserver(mViewModel as LifecycleObserver)
          }
          mBinding = DataBindingUtil.setContentView(this, bindLayout())
          mBinding.setVariable(bindViewModel(), mViewModel)
          mToolbar = mBinding.root.findViewById(bindToolbar())
    
      }
    
    
      override fun onDestroy() {
          super.onDestroy()
          mBinding.unbind()
      }
    
      @LayoutRes
      abstract fun bindLayout() : Int
    
      open fun bindViewModel() : Int = BR.vm
    
      open fun bindToolbar() : Int = R.id.toolbar
      }
    

    Activity持有binding和ViewModel,并將它們進(jìn)行綁定,這里預(yù)設(shè)BR.vm為xml布局中ViewModel的id。同時(shí)通過lifecycle把生命周期代理到ViewModel中去。lifecycles是Android Jetpack中用于處理生命周期的組件,在support包26.1.0以后activity和fragment已經(jīng)對其進(jìn)行了實(shí)現(xiàn),具體用法參照這里

  • ViewModel:數(shù)據(jù)模型,用于裝載視圖所需數(shù)據(jù)的容器

    abstract class BaseViewModel : BaseObservable(), LifecycleObserver,
          CoroutineScope by CoroutineScope(Dispatchers.Main + SupervisorJob()) {
    
      protected val mDisposable: CompositeDisposable by lazy { CompositeDisposable() }
    
      protected val disposableList by lazy { mutableListOf<Disposable>() }
    
      // 子viewModel list
      protected val mSubViewModelList by lazy { mutableListOf<BaseViewModel>() }
    
      // 生命周期代理
      @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
      open fun onDestroy(@NotNull owner: LifecycleOwner) {
          // 子ViewModel銷毀
          mSubViewModelList.forEach { it.onDestroy(owner) }
          // 取消rxjava任務(wù)
          disposableList.forEach { it.dispose() }
          // 取消協(xié)程任務(wù)
          coroutineContext.cancelChildren()
      }
    
      @OnLifecycleEvent(Lifecycle.Event.ON_ANY)
      open fun onLifecycleChanged(@NotNull owner: LifecycleOwner, @NotNull event: Lifecycle.Event) {
    
      }
    
      // 重載運(yùn)算符
      operator fun plus(vm: BaseViewModel?): BaseViewModel {
          vm?.let { mSubViewModelList.add(it) }
          return this
      }
    
      protected fun addDisposable(disposable: Disposable?) {
          disposable?.let {
              disposableList.add(it)
              }
          }
      }
    

    這里BaseViewModel分別實(shí)現(xiàn)了三個(gè)接口/抽象類,BaseObservable用于databinding綁定數(shù)據(jù),LifecycleObserver用于處理生命周期,CoroutineScope則用于創(chuàng)建協(xié)程域,如果不用協(xié)程可以去掉相關(guān)代碼。

  • Model 數(shù)據(jù)層,做一些獲取數(shù)據(jù)以及數(shù)據(jù)轉(zhuǎn)換的操作。

    創(chuàng)建Repository單例,從網(wǎng)絡(luò)獲取數(shù)據(jù)

    class IllustRepository private constructor() {
      
      val service: IllustService by lazy { RetrofitManager.instance.illustService }
      
      companion object {
          val instance by lazy { IllustRepository() }
          }
    
      /**
       * 獲取推薦  rxjava方式
       */
      fun loadRecommendIllust(@IllustCategory category: String): Observable<IllustListResp> {
          return service.getRecommendIllust(category)
                  .io()
          }
    
      /**
       * 獲取排行榜  協(xié)程方式
       * [category] illust插畫、漫畫 novel小說
       */
      suspend fun getRankIllust(mode: String, date: String = "", @IllustCategory category: String = ILLUST): IllustListResp {
          val realCategory = if (category == NOVEL) NOVEL else ILLUST
          return service.getRankIllust(realCategory, mode, date)
          }
      }
    

    一般來說Model層會(huì)擁有多個(gè)數(shù)據(jù)源,比如最常見的網(wǎng)絡(luò)數(shù)據(jù)和本地緩存數(shù)據(jù),但是我這里沒做數(shù)據(jù)持久化,所以就直接將獲取數(shù)據(jù)的實(shí)現(xiàn)方法放在了Repository類中。網(wǎng)絡(luò)層我用的是retrofit+rxjava/kotlin協(xié)程,retrofit高版本已經(jīng)添加了對于協(xié)程的支持。

2. 小試牛刀

這里我以一個(gè)用戶列表頁為例,來看一下代碼。


users.png

xml布局上就一個(gè)recyclerView沒啥好說的,我們直接去看item的xml文件。
它綁定了一個(gè)UserItemViewModel,使用了其中的數(shù)據(jù);包含作品列表、用戶頭像、昵稱等控件,同時(shí)綁定了點(diǎn)擊事件。

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <import type="com.lyj.fakepixiv.module.common.UserItemViewModel" />

        <import type="com.lyj.fakepixiv.app.network.LoadState" />

        <variable
            name="vm"
            type="UserItemViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="@android:color/white"
        android:clipChildren="false"
        android:orientation="vertical">

        <!--    用戶作品預(yù)覽列表    -->
        <androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" />

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:clipChildren="false"
            android:orientation="horizontal">

            <RelativeLayout
                android:id="@+id/container"
                android:layout_width="wrap_content"
                android:layout_height="match_parent"
                android:layout_gravity="bottom"
                android:layout_marginStart="8dp"
                android:onClick="@{() -> vm.goDetail()}">

                <ImageView
                    android:id="@+id/avatar"
                    android:layout_width="60dp"
                    android:layout_height="60dp"
                    android:layout_marginTop="-16dp"
                    android:visibility="gone"
                    app:circle="@{true}"
                    app:placeHolder="@{@drawable/no_profile}"
                    app:url="@{vm.data.user.profile_image_urls.medium}"
                    app:visible="@{vm.data.illusts.size > 0}" />

                ......
            </RelativeLayout>

        </LinearLayout>
    </LinearLayout>
</layout>

接下來再看UserItemViewModel類

class UserItemViewModel(val parent: BaseViewModel, val data: UserPreview) : BaseViewModel(), PreloadModel by data {

    // 是否關(guān)注/取消關(guān)注成功
    var followState: ObservableField<LoadState> = ObservableField(LoadState.Idle)

    init {
        parent + this
    }

    /**
     * 關(guān)注/取消關(guān)注
     */
    fun follow() {
        addDisposable(UserRepository.instance.follow(data.user, followState))
    }

    /**
     * 進(jìn)入用戶詳情頁
     */
    fun goDetail() {
        Router.goUserDetail(data.user)
    }
}
// 這是具體實(shí)現(xiàn)
fun follow(user: User, loadState: ObservableField<LoadState>, @Restrict restrict: String = Restrict.PUBLIC): Disposable? {
        if (loadState.get() !is LoadState.Loading) {
            val followed = user.is_followed
            return instance
                    .follow(user.id, !followed, restrict)
                    .doOnSubscribe { loadState.set(LoadState.Loading) }
                    .subscribeBy(onNext = {
                        user.is_followed = !followed
                        loadState.set(LoadState.Succeed)
                    }, onError = {
                        loadState.set(LoadState.Failed(it))
                    })
        }
        return null
    }

主要定義了兩個(gè)用于綁定點(diǎn)擊事件的方法,然后還有一個(gè)followState變量用于記錄網(wǎng)絡(luò)請求的狀態(tài),在點(diǎn)擊關(guān)注按鈕以后禁用它(android:enabled="@{!(vm.followState instanceof LoadState.Loading)}")防止重復(fù)點(diǎn)擊,直到請求完成。LoadState是我定義的一個(gè)密封類用于記錄狀態(tài)。

sealed class LoadState {
    object Idle : LoadState()
    object Loading : LoadState()
    object Succeed : LoadState()
    class Failed(val error: Throwable) : LoadState()
}

用戶item綁定了itemViewModel的點(diǎn)擊事件,那么我們就不用再給列表頁的recyclerView設(shè)置item點(diǎn)擊事件了,每個(gè)item的事件自己處理。
當(dāng)然并不是一定要把item的數(shù)據(jù)再封裝一層到ViewModel里面,你也可以直接使用list bean作為item xml的數(shù)據(jù),這都取決于你的業(yè)務(wù)復(fù)雜程度。

接下來我們看一下列表頁自己的Fragment和ViewModel

class UserListFragment : FragmentationFragment<CommonRefreshList, UserListViewModel?>() {

    override var mViewModel: UserListViewModel? = null

    companion object {
        fun newInstance() = UserListFragment()
    }

    private lateinit var layoutManager: LinearLayoutManager
    private lateinit var mAdapter: UserPreviewAdapter

    override fun init(savedInstanceState: Bundle?) {
        initList()
    }

    override fun onLazyInitView(savedInstanceState: Bundle?) {
        super.onLazyInitView(savedInstanceState)
        mViewModel?.load()
    }

    /**
     * 初始化列表
     */
    private fun initList() {
        with(mBinding) {
            mViewModel?.let {
                vm ->
                mAdapter = UserPreviewAdapter(vm.data)
                layoutManager = LinearLayoutManager(context)
                recyclerView.layoutManager = layoutManager
                mAdapter.bindToRecyclerView(recyclerView)
                // 加載更多
                recyclerView.attachLoadMore(vm.loadMoreState) { vm.loadMore() }

                mAdapter.bindState(vm.loadState,  refreshLayout = refreshLayout) {
                    vm.load()
                }
            }
        }
    }

    override fun immersionBarEnabled(): Boolean = false

    override fun bindLayout(): Int = R.layout.layout_common_refresh_recycler

}
class UserListViewModel(var action: (suspend () -> UserPreviewListResp)) : BaseViewModel() {

    // 列表數(shù)據(jù)
    val data: ObservableList<UserItemViewModel> = ObservableArrayList()

    // 加載數(shù)據(jù)狀態(tài)
    var loadState: ObservableField<LoadState> = ObservableField(LoadState.Idle)

    var loadMoreState: ObservableField<LoadState> = ObservableField(LoadState.Idle)

    var nextUrl = ""

    // 加載數(shù)據(jù)
    fun load() {
        launch(CoroutineExceptionHandler { _, err ->
            loadState.set(LoadState.Failed(err))
        }) {
            loadState.set(LoadState.Loading)
            val resp = withContext(Dispatchers.IO) {
                action.invoke()
            }
            if (resp.user_previews.isEmpty()) {
                throw ApiException(ApiException.CODE_EMPTY_DATA)
            }
            data.clear()
            // user bean轉(zhuǎn)換為itemViewModel
            data.addAll(resp.user_previews.map { UserItemViewModel(this@UserListViewModel, it) })
            nextUrl = resp.next_url
            loadState.set(LoadState.Succeed)
        }
    }

    // 加載更多
    fun loadMore() {
        if (nextUrl.isBlank())
            return
        launch(CoroutineExceptionHandler { _, err ->
            loadMoreState.set(LoadState.Failed(err))
        }) {
            loadMoreState.set(LoadState.Loading)
            val resp = withContext(Dispatchers.IO) {
                UserRepository.instance
                        .loadMore(nextUrl)
            }
            // user bean轉(zhuǎn)換為itemViewModel
            data.addAll(resp.user_previews.map { UserItemViewModel(this@UserListViewModel, it) })
            nextUrl = resp.next_url
            loadMoreState.set(LoadState.Succeed)
        }
    }

}

代碼非常簡單,F(xiàn)ragment中僅僅給recyclerView綁定了adapter,ViewModel請求網(wǎng)絡(luò)然后轉(zhuǎn)換了一下數(shù)據(jù)裝入ObservableList更新ui,adapter中已經(jīng)監(jiān)聽了observableList中的數(shù)據(jù)變化。細(xì)節(jié)代碼并不重要,這里網(wǎng)絡(luò)請求使用的是協(xié)程方式,可以隨意替換成別的方式。對協(xié)程有興趣可以參考這系列文章

在這個(gè)例子中我們在fragment中幾乎沒有干任何事情,它只是當(dāng)了一回工具人,用來初始化視圖。視圖綁定值在xml文件中通過引用ViewModel中的數(shù)據(jù)完成,ViewModel作為數(shù)據(jù)的容器,并保存一些狀態(tài)和事件函數(shù),將它們綁定起來以后DataBinding通過設(shè)置回調(diào)函數(shù)監(jiān)聽ViewModel中數(shù)據(jù)的變化更新ui。代碼被很好的分離開了,數(shù)據(jù)和視圖彼此分離,僅通過DataBinding建立橋梁,更易于移植代碼。

3. 復(fù)雜一些的場景

這里以一個(gè)作品詳情頁為例,它看起來像下面這個(gè)樣子。

detail.gif

可以看到整個(gè)頁面包含內(nèi)容比較多,而且底部dialog和主界面有部分相同的ui,這時(shí)候我們應(yīng)該適當(dāng)將頁面劃分為幾部分,抽象出一些子ViewModel,分開處理業(yè)務(wù)邏輯,相同的界面也可以組裝復(fù)用。
拆分出來的布局
parts.png

詳情頁整個(gè)界面都裝載在一個(gè)RecyclerView中,拆出了描述、用戶信息、評論等幾個(gè)部分,通過item的方式插入進(jìn)去,同時(shí)在底部dialog中將它們組裝到一個(gè)scrollView中達(dá)成xml的復(fù)用。

詳情頁ViewModel簡略代碼如下,它持有幾個(gè)子ViewModel。

open class DetailViewModel : BaseViewModel() {
    @get: Bindable
    var illust = Illust()
    set(value) {
        field = value
        relatedUserViewModel.user = value.user
        commentListViewModel.illust = value
        notifyPropertyChanged(BR.illust)
    }

    open var loadState: ObservableField<LoadState> = ObservableField(LoadState.Idle)

    // 收藏狀態(tài)
    var starState: ObservableField<LoadState> = ObservableField(LoadState.Idle)

    // 用戶信息vm
    val userFooterViewModel = UserFooterViewModel(this)
    // 評論列表vm
    val commentListViewModel = CommentListViewModel()
    // 相關(guān)作品vm
    val relatedIllustViewModel = RelatedIllustDialogViewModel(this)
    // 相關(guān)用戶vm
    val relatedUserViewModel = RelatedUserDialogViewModel(illust.user)
    // 作品系列vm
    open val seriesItemViewModel: SeriesItemViewModel? = null

    init {
        this + userFooterViewModel + commentListViewModel + relatedIllustViewModel + relatedUserViewModel
        ......
    }

    /**
     * 收藏/取消收藏
     */
    fun star() {
        val disposable = IllustRepository.instance
                .star(liveData, starState)
        disposable?.let {
            addDisposable(it)
        }
    }

    ......
}

同時(shí)底部dialog和詳情頁直接共用DetailViewModel,幾個(gè)子布局則通過include的方式組裝進(jìn)dialog的布局,代碼如下

val bottomDialog = AboutDialogFragment.newInstance().apply {
                    // 將詳情頁vm賦值給dialog
                    detailViewModel = mViewModel
                }
<--  dialog_detail_bottom.xml -->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <import type="android.view.View" />

        <import type="com.lyj.fakepixiv.module.common.DetailViewModel" />

        <import type="com.lyj.fakepixiv.module.illust.detail.comment.InputViewModel.State" />

        <variable
            name="vm"
            type="DetailViewModel" />
    </data>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <com.lyj.fakepixiv.widget.StaticScrollView
            android:id="@+id/scrollView"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:background="@color/white"
                android:orientation="vertical">

                <include
                    android:id="@+id/caption"
                    layout="@layout/layout_detail_caption"
                    app:showCaption="@{true}"
                    app:vm="@{vm}" />

                <!-- 作品介紹 -->
                <include
                    android:id="@+id/desc_container"
                    layout="@layout/layout_detail_desc"
                    app:data="@{vm.illust}" />

                <include
                    android:id="@+id/series_container"
                    layout="@layout/detail_illust_series"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:visibility="@{vm.illust.series != null ? View.VISIBLE : View.GONE}"
                    app:vm="@{vm.seriesItemViewModel}" />

                <!-- 用戶信息 -->
                <include
                    android:id="@+id/user_container"
                    layout="@layout/layout_detail_user"
                    app:vm="@{vm.userFooterViewModel}" />

                <!-- 評論 -->
                <include
                    android:id="@+id/comment_container"
                    layout="@layout/layout_detail_comment"
                    app:vm="@{vm.commentListViewModel}" />
            </LinearLayout>
        </com.lyj.fakepixiv.widget.StaticScrollView>
        ......
    </RelativeLayout>
</layout>

需要注意的是include需要給予id
然后只需要將各個(gè)子ViewModel綁定到視圖,完成子vm中的業(yè)務(wù)邏輯,同時(shí)請求網(wǎng)絡(luò)獲取數(shù)據(jù),再加一點(diǎn)細(xì)節(jié),兩個(gè)頁面就都完成了。

在此mvvm的好處就體現(xiàn)出來了,頁面拆分組裝更加靈活,而且通過共用ViewModel,兩個(gè)頁面還可以同步狀態(tài),只需要定義一個(gè)狀態(tài)變量,在xml表達(dá)式中都使用它來表示ui狀態(tài)就行了,做到一份數(shù)據(jù)同時(shí)驅(qū)動(dòng)兩個(gè)頁面

4. 結(jié)構(gòu)優(yōu)化

我的項(xiàng)目中搭建的mvvm還存在一些問題

  • 不同頁面共用ViewModel的問題

    由于我的項(xiàng)目是由單Activity多Fragment組成,所以可以通過拿到Fragment的實(shí)例直接為它的ViewModel賦值達(dá)到共用(這樣在fragment重建的時(shí)候可能會(huì)有問題)。而如果你的應(yīng)用是多Activity組成,Activity之間如何共用ViewModel呢?我的思路是設(shè)計(jì)一個(gè)類似Activity棧的ViewModel棧,每啟動(dòng)一個(gè)頁面就把它對應(yīng)的ViewModel壓入棧中,頁面銷毀時(shí)出棧,在別的Activity中通過Class和一個(gè)自定義的key值獲取ViewModel實(shí)例。

  • 組件選擇問題

    我的項(xiàng)目中并沒有用Android JetPack中的ViewModel和LiveData,這些都是可選的,用不用取決于你,具體的組件都是根據(jù)抽象的概念具現(xiàn)化出來的東西,不必太過糾結(jié)這些。不過要注意的是DataBinding對于LiveData的支持需要將編譯處理器升級為V2版本,在gradle.properties文件加入android.databinding.enableV2=true

整篇文章其實(shí)我寫得比較簡單,略過了不少東西,一方面的確是我本人表達(dá)能力堪憂,另一方面也是覺得看代碼可能更加直觀,大家不妨去看代碼更好。
項(xiàng)目是一個(gè)仿P站android客戶端,需要科學(xué)上網(wǎng)才可正常連接服務(wù)器使用

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