Kotlin 类委托(一):如何把一个列表页优化到十几行代码

相关文章

Kotlin 类委托(一):如何把一个列表页优化到十几行代码

Kotlin 类委托(二):实现原理及注意事项

痛点

​ 在之前,有用 玩AndroidAPI 写了一个 Demo 项目 SampleProject,初期开发完成之后开始着手进行优化,就突然发现 首页、项目、体系等 文章列表 数据结构相同、功能也相同,但是由于不同界面获取数据的接口不同,导致同样的代码写了很多遍,一个界面代码少说百来行,这样的重复低效肯定是不行的,必须要优化!

​ 这里贴上原有 ViewModel 代码:

/** 公众号文章列表 ViewModel,使用 [repository] 获取相关数据,进行网络请求 */
class BjnewsArticlesViewModel(
        private val repository: ArticlesRepository
) : BaseViewModel() {

    /** 公众号 id */
    var bjnewsId = ""

    /** 页码 */
    private var pageNumber: MutableLiveData<Int> = MutableLiveData()

    /** 文章列表返回数据 */
    private val articleListResultData: LiveData<NetResult<ArticleListEntity>> = pageNumber.switchMap { pageNum ->
        getBjnewsArticles(pageNum)
    }

    /** 文章列表数据 */
    val articleListData: LiveData<ArrayList<ArticleEntity>> = articleListResultData.map { result ->
        disposeArticleListResult(result)
    }

    /** 跳转 WebView 数据 */
    val jumpWebViewData = MutableLiveData<WebViewActivity.ActionModel>()

    /** 刷新状态 */
    val refreshing: MutableLiveData<SmartRefreshState> = MutableLiveData()

    /** 刷新回调 */
    val onRefresh: () -> Unit = {
        pageNumber.value = NET_PAGE_START
    }

    /** 加载更多状态 */
    val loadMore: MutableLiveData<SmartRefreshState> = MutableLiveData()

    /** 加载更多回调 */
    val onLoadMore: () -> Unit = {
        pageNumber.value = pageNumber.value.orElse(NET_PAGE_START) + 1
    }

    /** 文章列表的 `viewModel` 对象 */
    val articleListViewModel: ArticleListViewModel = object : ArticleListViewModel {

        /** 文章列表条目点击 */
        override val onArticleItemClick: (ArticleEntity) -> Unit = { item ->
            // 跳转 WebView 打开
            jumpWebViewData.value = WebViewActivity.ActionModel(item.id.orEmpty(), item.title.orEmpty(), item.link.orEmpty())
        }

        /** 文章收藏点击 */
        override val onArticleCollectClick: (ArticleEntity) -> Unit = { item ->
            if (item.collected.get().condition) {
                // 已收藏,取消收藏
                item.collected.set(false)
                unCollect(item)
            } else {
                // 未收藏,收藏
                item.collected.set(true)
                collect(item)
            }
        }
    }

    /** 获取公众号文章列表 */
    private fun getBjnewsArticles(pageNum: Int): LiveData<NetResult<ArticleListEntity>> {
        val result = MutableLiveData<NetResult<ArticleListEntity>>()
        viewModelScope.launch {
            try {
                // 获取文章列表数据
                result.value = repository.getBjnewsArticles(bjnewsId, pageNum)
            } catch (throwable: Throwable) {
                Logger.t("NET").e(throwable, "getBjnewsArticles")
                result.value = NetResult.fromThrowable(throwable)
            }
        }
        return result
    }

    /** 处理文章列表返回数据 [result],并返回文章列表 */
    private fun disposeArticleListResult(result: NetResult<ArticleListEntity>): ArrayList<ArticleEntity> {
        val refresh = pageNumber.value == NET_PAGE_START
        val smartControl = if (refresh) refreshing else loadMore
        return if (result.success()) {
            smartControl.value = SmartRefreshState(loading = false, success = true, noMore = result.data?.over.toBoolean())
            articleListData.value.copy(result.data?.datas, refresh)
        } else {
            smartControl.value = SmartRefreshState(loading = false, success = false)
            articleListData.value.orEmpty()
        }
    }

    /** 收藏文章[item] */
    private fun collect(item: ArticleEntity) {
        viewModelScope.launch {
            try {
                // 收藏
                val result = repository.collectArticleInside(item.id.orEmpty())
                if (!result.success()) {
                    // 收藏失败,提示、回滚收藏状态
                    snackbarData.value = SnackbarModel(result.errorMsg)
                    item.collected.set(false)
                }
            } catch (throwable: Throwable) {
                Logger.t("NET").e(throwable, "collect")
                // 收藏失败,提示、回滚收藏状态
                snackbarData.value = SnackbarModel(throwable.showMsg)
                item.collected.set(false)
            }
        }
    }

    /** 取消收藏文章[item] */
    private fun unCollect(item: ArticleEntity) {
        viewModelScope.launch {
            try {
                // 取消收藏
                val result = repository.unCollectArticleList(item.id.orEmpty())
                if (!result.success()) {
                    // 取消收藏失败,提示、回滚收藏状态
                    snackbarData.value = SnackbarModel(result.errorMsg)
                    item.collected.set(true)
                }
            } catch (throwable: Throwable) {
                Logger.t("NET").e(throwable, "unCollect")
                // 取消收藏失败,提示、回滚收藏状态
                snackbarData.value = SnackbarModel(throwable.showMsg)
                item.collected.set(true)
            }
        }
    }
}

