使用Kotlin構(gòu)建Android MVVM應(yīng)用程序

概述

說到MVVM,大家都會(huì)想起前端的MVVM框架,相較于前端MVVM的火熱,它在移動(dòng)開發(fā)領(lǐng)域就不那么熱門了。Google在2015年才推出DataBinding框架,起步較晚,而且2015年是MVP模式爆發(fā)的一年,2016年是各種熱修復(fù)、插件化爆發(fā)的一年,它沒趕上好時(shí)機(jī)。

PS:DataBinding和MVVM二者并不相同。MVVM是一種架構(gòu)模式,而DataBinding是Android中實(shí)現(xiàn)數(shù)據(jù)與UI綁定的框架,是構(gòu)建MVVM架構(gòu)的一個(gè)工具,作用類似于增強(qiáng)版的ButterKnife。

自16年接觸DataBinding以來,苦于這方面的知識(shí)較少,但是Databinding在使用過程中又十分便捷,所以一直以來都在不停探索怎樣才能構(gòu)建出合適的MVVM架構(gòu)程序,在經(jīng)過幾次的項(xiàng)目重構(gòu)之后,終于在近期結(jié)合Kotlin語(yǔ)言探索出了更適合Android的MVVM架構(gòu)。

小專欄 :使用Kotlin構(gòu)建Android MVVM應(yīng)用程序
Github示例:https://github.com/ditclear/PaoNet

我們先來看看什么是MVVM,然后再一步一步來闡述整個(gè)的MVVM框架

MVC、MVP、MVVM

我們先大致了解下Android開發(fā)的創(chuàng)建模式

MVC

Model:實(shí)體模型、數(shù)據(jù)的獲取、存儲(chǔ)等等

View:Activity、fragment、view、adapter、xml等等

Controller:為View層處理數(shù)據(jù),業(yè)務(wù)等等

從這個(gè)結(jié)構(gòu)來看,Android本身還是符合MVC架構(gòu)的。不過由于作為純View的xml功能太弱,以及controller能提供給開發(fā)者的作用較小,還不如在Activity頁(yè)面直接進(jìn)行處理,但這么做卻造成了代碼大爆炸。一個(gè)頁(yè)面邏輯復(fù)雜的頁(yè)面動(dòng)輒上千行,注釋沒寫好的話還十分不好維護(hù),而且難以進(jìn)行單元測(cè)試,所以這更像是一個(gè)Model-View的架構(gòu),不適用于打造穩(wěn)定的Android項(xiàng)目。

MVP

Model:實(shí)體模型、數(shù)據(jù)的獲取、存儲(chǔ)等等

View:Activity、fragment、view、adapter、xml等等

Presenter:負(fù)責(zé)完成View與Model間的交互和業(yè)務(wù)邏輯,以回調(diào)返回結(jié)果。

前面說,Activity充當(dāng)了View和Controller的作用, 造成了代碼爆炸。而MVP架構(gòu)很好的處理了這個(gè)問題。其核心理念是通過一個(gè)抽象的View接口(不是真正的View層)將Presenter與真正的View層進(jìn)行解耦。Persenter持有該View接口,對(duì)該接口進(jìn)行操作,而不是直接操作View層。這樣就可以把視圖操作和業(yè)務(wù)邏輯解耦,從而讓Activity成為真正的View層。

這也是現(xiàn)今比較流行的架構(gòu),可是弊端也是有的。如果業(yè)務(wù)復(fù)雜了,也可能導(dǎo)致P層太臃腫,而且V和P層有一定耦合度,如果UI有什么地方需要更改,那么P層不只改一個(gè)地方那么簡(jiǎn)單,還需要改View的接口及其實(shí)現(xiàn),牽一發(fā)動(dòng)全身,運(yùn)用MVP的同行都對(duì)此怨聲載道。

MVVM

Model:實(shí)體模型、數(shù)據(jù)的獲取、存儲(chǔ)等等

View:Activity、fragment、view、adapter、xml等等

ViewModel:負(fù)責(zé)完成View與Model間的交互和業(yè)務(wù)邏輯,基于DataBinding改變UI

MVVM的目標(biāo)和思想與MVP類似,但它沒有MVP那令人厭煩的各種回調(diào),利用DataBinding就可以更新UI和狀態(tài),達(dá)到理想的效果。

數(shù)據(jù)驅(qū)動(dòng)UI

