6. Jetpack---Paging你知道怎樣上拉加載嗎鸵膏?

之前的幾篇源碼分析我們分別對(duì)Navigation租副、LifecyclesViewModel较性、LiveData用僧、進(jìn)行了分析,也對(duì)JetPack有了更深入的了解赞咙。但是Jetpack遠(yuǎn)不止這些組件责循,今天的主角---Paging,Jetpack中的分頁(yè)組件攀操,官方是這么形容它的:‘’逐步從您的數(shù)據(jù)源按需加載信息‘’

如果你對(duì)Jetpack組件有了解或者想對(duì)源碼有更深入的了解院仿,請(qǐng)看我之前的幾篇文章:

1. Jetpack源碼解析---看完你就知道Navigation是什么了?

2. Jetpack源碼解析---Navigation為什么切換Fragment會(huì)重繪速和?

3. Jetpack源碼解析---用Lifecycles管理生命周期

4. Jetpack源碼解析—LiveData的使用及工作原理

5. Jetpack源碼解析---ViewModel基本使用及源碼解析

1. 背景

在我的Jetpack_Note系列中歹垫,對(duì)每一篇的分析都有相對(duì)應(yīng)的代碼片段及使用,我把它做成了一個(gè)APP颠放,目前功能還不完善排惨,代碼我也上傳到了GitHub上,參考了官方的Demo以及目前網(wǎng)上的一些文章碰凶,有興趣的小伙伴可以看一下暮芭,別忘了給個(gè)Star。

https://github.com/Hankkin/JetPack_Note

今天我們的主角是Paging欲低,介紹之前我們先看一下效果:

19-08-10-11-18-59.2019-08-10 11_33_51

2. 簡(jiǎn)介

2.1 基本介紹

官方定義:

分頁(yè)庫(kù)Pagin LibraryJetpack的一部分辕宏,它可以妥善的逐步加載數(shù)據(jù),幫助您一次加載和顯示一部分?jǐn)?shù)據(jù)砾莱,這樣的按需加載可以減少網(wǎng)絡(luò)貸款和系統(tǒng)資源的使用瑞筐。分頁(yè)庫(kù)支持加載有限以及無(wú)限的list,比如一個(gè)持續(xù)更新的信息源腊瑟,分頁(yè)庫(kù)可以與RecycleView無(wú)縫集合聚假,它還可以與LiveData或RxJava集成块蚌,觀察界面中的數(shù)據(jù)變化。

image

2.2 核心組件

1. PagedList

PageList是一個(gè)集合類(lèi)魔策,它以分塊的形式異步加載數(shù)據(jù),每一塊我們稱(chēng)之為頁(yè)河胎。它繼承自AbstractList,支持所有List的操作闯袒,它的內(nèi)部有五個(gè)主要變量:

  1. mMainThreadExecutor 主線程Executor,用于將結(jié)果傳遞到主線程
  2. mBackgroundThreadExecutor 后臺(tái)線程游岳,執(zhí)行負(fù)載業(yè)務(wù)邏輯
  3. BoundaryCallback 當(dāng)界面顯示緩存中靠近結(jié)尾的數(shù)據(jù)的時(shí)候政敢,它將加載更多的數(shù)據(jù)
  4. Config PageList從DataSource中加載數(shù)據(jù)的配置
  5. PagedStorage<T> 用于存儲(chǔ)加載到的數(shù)據(jù)

Config屬性:

  1. pageSize:分頁(yè)加載的數(shù)量
  2. prefetchDistance:預(yù)加載的數(shù)量
  3. initialLoadSizeHint:初始化數(shù)據(jù)時(shí)加載的數(shù)量,默認(rèn)為pageSize*3
  4. enablePlaceholders:當(dāng)item為null是否使用placeholder顯示

PageList會(huì)通過(guò)DataSource加載數(shù)據(jù)胚迫,通過(guò)Config的配置喷户,可以設(shè)置一次加載的數(shù)量以及預(yù)加載的數(shù)量。除此之外访锻,PageList還可以想RecycleView.Adapter發(fā)送更新的信號(hào)褪尝,驅(qū)動(dòng)UI的刷新。

2. DataSource