​ 上面的代码里,有很多元素都是重复的,比如 文章列表数据、刷新状态、收藏、取消收藏、文章点击事件等。

如何进行优化

​ 根据上面已有的条件,我们能很容易就看出一个方案,就是将公用逻辑抽取成基类,让各个列表界面继承,这就有了第一套优化方案。

方案一:抽取基类

​ 只需要将代码中的重复元素抽取出来,封装到基类里面,将有差异的方法抽象暴露出来,子类各自实现不就可以了吗?话不多说,直接上代码:

/** 文章列表 ViewModel 基类 */
abstract class BaseArticlesListViewModel(
    private val repository: ArticlesRepository
): BaseViewModel() {

    /** 页码 */
    private var pageNumber: MutableLiveData<Int> = MutableLiveData()

    /** 文章列表返回数据 */
    private val articleListResultData: LiveData<NetResult<ArticleListEntity>> = pageNumber.switchMap { pageNum ->
        getBjnewsArticles(pageNum)
    }

    /** 文章列表数据 */
    val articleListData: LiveData<ArrayList<ArticleEntity>> = articleListResultData.map { result ->
        disposeArticleListResult(result)
    }

    /** 跳转 WebView 数据 */
    val jumpWebViewData = MutableLiveData<WebViewActivity.ActionModel>()

    /** 刷新状态 */
    val refreshing: MutableLiveData<SmartRefreshState> = MutableLiveData()

    /** 刷新回调 */
    val onRefresh: () -> Unit = {
        pageNumber.value = NET_PAGE_START
    }

    /** 加载更多状态 */
    val loadMore: MutableLiveData<SmartRefreshState> = MutableLiveData()

    /** 加载更多回调 */
    val onLoadMore: () -> Unit = {
        pageNumber.value = pageNumber.value.orElse(NET_PAGE_START) + 1
    }

    /** 文章列表的 `viewModel` 对象 */
    val articleListViewModel: ArticleListViewModel = object : ArticleListViewModel {

        /** 文章列表条目点击 */
        override val onArticleItemClick: (ArticleEntity) -> Unit = { item ->
            // 跳转 WebView 打开
            jumpWebViewData.value = WebViewActivity.ActionModel(item.id.orEmpty(), item.title.orEmpty(), item.link.orEmpty())
        }

        /** 文章收藏点击 */
        override val onArticleCollectClick: (ArticleEntity) -> Unit = { item ->
            if (item.collected.get().condition) {
                // 已收藏,取消收藏
                item.collected.set(false)
                unCollect(item)
            } else {
                // 未收藏,收藏
                item.collected.set(true)
                collect(item)
            }
        }
    }

    /** 获取公众号文章列表 */
    private fun getBjnewsArticles(pageNum: Int): LiveData<NetResult<ArticleListEntity>> {
        val result = MutableLiveData<NetResult<ArticleListEntity>>()
        viewModelScope.launch {
            try {
                // 获取文章列表数据
                result.value = loadArticlesList(pageNum)
            } catch (throwable: Throwable) {
                Logger.t("NET").e(throwable, "getBjnewsArticles")
                result.value = NetResult.fromThrowable(throwable)
            }
        }
        return result
    }

    /** 处理文章列表返回数据 [result],并返回文章列表 */
    private fun disposeArticleListResult(result: NetResult<ArticleListEntity>): ArrayList<ArticleEntity> {
        val refresh = pageNumber.value == NET_PAGE_START
        val smartControl = if (refresh) refreshing else loadMore
        return if (result.success()) {
            smartControl.value = SmartRefreshState(loading = false, success = true, noMore = result.data?.over.toBoolean())
            articleListData.value.copy(result.data?.datas, refresh)
        } else {
            smartControl.value = SmartRefreshState(loading = false, success = false)
            articleListData.value.orEmpty()
        }
    }

    /** 收藏文章[item] */
    private fun collect(item: ArticleEntity) {
        viewModelScope.launch {
            try {
                // 收藏
                val result = repository.collectArticleInside(item.id.orEmpty())
                if (!result.success()) {
                    // 收藏失败,提示、回滚收藏状态
                    snackbarData.value = SnackbarModel(result.errorMsg)
                    item.collected.set(false)
                }
            } catch (throwable: Throwable) {
                Logger.t("NET").e(throwable, "collect")
                // 收藏失败,提示、回滚收藏状态
                snackbarData.value = SnackbarModel(throwable.showMsg)
                item.collected.set(false)
            }
        }
    }

    /** 取消收藏文章[item] */
    private fun unCollect(item: ArticleEntity) {
        viewModelScope.launch {
            try {
                // 取消收藏
                val result = repository.unCollectArticleList(item.id.orEmpty())
                if (!result.success()) {
                    // 取消收藏失败,提示、回滚收藏状态
                    snackbarData.value = SnackbarModel(result.errorMsg)
                    item.collected.set(true)
                }
            } catch (throwable: Throwable) {
                Logger.t("NET").e(throwable, "unCollect")
                // 取消收藏失败,提示、回滚收藏状态
                snackbarData.value = SnackbarModel(throwable.showMsg)
                item.collected.set(true)
            }
        }
    }
    
    /** 抽象暴露方法,子类实现,获取文章列表数据 */
    abstract suspend fun loadArticlesList(pageNum: Int): NetResult<ArticlesListEntity>
}