在使用MVC或MVP開發(fā)時(shí),我們?nèi)绻耈I,首先需要找到這個(gè)view的引用,然后賦予值,才能進(jìn)行更新。在MVVM中,這就不需要了。MVVM是通過數(shù)據(jù)驅(qū)動(dòng)UI的,這些都是自動(dòng)完成。數(shù)據(jù)的重要性在MVVM架構(gòu)中得到提高,成為主導(dǎo)因素。在這種架構(gòu)模式中,開發(fā)者重點(diǎn)關(guān)注的是怎樣處理數(shù)據(jù),保證數(shù)據(jù)的正確性。

關(guān)注點(diǎn)分離

常見的錯(cuò)誤就是把所有代碼都寫在Activity或者Fragment中。任何跟UI和系統(tǒng)交互無關(guān)的事情都不應(yīng)該放在這些類當(dāng)中。盡可能讓它們保持簡(jiǎn)單輕量可以避免很多生命周期方面的問題。MVVM架構(gòu)模式下,數(shù)據(jù)和業(yè)務(wù)邏輯都處于ViewModel中,ViewModel只關(guān)心數(shù)據(jù)和業(yè)務(wù),不需要直接和UI打交道,而Model只需要提供ViewModel的數(shù)據(jù)源,View則關(guān)心如何顯示數(shù)據(jù)和處理與用戶的交互。

通過以上簡(jiǎn)述和與MVC、MVP的對(duì)比,我們可以發(fā)現(xiàn)MVVM還是很有優(yōu)勢(shì)的,而如果再搭配Kotlin語(yǔ)言的話,可以說是如虎添翼了。

如何開始?

其實(shí)結(jié)構(gòu)已經(jīng)很清晰了,我們只需要做M-V-VM層各層應(yīng)該做的事情,做到關(guān)注點(diǎn)分離。

M層 的關(guān)注點(diǎn)是怎么提供數(shù)據(jù)給ViewModel

ViewModel層 關(guān)注點(diǎn)是怎么處理數(shù)據(jù)(包括使用DataBinding綁定數(shù)據(jù),以及控制loading、empty狀態(tài))

View層的關(guān)注點(diǎn)是顯示數(shù)據(jù),接收用戶的操作,調(diào)用ViewModel中的方法

為了打造更適合Android的MVVM架構(gòu),使用到的技術(shù)有AOP、Dagger2、RxJava、Retrofit、Room和Kotlin,并遵循統(tǒng)一的命名規(guī)范和調(diào)用準(zhǔn)則,保證開發(fā)時(shí)的一致性。

以下是我們現(xiàn)今的架構(gòu):

MVVM

創(chuàng)建文章詳情界面

接下來我將展示一下M-V-VM三層之間如何協(xié)作,以文章詳情頁(yè)面為例

V—VM

UI由ArtcileDetailActivity.kt及article_detail_activity.xml組成。

要驅(qū)動(dòng)UI,我們的數(shù)據(jù)模型需要持有幾個(gè)元素:

  • Article Id:文章詳情的id,用于加載文章詳情
  • title:文章的標(biāo)題
  • content:文章的內(nèi)容
  • state:加載狀態(tài),用一個(gè)State類來封裝

我們將創(chuàng)建一個(gè)ArticleDetailViewModel.kt來保存。

一個(gè)ViewModel為特定的UI組件提供數(shù)據(jù),比如fragment 或者 activity,并負(fù)責(zé)和數(shù)據(jù)處理的業(yè)務(wù)邏輯部分通信,比如調(diào)用其它組件加載數(shù)據(jù)或者轉(zhuǎn)發(fā)用戶的修改。ViewModel并不知道View的存在,也不會(huì)受configuration change影響。

現(xiàn)在我們有了三個(gè)文件。

article_detail_activity.xml: 定義頁(yè)面的UI

ArticleDetailViewModel.kt: 為UI準(zhǔn)備數(shù)據(jù)的類

ArtcileDetailActivity.kt: 顯示ViewModel中的數(shù)據(jù)與響應(yīng)用戶交互的控制器

下面開始實(shí)現(xiàn)(為了簡(jiǎn)單,只顯示了主要部分):

<?xml version="1.0" encoding="utf-8"?>
<layout >

    <data>
        <import type="android.view.View"/>
        <variable
                name="vm"
                type="io.ditclear.app.viewmodel.ArticleDetailViewModel"/>
    </data>

    <android.support.design.widget.CoordinatorLayout>

        <android.support.design.widget.AppBarLayout>

            <android.support.design.widget.CollapsingToolbarLayout>
                <android.support.v7.widget.Toolbar
                        app:title="@{vm.title}"/>

            </android.support.design.widget.CollapsingToolbarLayout>
        </android.support.design.widget.AppBarLayout>

        <android.support.v4.widget.NestedScrollView>

            <LinearLayout>
                <ProgressBar
                        android:visibility="@{vm.loading?View.VISIBLE:View.GONE}"/>

                <WebView
                        android:id="@+id/web_view"
                        app:markdown="@{vm.content}"
                        android:visibility="@{vm.loading?View.GONE:View.VISIBLE}"/>

            </LinearLayout>
        </android.support.v4.widget.NestedScrollView>


    </android.support.design.widget.CoordinatorLayout>