DataSource<Key,Value> 顧名思義就是數(shù)據(jù)源期犬,它是一個(gè)抽象類(lèi)河哑,其中Key對(duì)應(yīng)加載數(shù)據(jù)的條件信息,Value對(duì)應(yīng)加載數(shù)據(jù)的實(shí)體類(lèi)龟虎。Paging庫(kù)中提供了三個(gè)子類(lèi)來(lái)讓我們?cè)诓煌瑘?chǎng)景的情況下使用:

  1. PageKeyedDataSource:如果后端API返回?cái)?shù)據(jù)是分頁(yè)之后的璃谨,可以使用它;例如:官方Demo中GitHub API中的SearchRespositories就可以返回分頁(yè)數(shù)據(jù)鲤妥,我們?cè)贕itHub API的請(qǐng)求中制定查詢(xún)關(guān)鍵字和想要的哪一頁(yè)佳吞,同時(shí)也可以指明每個(gè)頁(yè)面的項(xiàng)數(shù)。
  2. ItemKeyedDataSource:如果通過(guò)鍵值請(qǐng)求后端數(shù)據(jù)棉安;例如我們需要獲取在某個(gè)特定日期起Github的前100項(xiàng)代碼提交記錄底扳,該日期將成為DataSource的鍵,ItemKeyedDataSource允許自定義如何加載初始頁(yè);該場(chǎng)景多用于評(píng)論信息等類(lèi)似請(qǐng)求
  3. PositionalDataSource:適用于目標(biāo)數(shù)據(jù)總數(shù)固定贡耽,通過(guò)特定的位置加載數(shù)據(jù)花盐,這里Key是Integer類(lèi)型的位置信息,T即Value菇爪。 比如從數(shù)據(jù)庫(kù)中的1200條開(kāi)始加在20條數(shù)據(jù)算芯。

3. PagedListAdapter

PageListAdapter繼承自RecycleView.Adapter,和RecycleView實(shí)現(xiàn)方式一樣,當(dāng)數(shù)據(jù)加載完畢時(shí)凳宙,通知RecycleView數(shù)據(jù)加載完畢熙揍,RecycleView填充數(shù)據(jù);當(dāng)數(shù)據(jù)發(fā)生變化時(shí)氏涩,PageListAdapter會(huì)接受到通知届囚,交給委托類(lèi)AsyncPagedListDiffer來(lái)處理有梆,AsyncPagedListDiffer是對(duì)DiffUtil.ItemCallback<T>持有對(duì)象的委托類(lèi),AsyncPagedListDiffer使用后臺(tái)線程來(lái)計(jì)算PagedList的改變,item是否改變,由DiffUtil.ItemCallback<T>決定。

3.基本使用

3.1 添加依賴(lài)包

implementation "androidx.paging:paging-runtime:$paging_version" // For Kotlin use paging-runtime-ktx
    implementation "androidx.paging:paging-runtime-ktx:$paging_version" // For Kotlin use paging-runtime-ktx
    // alternatively - without Android dependencies for testing
    testImplementation "androidx.paging:paging-common:$paging_version" // For Kotlin use paging-common-ktx
    // optional - RxJava support
    implementation "androidx.paging:paging-rxjava2:$paging_version" // For Kotlin use paging-rxjava2-ktx

3.2 PagingWithRoom使用

新建UserDao

/**
 * created by Hankkin
 * on 2019-07-19
 */
@Dao
interface UserDao {


    @Query("SELECT * FROM User ORDER BY name COLLATE NOCASE ASC")
    fun queryUsersByName(): DataSource.Factory<Int, User>

    @Insert
    fun insert(users: List<User>)

    @Insert
    fun insert(user: User)

    @Delete
    fun delete(user: User)

}

創(chuàng)建UserDB數(shù)據(jù)庫(kù)

/**
 * created by Hankkin
 * on 2019-07-19
 */
@Database(entities = arrayOf(User::class), version = 1)
abstract class UserDB : RoomDatabase() {