​ 在上面的基类基础上,我们能很简单的实现一个文章列表的 ViewModel

/** 公众号文章列表 ViewModel,使用 [repository] 获取相关数据,进行网络请求 */
class BjnewsArticlesViewModel(
        private val repository: ArticlesRepository
) : BaseArticlesListViewModel(repository) {

    /** 公众号 id */
    var bjnewsId = ""
    
    override suspend fun loadArticlesList(pageNum: Int): NetResult<ArticlesListEntity> {
        return repository.getBjnewsArticles(bjnewsId, pageNum)
    }
    
}

​ 这么一看已经达成了我标题的要求了,不是很简单吗?可是并不是所有界面都需要有收藏功能的,也并不是所有界面都需要做分页加载的,如果把不同功能拆分成接口,按照需要组装起来,即使是这样也还是要封装成好几个不同情况的基类,更别说我也不想把 ViewModel 的继承关系搞得太复杂,要是 能够同时继承多个类就好了

{% note info %}

没错,这里就到了我们这篇文章的重点,达到类似 同时继承多个类 的效果。

{% endnote %}

方案二:Kotlin 类委托

什么是类委托?

委托模式 已经证明是实现继承的一个很好的替代方式, 而 Kotlin 可以零样板代码地原生支持它。具体说明可以参考Kotlin中文