</layout>
/**
 * 頁(yè)面描述:ArticleDetailViewModel
 * @param repo 數(shù)據(jù)源Model(MVVM 中的M),負(fù)責(zé)提供ViewModel中需要處理的數(shù)據(jù)
 * Created by ditclear on 2017/11/17.
 */
class ArticleDetailViewModel @Inject constructor(val repo: ArticleRepository) {

    //////////////////data//////////////
    lateinit var articleId:Int
    val loading=ObservableBoolean(false)
    val content = ObservableField<String>()
    val title = ObservableField<String>()

    //////////////////binding//////////////
    fun loadArticle():Single<Article> =
        repo.getArticleDetail(articleId)
                .async()
                .doOnSuccess { t: Article? ->
                    t?.let {
                        title.set(it.title)
                        content.set(it.content)

                    }
                }
                .doOnSubscribe { startLoad()}
                .doAfterTerminate { stopLoad() }



    fun startLoad()=loading.set(true)
    fun stopLoad()=loading.set(false)
}
/**
 * 頁(yè)面描述:ArticleDetailActivity,處理和用戶的交互(點(diǎn)擊事件),以及處理
 * viewModel層回調(diào)的數(shù)據(jù),附加一些顯示Loading,空狀態(tài)和綁定生命周期等等的操作
 * Created by ditclear on 2017/11/17.
 */
class ArticleDetailActivity : BaseActivity<ArticleDetailActivityBinding>() {

    override fun getLayoutId(): Int = R.layout.article_detail_activity

    @Inject
    lateinit var viewModel: ArticleDetailViewModel
    
    //init
    override fun initView() {
       //統(tǒng)一都是KEY_DATA,別自己瞎命名
        val articleID: Int? = intent?.extras?.getInt(Constants.KEY_DATA)
        if (articleID == null) {
            toast("文章不存在", ToastType.WARNING)
            finish()
        }

        getComponent().inject(this)
      
        mBinding.vm = viewModel.apply {
            this.articleID = articleID
        }
    }
  
    //加載數(shù)據(jù)
    override fun loadData() {

        viewModel.loadData()
                .compose(bindToLifecycle())
//              .doOnSubcribe{ showLoadingDialog() }
//              .doAfterTerminate{ hideLoadingDialog() }
                .subscribe({},{ dispatchFailure(it) })

    }

}

他們是如何工作的呢?

在進(jìn)入到ArticleDetailActivity頁(yè)面之后

  1. init()方法->先進(jìn)行數(shù)據(jù)的初始化,將viewModel和xml文件進(jìn)行綁定
  2. loadData()方法->調(diào)用viewModel的方法

進(jìn)入ArticleDetailViewModel

  1. 調(diào)用Model層獲取詳情方法獲取數(shù)據(jù)源
  2. 根據(jù)需要使用RxJava操作符對(duì)數(shù)據(jù)進(jìn)行轉(zhuǎn)換,通過DataBinding更新UI
  3. 返回可觀測(cè)的Single對(duì)象給View

回到ArticleDetailActivity頁(yè)面

  1. 綁定生命周期,避免內(nèi)存泄漏
  2. 對(duì)返回的可觀測(cè)對(duì)象進(jìn)行訂閱
  3. 處理成功和失敗的情況

至此,V-VM之間如何協(xié)作就清楚了。

M—VM

現(xiàn)在我們把View和ViewModel聯(lián)系了起來,但是ViewModel該如何獲取數(shù)據(jù)呢?

我們使用Retrofit來從后端獲取網(wǎng)絡(luò)數(shù)據(jù)。

interface ArticleService{
    //文章詳情
    @GET("article_detail.php")
    fun getArticleDetail(@Query("id") id: Int): Single<Article>
}

使用Room數(shù)據(jù)庫(kù)來進(jìn)行持久化

@Dao
interface ArticleDao{

    @Query("SELECT * FROM Articles WHERE articleid= :id")
    fun getArticleById(id:Int):Single<Article>


    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertArticle(article :Article)

}