    abstract fun userDao(): UserDao
    companion object {
        private var instance: UserDB? = null
        @Synchronized
        fun get(context: Context): UserDB {
            if (instance == null) {
                instance = Room.databaseBuilder(context.applicationContext,
                    UserDB::class.java, "UserDatabase")
                    .addCallback(object : RoomDatabase.Callback() {
                        override fun onCreate(db: SupportSQLiteDatabase) {
                            fillInDb(context.applicationContext)
                        }
                    }).build()
            }
            return instance!!
        }

        /**
         * fill database with list of cheeses
         */
        private fun fillInDb(context: Context) {
            // inserts in Room are executed on the current thread, so we insert in the background
            ioThread {
                get(context).userDao().insert(
                    CHEESE_DATA.map { User(id = 0, name = it) })
            }
        }
    }
}

創(chuàng)建PageListAdapter

/**
 * created by Hankkin
 * on 2019-07-19
 */
class PagingDemoAdapter : PagedListAdapter<User, PagingDemoAdapter.ViewHolder>(diffCallback) {

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
        ViewHolder(AdapterPagingItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val item = getItem(position)
        holder.apply {
            bind(createOnClickListener(item), item)
            itemView.tag = item
        }
    }

    private fun createOnClickListener(item: User?): View.OnClickListener {
        return View.OnClickListener {
            Toast.makeText(it.context, item?.name, Toast.LENGTH_SHORT).show()
        }
    }


    class ViewHolder(private val binding: AdapterPagingItemBinding) : RecyclerView.ViewHolder(binding.root) {
        fun bind(listener: View.OnClickListener, item: User?) {
            binding.apply {
                clickListener = listener
                user = item
                executePendingBindings()
            }
        }
    }

    companion object {
        /**
         * This diff callback informs the PagedListAdapter how to compute list differences when new
         * PagedLists arrive.
         * <p>
         * When you add a Cheese with the 'Add' button, the PagedListAdapter uses diffCallback to
         * detect there's only a single item difference from before, so it only needs to animate and
         * rebind a single view.
         *
         * @see android.support.v7.util.DiffUtil
         */
        private val diffCallback = object : DiffUtil.ItemCallback<User>() {
            override fun areItemsTheSame(oldItem: User, newItem: User): Boolean =
                oldItem.id == newItem.id

            /**
             * Note that in kotlin, == checking on data classes compares all contents, but in Java,
             * typically you'll implement Object#equals, and use it to compare object contents.
             */
            override fun areContentsTheSame(oldItem: User, newItem: User): Boolean =
                oldItem == newItem
        }
    }
}

ViewModel承載數(shù)據(jù)

class PagingWithDaoViewModel internal constructor(private val pagingRespository: PagingRespository) : ViewModel() {

    val allUsers = pagingRespository.getAllUsers()

    fun insert(text: CharSequence) {
        pagingRespository.insert(text)
    }

    fun remove(user: User) {
        pagingRespository.remove(user)
    }
}

Activity中觀察到數(shù)據(jù)源的變化后意系,會(huì)通知Adapter自動(dòng)更新數(shù)據(jù)

class PagingWithDaoActivity : AppCompatActivity() {

    private lateinit var viewModel: PagingWithDaoViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_paging_with_dao)
        setLightMode()
        setupToolBar(toolbar) {
            title = resources.getString(R.string.paging_with_dao)
            setDisplayHomeAsUpEnabled(true)
        }
        viewModel = obtainViewModel(PagingWithDaoViewModel::class.java)
        val adapter = PagingDemoAdapter()
        rv_paging.adapter = adapter
        viewModel.allUsers.observe(this, Observer(adapter::submitList))
    }


    override fun onOptionsItemSelected(item: MenuItem?): Boolean {
        when (item?.itemId) {
            android.R.id.home -> finish()
        }
        return super.onOptionsItemSelected(item)
    }
}

3.3 PagingWithNetWork 使用

上面我們通過(guò)Room進(jìn)行了數(shù)據(jù)庫(kù)加載數(shù)據(jù)泥耀,下面看一下通過(guò)網(wǎng)絡(luò)請(qǐng)求記載列表數(shù)據(jù):

和上面不同的就是Respository數(shù)據(jù)源的加載,之前我們是通過(guò)Room加載DB數(shù)據(jù)蛔添,現(xiàn)在我們要通過(guò)網(wǎng)絡(luò)獲取數(shù)據(jù):