​ 简单来说,Kotlin 在语法层添加了对 委托模式 的支持,你可以简单的通过 by 关键字来实现,我们来看实际案例。

​ 以超市中的水果为例,我们定义一个水果接口,里面定义了获取水果的名称、外形、价格的方法

interface Fruit {
    /** 名称 */
    fun name(): String
    /** 外形 */
    fun shape(): String
    /** 价格 */
    fun price(): String
}

​ 然后超市里进了一批白心火龙果,我们定义一个类,继承水果接口 Fruit

class WhitePitaya: Fruit {
    override fun name(): String {
        return "白心火龙果"
    }
    override fun shape(): String {
        return "火龙果的形状"
    }
    override fun price(): String {
        return "12.8"
    }
}
val pitaya = WhitePitaya()
println("WhitePitaya={name=${pitaya.name()}, shape=${pitaya.shape()}, price=${pitaya.price()}}")
> WhitePitaya={name="白心火龙果", shape="火龙果的形状", price="12.8"}

​ 接下来超市里又来了一批红心火龙果,按照习惯的方式,我们一般会定义一个类继承 WhitePitaya,然后重写 name()price() 方法,当然我们也可以用 类委托 的方式实现

class RedPitaya: Fruit by WhitePitaya {
    override fun name(): String {
        return "红心火龙果"
    }
    override fun price(): String {
        return "22.8"
    }
}
val pitaya = RedPitaya()
println("RedPitaya={name=${pitaya.name()}, shape=${pitaya.shape()}, price=${pitaya.price()}}")
> RedPitaya={name="红心火龙果", shape="火龙果的形状", price="22.8"}

​ 这个时候打印 RedPitaya 的几个方法,重写的两个方法已经变了,没有重写的方法打印的是 WhitePitaya 中的数据。可能有人要说了,这不就和继承一个样吗,从这个例子上看,实现的效果确实和继承一样,但是我们都知道的是,一个类只能继承一个类,但是能同时实现多个接口啊!!通过这种方式我们不就能实现类似继承多个类的效果了吗!

用类委托优化列表页

​ 依照上面的思路,我们可以把列表页的功能拆分为 获取数据相关、收藏相关、文章点击相关 三个部分。

  1. 首先是获取数据相关的接口:
/** 分页获取数据相关接口 */
interface ArticleListPagingInterface {
    
     /** 页码 */
    val pageNumber: MutableLiveData<Int>

    /** 文章列表数据 */
    val articleListData: LiveData<ArrayList<ArticleEntity>>

    /** 刷新状态 */
    val refreshing: MutableLiveData<SmartRefreshState>

    /** 加载更多状态 */
    val loadMore: MutableLiveData<SmartRefreshState>

    /** 刷新回调 */
    val onRefresh: () -> Unit

    /** 加载更多回调 */
    val onLoadMore: () -> Unit

    /** 根据页码 [Int] 获取文章列表数据 */
    var getArticleList: (Int) -> LiveData<NetResult<ArticleListEntity>>
}