然后使用ArticleRepository.kt對(duì)網(wǎng)絡(luò)和本地操作進(jìn)行一層封裝

/**
 * 頁(yè)面描述:ArticleRepository
 * 提供數(shù)據(jù)給ViewModel層 , 處理網(wǎng)絡(luò)數(shù)據(jù)和本地緩存之間的關(guān)系
 * Created by ditclear on 2017/11/17.
 */
class ArticleRepository @Inject constructor
    (private val remote: ArticleService, private val local: ArticleDao) {

    /* 文章詳情
     * 先查看本地是否有緩存,如果沒有那么再去請(qǐng)求網(wǎng)絡(luò),成功后更新本地緩存
     */
    fun getArticle(articleId: Int): Single<Article> =
           local.getArticleById(articleId).onErrorResumeNext {
        if (it is EmptyResultSetException) {
            remote.getArticleDetail(articleId).doOnSuccess { t -> t?.let { local.insertArticle(it) } }
        } else throw it
    }
  
    }

先查看本地是否有緩存,如果沒有那么再去請(qǐng)求網(wǎng)絡(luò),成功后更新本地緩存。

封裝成Repository的原因是ViewModel不需要知道它的數(shù)據(jù)具體是從哪來的,這不是ViewModel這一層需要關(guān)心的事情。

即使你的項(xiàng)目沒有進(jìn)行數(shù)據(jù)緩存,總是從網(wǎng)絡(luò)拉取數(shù)據(jù),也建議封裝成Repository,這意味著你的網(wǎng)絡(luò)層是可以替換的,意義有點(diǎn)類似于封裝一個(gè)ImageLoadUtil。

總體的流程就這么多,其實(shí)弄懂就很簡(jiǎn)單了。關(guān)鍵點(diǎn)是各層之間職責(zé)明確,以及解耦(Dagger2)和使用DataBinding時(shí)需要一個(gè)統(tǒng)一的規(guī)范。

而再細(xì)分,優(yōu)化,也就是進(jìn)行模塊化、組件化的工作,深入些的插件化、熱修復(fù)等等。不過萬(wàn)丈高樓平地起,我們的地基打的嚴(yán)實(shí),以后的工作才會(huì)相對(duì)容易。

本文的代碼都可以在https://github.com/ditclear/PaoNet中找到

一些建議

建議一:在Activity或Fragment里處理點(diǎn)擊事件

使用Presenter來繼承View.OnClickListener

interface Presenter:View.OnClickListener{
    override fun onClick(v: View?)
}

然后在BaseActivity/BaseFragment里實(shí)現(xiàn)它

abstract class BaseActivity<VB : ViewDataBinding> : RxAppCompatActivity(),Presenter{
    
}

這樣當(dāng)我們要設(shè)置點(diǎn)擊事件時(shí),只需要

class ArticleDetailActivity : BaseActivity<ArticleDetailActivityBinding>() {
  
    //...
    //init
    override fun initView() {

        mBinding.let{
            it.vm=mViewModel
            it.presenter=this
        }
    }
}

在xml中使用時(shí),則統(tǒng)一使用presenter.onClick(view)方法

<layout>
    <data>
        <variable
                name="presenter"
                type="com.ditclear.paonet.view.helper.presenter.Presenter"/>
    </data>
  
    <android.support.design.widget.CoordinatorLayout>
        
        <android.support.design.widget.FloatingActionButton
                android:id="@+id/stow_fab"                                                                            
                android:onClick="@{(v)->presenter.onClick(v)}"
/>
    </android.support.design.widget.CoordinatorLayout>
</layout>

真正處理則放在相應(yīng)的Activity/Fragment里

class ArticleDetailActivity : BaseActivity<ArticleDetailActivityBinding>() {
  
    //...
    @SingleClick
    override fun onClick(v: View?) {
        when (v?.id) {
            R.id.stow_fab -> stow()
            //more ..
            R.id.other_action -> other()
        }
    }
  //其它
   private fun stow() {
   }
  
    //收藏
    private fun stow() {
        viewModel.stow().compose(bindToLifecycle())
                .subscribe({ toastSuccess(it?.message?:"收藏成功") }
                        , {  toastFailure(it) } })
    }
}

@SingleClick是一個(gè)注解,作為AspectJ的切面,來防止多次點(diǎn)擊,需要將view作為參數(shù),詳細(xì)可參考文章

DataBinding結(jié)合AspectJ防止多次點(diǎn)擊

這是這樣處理點(diǎn)擊事件的原因之一,另一個(gè)好處是方便綁定生命周期,和進(jìn)行回調(diào)處理(比如一些需要用到activity context的dialog和toast的時(shí)候,都可以寫在doOnSubscribe和doAfterTerminate操作符里),避免了ViewModel層持有context。