GankRespository 干貨數(shù)據(jù)源倉(cāng)庫(kù)

/**
 * created by Hankkin
 * on 2019-07-30
 */
class GankRespository {

    companion object {

        private const val PAGE_SIZE = 20

        @Volatile
        private var instance: GankRespository? = null

        fun getInstance() =
            instance ?: synchronized(this) {
                instance
                    ?: GankRespository().also { instance = it }
            }
    }

    fun getGank(): Listing<Gank> {
        val sourceFactory = GankSourceFactory()
        val config = PagedList.Config.Builder()
            .setPageSize(PAGE_SIZE)
            .setInitialLoadSizeHint(PAGE_SIZE * 2)
            .setEnablePlaceholders(false)
            .build()
        val livePageList = LivePagedListBuilder<Int, Gank>(sourceFactory, config).build()
        val refreshState = Transformations.switchMap(sourceFactory.sourceLiveData) { it.initialLoad }
        return Listing(
            pagedList = livePageList,
            networkState = Transformations.switchMap(sourceFactory.sourceLiveData) { it.netWorkState },
            retry = { sourceFactory.sourceLiveData.value?.retryAllFailed() },
            refresh = { sourceFactory.sourceLiveData.value?.invalidate() },
            refreshState = refreshState
        )
    }

}

可以看到getGank()方法返回了Listing<Gank>,那么Listing是個(gè)什么呢痰催?

/**
 * Data class that is necessary for a UI to show a listing and interact w/ the rest of the system
 * 封裝需要監(jiān)聽(tīng)的對(duì)象和執(zhí)行的操作,用于上拉下拉操作
 * pagedList : 數(shù)據(jù)列表
 * networkState : 網(wǎng)絡(luò)狀態(tài)
 * refreshState : 刷新?tīng)顟B(tài)
 * refresh : 刷新操作
 * retry : 重試操作
 */
data class Listing<T>(
        // the LiveData of paged lists for the UI to observe
    val pagedList: LiveData<PagedList<T>>,
        // represents the network request status to show to the user
    val networkState: LiveData<NetworkState>,
        // represents the refresh status to show to the user. Separate from networkState, this
        // value is importantly only when refresh is requested.
    val refreshState: LiveData<NetworkState>,
        // refreshes the whole data and fetches it from scratch.
    val refresh: () -> Unit,
        // retries any failed requests.
    val retry: () -> Unit)

Listing是我們封裝的一個(gè)數(shù)據(jù)類(lèi)迎瞧,將數(shù)據(jù)源夸溶、網(wǎng)絡(luò)狀態(tài)、刷新?tīng)顟B(tài)凶硅、下拉刷新操作以及重試操作都封裝進(jìn)去了缝裁。那么我們的數(shù)據(jù)源從哪里獲取呢,可以看到Listing的第一個(gè)參數(shù)pageList = livePageList,livePageList通過(guò)LivePagedListBuilder創(chuàng)建足绅,LivePagedListBuilder需要兩個(gè)參數(shù)(DataSource,PagedList.Config):

GankSourceFactory

?

/**
 * created by Hankkin
 * on 2019-07-30
 */
class GankSourceFactory(private val api: Api = Injection.provideApi()) : DataSource.Factory<Int, Gank>(){

    val sourceLiveData = MutableLiveData<GankDataSource>()

    override fun create(): DataSource<Int, Gank> {
        val source = GankDataSource(api)
        sourceLiveData.postValue(source)
        return  source
    }
}

GankDataSource


/**
 * created by Hankkin
 * on 2019-07-30
 */
class GankDataSource(private val api: Api = Injection.provideApi()) : PageKeyedDataSource<Int, Gank>() {

    private var retry: (() -> Any)? = null
    val netWorkState = MutableLiveData<NetworkState>()
    val initialLoad = MutableLiveData<NetworkState>()

    fun retryAllFailed() {
        val prevRetry = retry
        retry = null
        prevRetry?.also { it.invoke() }
    }