/** 分页获取数据相关接口实现类 */
class ArticleListPagingInterfaceImpl
    : ArticleListPagingInterface {

    /** 页码 */
    override val pageNumber: MutableLiveData<Int> = MutableLiveData()

    /** 文章列表请求返回数据 */
    private val articleListResultData: LiveData<NetResult<ArticleListEntity>> = pageNumber.switchMap { pageNum ->
        getArticleList.invoke(pageNum)
    }

    /** 文章列表 */
    override val articleListData: LiveData<ArrayList<ArticleEntity>> = articleListResultData.switchMap { result ->
        disposeArticleListResult(result)
    }

    /** 刷新状态 */
    override val refreshing: MutableLiveData<SmartRefreshState> = MutableLiveData()

    /** 加载更多状态 */
    override val loadMore: MutableLiveData<SmartRefreshState> = MutableLiveData()

    /** 刷新回调 */
    override val onRefresh: () -> Unit = {
        pageNumber.value = NET_PAGE_START
    }

    /** 加载更多回调 */
    override val onLoadMore: () -> Unit = {
        pageNumber.value = pageNumber.value.orElse(NET_PAGE_START) + 1
    }

    override var getArticleList: (Int) -> LiveData<NetResult<ArticleListEntity>> = {
        throw RuntimeException("Please set your custom method!")
    }

    /** 处理文章列表返回数据 [result],并返回文章列表 */
    private fun disposeArticleListResult(result: NetResult<ArticleListEntity>): LiveData<ArrayList<ArticleEntity>> {
        val liveData = MutableLiveData<ArrayList<ArticleEntity>>()
        val refresh = pageNumber.value == NET_PAGE_START
        val smartControl = if (refresh) refreshing else loadMore
        result.judge(
                onSuccess = {
                    smartControl.value = SmartRefreshState(loading = false, success = true, noMore = data?.over.toBoolean())
                    liveData.value = articleListData.value.copy(data?.datas, refresh)
                },
                onFailed = {
                    smartControl.value = SmartRefreshState(loading = false, success = false)
                    liveData.value = articleListData.value.orEmpty()
                },
                onFailed4Login = {
                    smartControl.value = SmartRefreshState(loading = false, success = false)
                    liveData.value = articleListData.value.orEmpty()
                    false
                }
        )
        return liveData
    }
}
  1. 收藏相关接口
/** 收藏相关接口 */
interface ArticleCollectionInterface {
    
     /** 收藏文章[item],使用 [snackbarData] 弹出提示 */
    suspend fun collect(item: ArticleEntity, snackbarData: MutableLiveData<SnackbarModel>)
    
    /** 取消收藏文章[item] */
    suspend fun unCollect(item: ArticleEntity, snackbarData: MutableLiveData<SnackbarModel>)
}

/** 收藏相关接口实现类 */
class ArticaleCollectionInterfaceImpl(
    private val repository: ArticleRepository
): ArticleCollectionInterface {
    
      /** 收藏文章[item],使用 [snackbarData] 弹出提示 */
    override suspend fun collect(item: ArticleEntity, snackbarData: MutableLiveData<SnackbarModel>) {
        try {
            // 收藏
            repository.collectArticleInside(item.id.orEmpty())
                    .judge(onFailed = {
                        // 收藏失败,提示、回滚收藏状态
                        snackbarData.value = this.toSnackbarModel()
                        item.collected.set(false)
                    })
        } catch (throwable: Throwable) {
            Logger.t("NET").e(throwable, "collect")
            // 收藏失败,提示、回滚收藏状态
            snackbarData.value = throwable.toSnackbarModel()
            item.collected.set(false)
        }
    }

    /** 取消收藏文章[item],使用 [snackbarData] 弹出提示 */
    override suspend fun unCollect(item: ArticleEntity, snackbarData: MutableLiveData<SnackbarModel>) {
        try {
            // 取消收藏
            repository.unCollectArticleList(item.id.orEmpty()).judge(onFailed = {
                // 取消收藏失败,提示、回滚收藏状态
                snackbarData.value = toSnackbarModel()
                item.collected.set(true)
            })
        } catch (throwable: Throwable) {
            Logger.t("NET").e(throwable, "unCollect")
            // 取消收藏失败,提示、回滚收藏状态
            snackbarData.value = throwable.toSnackbarModel()
            item.collected.set(true)
        }
    }
}
  1. 列表文章点击相关接口
/** 列表文章点击接口 */
interface ArticleListItemInterface {