建議二:多寫單元測(cè)試

單元測(cè)試能保證數(shù)據(jù)和邏輯的正確性,而且語(yǔ)法相對(duì)簡(jiǎn)單,很容易學(xué)習(xí)。而且運(yùn)行一次單元測(cè)試的時(shí)間簡(jiǎn)直毫秒殺運(yùn)行一次app的時(shí)間。

我認(rèn)為程序員和普通碼農(nóng)直接的區(qū)別之一便是是否進(jìn)行單元測(cè)試。

而且由于ViewModel層是純Kotlin/Java代碼,感覺就如以前使用Eclipse寫簡(jiǎn)單的控制臺(tái)程序。

當(dāng)然單元測(cè)試的作用不僅限于寫測(cè)試代碼,我一般都會(huì)在里面玩玩RxJava的操作符,進(jìn)行一些算法的練習(xí),驗(yàn)證數(shù)據(jù)的輸出是否正確等等。

如果你想學(xué)習(xí)或了解單元測(cè)試,可以查看以下文章:

關(guān)于安卓單元測(cè)試,你需要知道的一切(by 小創(chuàng))

使用Kotlin和RxJava測(cè)試MVP架構(gòu)的完整示例

關(guān)于DataBinding

很多開發(fā)者放棄DataBinding原因就在于出錯(cuò)了不容易排查錯(cuò)誤。
只顯示出很多XXBinding未找到。
如果有一定使用經(jīng)驗(yàn)的就知道只看最后一條報(bào)錯(cuò)信息就夠了。
這里介紹一種我經(jīng)常使用來排查錯(cuò)誤的方式:
在Android Studio 的terminal 里運(yùn)行

./gradlew clean assembleDebug

或者

./gradlew compileDebugJavaWithJavac

因?yàn)镈ataBinding是編譯生成代碼的,很多錯(cuò)誤都是xml中表達(dá)式寫的有問題導(dǎo)致的,所以運(yùn)行以上命令容易在terminal中打印出具體錯(cuò)誤的信息。這些命令對(duì)于需要編譯生成代碼的框架排查錯(cuò)誤十分有用,比如Dagger2。

更多信息請(qǐng)查閱 DataBinding實(shí)用指南

規(guī)范

想要在使用DataBinding的過程中不出錯(cuò),遵守統(tǒng)一的規(guī)范是一定的

  1. ViewModel—View—XML—Model(盡量) 應(yīng)該相互對(duì)應(yīng),以功能模塊開頭

普通頁(yè)面

ViewModel View XML
ArticleDetailViewModel.kt ArticleDetailActivity.kt article_detail_activity.xml

列表頁(yè)面 :請(qǐng)參考文章 告別反復(fù)、冗余的自定義Adapter

ViewModel View XML
ArticleListViewModel.kt ArticleListActivity.kt article_list_activity.xml
Item ViewModel Item XML
ArticleListItemViewModel.kt article_list_item.xml

Model 層命名

Remote Local Repository
ArticleService.kt ArticleDao.kt ArticleRepository.kt
  1. xml布局文件中的variable統(tǒng)一命名

    ViewModel Presenter(點(diǎn)擊事件) Item(列表項(xiàng))
    vm presenter item

參考資料

如何構(gòu)建Android MVVM 應(yīng)用框架

App開發(fā)架構(gòu)指南(谷歌官方文檔譯文)

招人:

公司大量招人,產(chǎn)品/Android/Flutter/IOS/前后端/測(cè)試/UI等等都需要,公司在全球各地都有辦事處,國(guó)內(nèi)外大佬眾多,保底16薪,薪資很有吸引力,外企氛圍,工作時(shí)間955,不加班,加班按3倍工資算,每年一次免費(fèi)出國(guó)旅游,地點(diǎn)在上海,如果有興趣的可以發(fā)送簡(jiǎn)歷至郵箱ditclear@qq.com,期待一起共事

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

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

  • 1、概述 Databinding 是一種框架,MVVM是一種模式,兩者的概念是不一樣的。我的理解DataBindi...
    Kelin閱讀 76,854評(píng)論 68 521
  • 早上解決完客戶門壞跟燃?xì)獗戆惭b的問題,坐在公交站臺(tái)等去另一個(gè)項(xiàng)目的公車。一個(gè)爸爸帶著女兒在一邊等公交一邊交談。女孩...
    一粒簡(jiǎn)單閱讀 111評(píng)論 0 0