    override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Gank>) {
        initialLoad.postValue(NetworkState.LOADED)
        netWorkState.postValue(NetworkState.HIDDEN)
        api.getGank(params.requestedLoadSize, 1)
            .enqueue(object : Callback<GankResponse> {
                override fun onFailure(call: Call<GankResponse>, t: Throwable) {
                    retry = {
                        loadInitial(params, callback)
                    }
                    initialLoad.postValue(NetworkState.FAILED)
                }

                override fun onResponse(call: Call<GankResponse>, response: Response<GankResponse>) {
                    if (response.isSuccessful) {
                        retry = null
                        callback.onResult(
                            response.body()?.results ?: emptyList(),
                            null,
                            2
                        )
                        initialLoad.postValue(NetworkState.LOADED)
                    } else {
                        retry = {
                            loadInitial(params, callback)
                        }
                        initialLoad.postValue(NetworkState.FAILED)
                    }
                }

            })
    }

    override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Gank>) {
        netWorkState.postValue(NetworkState.LOADING)
        api.getGank(params.requestedLoadSize, params.key)
            .enqueue(object : Callback<GankResponse> {
                override fun onFailure(call: Call<GankResponse>, t: Throwable) {
                    retry = {
                        loadAfter(params, callback)
                    }
                    netWorkState.postValue(NetworkState.FAILED)
                }

                override fun onResponse(call: Call<GankResponse>, response: Response<GankResponse>) {
                    if (response.isSuccessful) {
                        retry = null
                        callback.onResult(
                            response.body()?.results ?: emptyList(),
                            params.key + 1
                        )
                        netWorkState.postValue(NetworkState.LOADED)
                    } else {
                        retry = {
                            loadAfter(params, callback)
                        }
                        netWorkState.postValue(NetworkState.FAILED)
                    }
                }

            })
    }

    override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Gank>) {
    }


}

網(wǎng)絡(luò)請(qǐng)求的核心代碼在GankDataSource中捷绑,因?yàn)槲覀兊恼?qǐng)求是分頁(yè)請(qǐng)求,所以這里的GankDataSource我們繼承自PageKeyedDataSource,它實(shí)現(xiàn)了三個(gè)方法:

loadInitial: 初始化加載氢妈,初始加載的數(shù)據(jù) 也就是我們直接能看見(jiàn)的數(shù)據(jù)

loadAfter: 下一頁(yè)加載胎食,每次傳遞的第二個(gè)參數(shù) 就是 你加載數(shù)據(jù)依賴(lài)的key

loadBefore: 往上滑加載的數(shù)據(jù)

可以看到我們?cè)?code>loadInitial中設(shè)置了initialLoadnetWorkState的狀態(tài)值,同時(shí)通過(guò)RetrofitApi獲取網(wǎng)絡(luò)數(shù)據(jù)允懂,并在成功和失敗的回調(diào)中對(duì)數(shù)據(jù)和網(wǎng)絡(luò)狀態(tài)值以及加載初始化做了相關(guān)的設(shè)置厕怜,具體就不介紹了,可看代碼蕾总。loadAfter同理粥航,只不過(guò)我們?cè)诩虞d數(shù)據(jù)后對(duì)key也就是我們的page進(jìn)行了+1操作。

Config參數(shù)就是我們對(duì)分頁(yè)加載的一些配置:

val config = PagedList.Config.Builder()
            .setPageSize(PAGE_SIZE)
            .setInitialLoadSizeHint(PAGE_SIZE * 2)
            .setEnablePlaceholders(false)
            .build()

下面看我們?cè)贏ctivity中怎樣使用:

PagingWithNetWorkActivity

class PagingWithNetWorkActivity : AppCompatActivity() {

    private lateinit var mViewModel: PagingWithNetWorkViewModel
    private lateinit var mDataBinding: ActivityPagingWithNetWorkBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mDataBinding = DataBindingUtil.setContentView(this,R.layout.activity_paging_with_net_work)
        setLightMode()
        setupToolBar(toolbar) {
            title = resources.getString(R.string.paging_with_network)
            setDisplayHomeAsUpEnabled(true)
        }
        mViewModel = obtainViewModel(PagingWithNetWorkViewModel::class.java)
        mDataBinding.vm = mViewModel
        mDataBinding.lifecycleOwner = this