    /** 文章列表条目点击 */
    val onArticleItemClick: (ArticleEntity) -> Unit

    /** 文章收藏点击 */
    val onArticleCollectClick: (ArticleEntity) -> Unit
}

/** 列表文章点击接口实现类 */
class ArticleListItemInterfaceImpl(
        private val viewModel: BaseViewModel,
        private val jumpToWebViewData: MutableLiveData<WebViewActivity.ActionModel>
) : ArticleListItemInterface {

    /** 文章列表条目点击 */
    override val onArticleItemClick: (ArticleEntity) -> Unit = { item ->
        jumpToWebViewData.value = WebViewActivity.ActionModel(item.id.orEmpty(), item.title.orEmpty(), item.link.orEmpty())
    }

    /** 文章收藏点击 */
    override val onArticleCollectClick: (ArticleEntity) -> Unit = fun(item) {
        val impl = viewModel as? ArticleCollectionInterface ?: return
        viewModel.viewModelScope.launch {
            if (item.collected.get().condition) {
                // 已收藏,取消收藏
                item.collected.set(false)
                impl.unCollect(item, viewModel.snackbarData)
            } else {
                // 未收藏,收藏
                item.collected.set(true)
                impl.collect(item, viewModel.snackbarData)
            }
        }
    }
}

​ 这样我们对功能的拆分就完成了,接下来我们就来看看用 类委托 实现的列表页是怎么样的吧

class BjnewsArticlesViewModel(
        private val repository: ArticleRepository
) : BaseViewModel(),
        ArticleCollectionInterface by ArticleCollectionInterfaceImpl(repository),
        ArticleListPagingInterface by ArticleListPagingInterfaceImpl() {

    /** 公众号 id */
    var bjnewsId = ""

    init {
        getArticleList = { pageNum ->
            val result = MutableLiveData<NetResult<ArticleListEntity>>()
            viewModelScope.launch {
                try {
                    result.value = repository.getBjnewsArticles(bjnewsId, pageNum)
                } catch (throwable: Throwable) {
                    Logger.t("NET").e(throwable, "getArticleList")
                }
            }
            result
        }
    }

    /** 跳转网页数据 */
    val jumpWebViewData = MutableLiveData<WebViewActivity.ActionModel>()

    /** 列表事件 */
    val articleListItemInterface: ArticleListItemInterface by lazy {
        ArticleListItemInterfaceImpl(this, jumpWebViewData)
    }
}

​ 这就是优化之后的最终版本,不过好像有30多行、、、不过这并不重要( ̄y▽, ̄)╭ ,重要的是我们在这过程中使用 类委托 对功能的拆分,主要的功能逻辑都抽离到 ArticleCollectionInterfaceArticleListPagingInterface 中,并且实际使用了对应的 ArticleCollectionInterfaceImplArticleListPagingInterfaceImpl 中的实现。

总结

​ 经过上面的优化,我们减少了大量的重复代码,APP中的四五个相似的界面后能够简单的实现完成,当然,更重要的是不同的功能拆分出来后你就可以更具需求将不同的功能进行组装,以达到不同的效果,并且功能分类清晰,让项目更容易维护。

​ 那么关于列表页的优化我们就讲到这里了,下一章我们再来说说 Kotlin类委托 实现原理以及使用过程中需要注意的事项,可能有人也已经对我上面的部分代码产生疑问了,这点我们也会在下一章讲解。

​ 想要我的源码吗?想要的话可以全部给你,去找吧!我把所有源码都放在那里!>> SampleProject <<

​ 感谢大家的耐心观看,我是 WangJie0822 ,一个平平凡凡的程序猿,欢迎关注。

作者: WangJie0822
链接: http://www.wangjie0822.top/posts/c419796a
来源: WangJie0822
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,542评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,596评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,021评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,682评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,792评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,985评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,107评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,845评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,299评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,612评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,747评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,441评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,072评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,828评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,069评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,545评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,658评论 2 350

推荐阅读更多精彩内容