        val adapter = PagingWithNetWorkAdapter()
        mDataBinding.rvPagingWithNetwork.adapter = adapter
        mDataBinding.vm?.gankList?.observe(this, Observer { adapter.submitList(it) })
        mDataBinding.vm?.refreshState?.observe(this, Observer {
            mDataBinding.rvPagingWithNetwork.post {
                mDataBinding.swipeRefresh.isRefreshing = it == NetworkState.LOADING
            }
        })

        mDataBinding.vm?.netWorkState?.observe(this, Observer {
            adapter.setNetworkState(it)
        })
    }

    override fun onOptionsItemSelected(item: MenuItem?): Boolean {
        when (item?.itemId) {
            android.R.id.home -> finish()
        }
        return super.onOptionsItemSelected(item)
    }
}

ViewModel中的gankList是一個(gè)LiveData生百,所以我們?cè)谶@里給它設(shè)置一個(gè)觀察递雀,當(dāng)數(shù)據(jù)變動(dòng)是調(diào)用adapter.submitList(it),刷新數(shù)據(jù),這個(gè)方法是PagedListAdapter中的蚀浆,里面回去檢查新數(shù)據(jù)和舊數(shù)據(jù)是否相同缀程,也就是上面我們提到的AsyncPagedListDiffer來(lái)實(shí)現(xiàn)的。到這里整個(gè)流程就已經(jīng)結(jié)束了市俊,想看源碼可以到Github上杨凑。

4. 總結(jié)

我們先看下官網(wǎng)給出的gif圖:

image

總結(jié)一下,Paging的基本原理為:

  1. 使用DataSource從網(wǎng)絡(luò)或者數(shù)據(jù)庫(kù)獲取數(shù)據(jù)
  2. 將數(shù)據(jù)保存到PageList中
  3. 將PageList中的數(shù)據(jù)提交給PageListAdapter
  4. PageListAdapter在后臺(tái)線程中通過(guò)Diff對(duì)比新老數(shù)據(jù)摆昧,反饋到RecycleView中
  5. RecycleView刷新數(shù)據(jù)

基本原理在圖上我們可以很清晰的了解到了撩满,本篇文章的Demo中結(jié)合了ViewModel以及DataBinding進(jìn)行了數(shù)據(jù)的存儲(chǔ)和綁定。

最后代碼地址:

https://github.com/Hankkin/JetPack_Note

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市伺帘,隨后出現(xiàn)的幾起案子昭躺,更是在濱河造成了極大的恐慌,老刑警劉巖伪嫁,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件领炫,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡张咳,警方通過(guò)查閱死者的電腦和手機(jī)帝洪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)晶伦,“玉大人碟狞,你說(shuō)我怎么就攤上這事啄枕』榕悖” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵频祝,是天一觀的道長(zhǎng)泌参。 經(jīng)常有香客問(wèn)我,道長(zhǎng)常空,這世上最難降的妖魔是什么沽一? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮漓糙,結(jié)果婚禮上铣缠,老公的妹妹穿的比我還像新娘。我一直安慰自己昆禽,他們只是感情好蝗蛙,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著醉鳖,像睡著了一般捡硅。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上盗棵,一...
    開(kāi)封第一講書(shū)人閱讀 52,262評(píng)論 1 308
  • 那天壮韭,我揣著相機(jī)與錄音,去河邊找鬼纹因。 笑死喷屋,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的瞭恰。 我是一名探鬼主播逼蒙,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了是牢?” 一聲冷哼從身側(cè)響起僵井,我...
    開(kāi)封第一講書(shū)人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎驳棱,沒(méi)想到半個(gè)月后批什,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡社搅,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年驻债,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片形葬。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡合呐,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出笙以,到底是詐尸還是另有隱情淌实,我是刑警寧澤,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布猖腕,位于F島的核電站拆祈,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏倘感。R本人自食惡果不足惜放坏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望老玛。 院中可真熱鬧淤年,春花似錦、人聲如沸蜡豹。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)余素。三九已至豹休,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間桨吊,已是汗流浹背威根。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留视乐,地道東北人洛搀。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像佑淀,于是被迫代替她去往敵國(guó)和親留美。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359

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