用 Kotlin 開(kāi)發(fā)現(xiàn)代 Android 項(xiàng)目 Part 2

簡(jiǎn)評(píng):繼續(xù)第一部分的文章业栅,作者在第二部分中使用的技術(shù)包括 MVVM,RxJava2.

5. MVVM 架構(gòu) + 存儲(chǔ)庫(kù)模式 + Android 管理封裝器

關(guān)于 Android 世界的一點(diǎn)點(diǎn)架構(gòu)知識(shí)

長(zhǎng)時(shí)間以來(lái)漾抬,Android 開(kāi)發(fā)者們?cè)谒麄兊捻?xiàng)目中沒(méi)有使用任何類型的架構(gòu)。近三年以來(lái)常遂,架構(gòu)在 Android 開(kāi)發(fā)者社區(qū)中被炒得天花亂墜纳令。Activity 之神的時(shí)代已經(jīng)過(guò)去了,Google 發(fā)布了 Android 架構(gòu)藍(lán)圖倉(cāng)庫(kù),提供了許多樣例和說(shuō)明來(lái)實(shí)現(xiàn)不同的架構(gòu)方式平绩。最終圈匆,在 Google IO 17 大會(huì),他們介紹了 Android 架構(gòu)組件捏雌,這些庫(kù)的集合幫助我們編寫(xiě)更清晰的代碼和更好的 app跃赚。你可以使用所有的組件,也可以使用其中的部分性湿。但是纬傲,我覺(jué)得它們都挺有用的。接下來(lái)我們將使用這些組件肤频。我會(huì)先解決這些問(wèn)題叹括,然后用這些組件和庫(kù)來(lái)重構(gòu)代碼,看看這些庫(kù)解決了哪些問(wèn)題宵荒。

有兩種主要的架構(gòu)模式分離了 GUI 代碼:

  • MVP
  • MVVM

很難說(shuō)哪一種更好汁雷。你應(yīng)該兩種都嘗試一下,然后再做出決定报咳。我傾向于使用管理生命周期組件的 MVVM 模式侠讯,并且接下來(lái)我會(huì)介紹它。如果你沒(méi)試過(guò) MVP少孝,在 Medium 上有大量很好的關(guān)于它的文章继低。

什么是 MVVM 模式?

MVVM 模式是一種架構(gòu)模式稍走。它代表 Model-View-ViewModel袁翁。我覺(jué)得這個(gè)名字會(huì)讓開(kāi)發(fā)者困擾。如果我是那個(gè)命名的人婿脸,我會(huì)稱之為 View-ViewModel-Model粱胜,因?yàn)?ViewModel 是連接視圖和模型的中間件。

  • View 是你的 Activity狐树,F(xiàn)ragment 或者其他 Android 自定義視圖的抽象焙压。注意千萬(wàn)別和 Android View 混淆了。這個(gè) View 應(yīng)該是個(gè)啞巴抑钟,我們不能在 View 里寫(xiě)任何邏輯涯曲。視圖不應(yīng)該持有任何數(shù)據(jù)。它應(yīng)該有一個(gè) ViewModel 實(shí)例在塔,所有 View 需要的數(shù)據(jù)應(yīng)該從這個(gè)實(shí)例中獲取幻件。同時(shí),View 應(yīng)該觀察這些數(shù)據(jù)蛔溃,一旦 ViewModel 中的數(shù)據(jù)發(fā)生了變化绰沥,布局就會(huì)發(fā)生改變篱蝇。View 有一個(gè)職責(zé):不同的數(shù)據(jù)和狀態(tài)下,布局會(huì)怎樣顯示徽曲。

  • ViewModel 是持有數(shù)據(jù)和邏輯類的一個(gè)抽象零截,負(fù)責(zé)什么時(shí)候獲取數(shù)據(jù),什么時(shí)候傳遞數(shù)據(jù)秃臣。ViewModel 保留當(dāng)前的狀態(tài)涧衙。同時(shí),ViewModel 持有一個(gè)或多個(gè) Model 實(shí)例甜刻,所有的數(shù)據(jù)都從這些實(shí)例中獲取绍撞。它不應(yīng)該知道這些數(shù)據(jù)是從數(shù)據(jù)庫(kù)中獲取的還是從遠(yuǎn)程服務(wù)器中獲取的。此外得院,ViewModel 也不應(yīng)該知道關(guān)于 View 的一切傻铣。而且,ViewModel 也不應(yīng)該知道任何關(guān)于 Android 框架的事祥绞。

  • Model 是為 ViewModel 準(zhǔn)備數(shù)據(jù)的抽象層非洲。它是那些從遠(yuǎn)程服務(wù)器或者內(nèi)存緩存或者本地?cái)?shù)據(jù)庫(kù)獲取數(shù)據(jù)的類。這和那些 User蜕径,Car两踏,Square 等等是不一樣的。模型類知識(shí)一些保存數(shù)據(jù)的類兜喻。通常梦染,它是倉(cāng)庫(kù)模式的一種實(shí)現(xiàn),我們接下來(lái)將會(huì)講到朴皆。Model 不該知道關(guān)于 ViewModel 的一切帕识。

MVVM,如果正確實(shí)現(xiàn)遂铡,將是一種很好的分離代碼的方式肮疗,這樣也會(huì)讓它更容易測(cè)試。它幫助我們遵循 SOLID 原則扒接,因此我們的代碼更容易維護(hù)伪货。

現(xiàn)在我將寫(xiě)一個(gè)最簡(jiǎn)單的示例來(lái)解釋它是怎樣工作的。

首先钾怔,創(chuàng)建一個(gè)簡(jiǎn)單的Model來(lái)返回字符串:

class RepoModel {

    fun refreshData() : String {
        return "Some new data"
    }
}

通常碱呼,獲取數(shù)據(jù)是異步的,所以我們必須等待一下宗侦。為了模擬這種情況巍举,我把它改成下面這樣:

class RepoModel {

    fun refreshData(onDataReadyCallback: OnDataReadyCallback) {
        Handler().postDelayed({ onDataReadyCallback.onDataReady("new data") },2000)
    }
}

interface OnDataReadyCallback {
    fun onDataReady(data : String)
}

首先,我創(chuàng)建了 OnDataReadyCallback 接口凝垛,它有個(gè) onDataReady 函數(shù)“妹酰現(xiàn)在,我們的 refreshData 函數(shù)實(shí)現(xiàn)了 OnDataReadyCallback 梦皮。為了模擬等待炭分,我使用了 Handler。2 秒后剑肯,OnDataReadyCallback 的實(shí)現(xiàn)將會(huì)調(diào)用 onDataReady 函數(shù)捧毛。

現(xiàn)在來(lái)創(chuàng)建我們的ViewModel

class MainViewModel {
    var repoModel: RepoModel = RepoModel()
    var text: String = ""
    var isLoading: Boolean = false
}

可以看到,有一個(gè) RepoModel 的示例让网,即將展示的 text 以及保存當(dāng)前狀態(tài)的 isLoading ⊙接牵現(xiàn)在,創(chuàng)建一個(gè) refresh 函數(shù)溃睹,用來(lái)獲取數(shù)據(jù):

class MainViewModel {
    ...

    val onDataReadyCallback = object : OnDataReadyCallback {
        override fun onDataReady(data: String) {
            isLoading.set(false)
            text.set(data)
        }
    }

    fun refresh(){
        isLoading.set(true)
        repoModel.refreshData(onDataReadyCallback)
    }
}

refresh 函數(shù)調(diào)用了 RepoModel 中的 refreshData而账,傳遞了一個(gè)實(shí)現(xiàn) OnDataReadyCallback 接口的實(shí)例。好因篇,那么什么是對(duì)象呢泞辐?無(wú)論何時(shí),當(dāng)你想實(shí)現(xiàn)一些接口或者繼承一些類而不用創(chuàng)建子類時(shí)竞滓,你都會(huì)使用對(duì)象聲明咐吼。如果你想要使用匿名類呢?在這里商佑,你需要使用 object 表達(dá)式:

class MainViewModel {
    var repoModel: RepoModel = RepoModel()
    var text: String = ""
    var isLoading: Boolean = false

    fun refresh() {
        repoModel.refreshData( object : OnDataReadyCallback {
        override fun onDataReady(data: String) {
            text = data
        })
    }
}

當(dāng)我們調(diào)用 refresh锯茄,我們應(yīng)該把 view 改成加載中的狀態(tài),并且一旦獲取到數(shù)據(jù)茶没,就把 isLoading 設(shè)置為 false肌幽。同時(shí),我們應(yīng)該把 text 改成ObservableField<String>礁叔,把 isLoading 改成 ObservableField<Boolean>牍颈。ObservableField 是一個(gè)數(shù)據(jù)綁定庫(kù)中的類,我們可以用它來(lái)創(chuàng)建一個(gè)可觀察對(duì)象琅关。它把對(duì)象包裹成可被觀察的煮岁。

class MainViewModel {
    var repoModel: RepoModel = RepoModel()

    val text = ObservableField<String>()

    val isLoading = ObservableField<Boolean>()

    fun refresh(){
        isLoading.set(true)
        repoModel.refreshData(object : OnDataReadyCallback {
            override fun onDataReady(data: String) {
                isLoading.set(false)
                text.set(data)
            }
        })
    }
}

注意到我使用了 val 而不是 var,因?yàn)槲覀儍H在字段里改變它的值涣易,而不是字段本身画机。如果你想要初始化的話,應(yīng)該這樣:

val text = ObservableField("old data")
val isLoading = ObservableField(false)

現(xiàn)在改變我們的布局新症,讓它可以觀察 textisLoading 枪萄。首先问慎,我們會(huì)綁定 MainViewModel 而不是 Repository:

<data>
    <variable
        name="viewModel"
        type="me.fleka.modernandroidapp.MainViewModel" />
</data>

然后:

  • 讓 TextView 觀察 MainViewModel 中的 text

  • 添加一個(gè) ProgressBar,當(dāng) isLoading 為 true 時(shí)才會(huì)顯示

  • 添加一個(gè) Button妇菱,在 onClick 中調(diào)用 refresh 函數(shù),僅當(dāng) isLoading 為 false 時(shí)才能點(diǎn)擊

...

        <TextView
            android:id="@+id/repository_name"
            android:text="@{viewModel.text}"
            ...
            />

        ...
        <ProgressBar
            android:id="@+id/loading"
            android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}"
            ...
            />

        <Button
            android:id="@+id/refresh_button"
            android:onClick="@{() -> viewModel.refresh()}"
            android:clickable="@{viewModel.isLoading ? false : true}"
            />
...

如果現(xiàn)在運(yùn)行的話长踊,你會(huì)得到一個(gè)錯(cuò)誤,因?yàn)槿绻麤](méi)有導(dǎo)入 View 的話,View.VISIBLEView.GONE 不能使用侯繁。所以,我們應(yīng)該導(dǎo)入:

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

        <variable
            name="viewModel"
            type="me.fleka.modernandroidapp.MainViewModel" />
</data>

好泡躯,布局完成了≈梗現(xiàn)在我們來(lái)完成綁定。如我們所說(shuō)的 View 應(yīng)該持有 ViewModel 的實(shí)例:

class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding

    var mainViewModel = MainViewModel()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.viewModel = mainViewModel
        binding.executePendingBindings()

    }
}

最終较剃,我們的運(yùn)行效果:


可以看到 old data 變成了 new data咕别。

這就是最簡(jiǎn)單的 MVVM 示例。

還有一個(gè)問(wèn)題写穴,讓我們來(lái)旋轉(zhuǎn)手機(jī):


new data 又變成了 old data惰拱。怎么可能?看下 Activity 的生命周期:

一旦你旋轉(zhuǎn)屏幕确垫,新的 Activity 實(shí)例就會(huì)創(chuàng)建弓颈,onCreate() 方法會(huì)被調(diào)用。現(xiàn)在删掀,看下我們的 Activity:

class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding

    var mainViewModel = MainViewModel()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.viewModel = mainViewModel
        binding.executePendingBindings()

    }
}

如你所見(jiàn)翔冀,一旦創(chuàng)建了一個(gè)新的 Activity 實(shí)例,MainViewModel 的實(shí)例也被創(chuàng)建了披泪。如果每次重新創(chuàng)建的 MainActivity 都有一個(gè)相同的
MainViewModel 實(shí)例會(huì)不會(huì)好點(diǎn)纤子?

隆重推出生命周期感知組件

因?yàn)樵S多的開(kāi)發(fā)者面臨這個(gè)問(wèn)題,Android 框架團(tuán)隊(duì)的開(kāi)發(fā)者決定創(chuàng)建一個(gè)庫(kù)來(lái)幫我們解決這個(gè)問(wèn)題款票。ViewModel 類是其中一個(gè)控硼。我們所有的 ViewModel 類都應(yīng)該繼承自它。

讓我們的 MainViewModel 繼承來(lái)自于生命周期感知組件的 ViewModel艾少。首先卡乾,我們需要在 build.gradle 文件中添加依賴:

dependencies {
    ... 

    implementation "android.arch.lifecycle:runtime:1.0.0-alpha9"
    implementation "android.arch.lifecycle:extensions:1.0.0-alpha9"
    kapt "android.arch.lifecycle:compiler:1.0.0-alpha9"
}

然后繼承 ViewModel:

package me.fleka.modernandroidapp

import android.arch.lifecycle.ViewModel

class MainViewModel : ViewModel() {
    ...
}

在 MainActivity 的 onCreate() 方法中,你應(yīng)該這樣:

class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
        binding.executePendingBindings()

    }
}

注意到我們已經(jīng)沒(méi)有創(chuàng)建 MainViewModel 的實(shí)例了「抗唬現(xiàn)在幔妨,我們從 ViewModelProviders 中獲取它。ViewModelProviders 是一個(gè)功能類谍椅,有一個(gè)獲取 ViewModelProvider 的方法误堡。和作用范圍相關(guān),所以雏吭,如果你在 Activity 調(diào)用 ViewModelProviders.of(this) 锁施,那么你的
ViewModel 會(huì)存活直到 Activity 被銷(xiāo)毀(被銷(xiāo)毀而且沒(méi)有被重新創(chuàng)建)。類似地,如果你在 Fragment 中調(diào)用悉抵,你的 ViewModel 也會(huì)存活直到 Fragment 被銷(xiāo)毀肩狂。看下下面的圖解:

ViewModelProvider 的職責(zé)是在第一次調(diào)用的時(shí)候創(chuàng)建實(shí)例基跑,并在 Activity/Fragment 重新創(chuàng)建時(shí)返回舊的實(shí)例婚温。

不要混淆了:

MainViewModel::class.java

在 Kotlin 中,如果你僅僅寫(xiě)成:

MainViewModel::class

它會(huì)返回一個(gè)KClass媳否,和 Java 中的 Class 不一樣。因此荆秦,如果我們加上.java篱竭,它表示:

返回一個(gè)和給定的 KClass 實(shí)例關(guān)聯(lián)的Java 類實(shí)例。

現(xiàn)在讓我們來(lái)旋轉(zhuǎn)一下屏幕看看會(huì)發(fā)生什么:


我們的數(shù)據(jù)和旋轉(zhuǎn)之前一樣步绸。

上一篇文章中掺逼,我說(shuō)過(guò)我們的 app 將會(huì)獲取 GitHub 倉(cāng)庫(kù)列表并展示。要想完成它瓤介,我們需要添加
getRepositories 函數(shù)吕喘,它會(huì)返回一個(gè)偽造的倉(cāng)庫(kù)列表:

class RepoModel {

    fun refreshData(onDataReadyCallback: OnDataReadyCallback) {
        Handler().postDelayed({ onDataReadyCallback.onDataReady("new data") },2000)
    }

    fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) {
        var arrayList = ArrayList<Repository>()
        arrayList.add(Repository("First", "Owner 1", 100 , false))
        arrayList.add(Repository("Second", "Owner 2", 30 , true))
        arrayList.add(Repository("Third", "Owner 3", 430 , false))

        Handler().postDelayed({ onRepositoryReadyCallback.onDataReady(arrayList) },2000)
    }
}

interface OnDataReadyCallback {
    fun onDataReady(data : String)
}

interface OnRepositoryReadyCallback {
    fun onDataReady(data : ArrayList<Repository>)
}

同時(shí),我們的 MainViewModel 會(huì)有一個(gè)調(diào)用 getRepositories 的函數(shù):

class MainViewModel : ViewModel() {
    ...
    var repositories = ArrayList<Repository>()

    fun refresh(){
        ...
    }

    fun loadRepositories(){
        isLoading.set(true)
        repoModel.getRepositories(object : OnRepositoryReadyCallback{
            override fun onDataReady(data: ArrayList<Repository>) {
                isLoading.set(false)
                repositories = data
            }
        })
    }
}

最后刑桑,我們需要在 RecyclerView 中展示這些倉(cāng)庫(kù)氯质。要這么做,我們必須:

  • 創(chuàng)建 rv_item_repository.xml 布局

  • activity_main.xml 布局中添加 RecyclerView

  • 創(chuàng)建 RepositoryRecyclerViewAdapter

  • set adapter

創(chuàng)建 rv_item_repository.xml 我將使用 CardView 庫(kù)祠斧,所以我們要在 build.gradle 中添加依賴:

implementation 'com.android.support:cardview-v7:26.0.1'

布局看起來(lái)是這樣的:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

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

        <variable
            name="repository"
            type="me.fleka.modernandroidapp.uimodels.Repository" />
    </data>

    <android.support.v7.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="96dp"
        android:layout_margin="8dp">

        <android.support.constraint.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <TextView
                android:id="@+id/repository_name"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="16dp"
                android:layout_marginStart="16dp"
                android:text="@{repository.repositoryName}"
                android:textSize="20sp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintHorizontal_bias="0.0"
                app:layout_constraintLeft_toLeftOf="parent"
                app:layout_constraintRight_toRightOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintVertical_bias="0.083"
                tools:text="Modern Android App" />

            <TextView
                android:id="@+id/repository_has_issues"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="16dp"
                android:layout_marginStart="16dp"
                android:layout_marginTop="8dp"
                android:text="@string/has_issues"
                android:textStyle="bold"
                android:visibility="@{repository.hasIssues ? View.VISIBLE : View.GONE}"
                app:layout_constraintBottom_toBottomOf="@+id/repository_name"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintHorizontal_bias="1.0"
                app:layout_constraintStart_toEndOf="@+id/repository_name"
                app:layout_constraintTop_toTopOf="@+id/repository_name"
                app:layout_constraintVertical_bias="1.0" />

            <TextView
                android:id="@+id/repository_owner"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginBottom="8dp"
                android:layout_marginEnd="16dp"
                android:layout_marginStart="16dp"
                android:text="@{repository.repositoryOwner}"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/repository_name"
                app:layout_constraintVertical_bias="0.0"
                tools:text="Mladen Rakonjac" />

            <TextView
                android:id="@+id/number_of_starts"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginBottom="8dp"
                android:layout_marginEnd="16dp"
                android:layout_marginStart="16dp"
                android:layout_marginTop="8dp"
                android:text="@{String.valueOf(repository.numberOfStars)}"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintHorizontal_bias="1"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/repository_owner"
                app:layout_constraintVertical_bias="0.0"
                tools:text="0 stars" />

        </android.support.constraint.ConstraintLayout>

    </android.support.v7.widget.CardView>

</layout>

下一步闻察,在 activity_main.xml 中添加 RecyclerView。別忘了添加依賴:

implementation 'com.android.support:recyclerview-v7:26.0.1'

接下來(lái)是布局:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

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

        <variable
            name="viewModel"
            type="me.fleka.modernandroidapp.MainViewModel" />
    </data>

    <android.support.constraint.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context="me.fleka.modernandroidapp.MainActivity">

        <ProgressBar
            android:id="@+id/loading"
            android:layout_width="48dp"
            android:layout_height="48dp"
            android:indeterminate="true"
            android:visibility="@{viewModel.isLoading ? View.VISIBLE : View.GONE}"
            app:layout_constraintBottom_toTopOf="@+id/refresh_button"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <android.support.v7.widget.RecyclerView
            android:id="@+id/repository_rv"
            android:layout_width="0dp"
            android:layout_height="0dp"
            android:indeterminate="true"
            android:visibility="@{viewModel.isLoading ? View.GONE : View.VISIBLE}"
            app:layout_constraintBottom_toTopOf="@+id/refresh_button"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            tools:listitem="@layout/rv_item_repository" />

        <Button
            android:id="@+id/refresh_button"
            android:layout_width="160dp"
            android:layout_height="40dp"
            android:layout_marginBottom="8dp"
            android:layout_marginEnd="8dp"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:onClick="@{() -> viewModel.loadRepositories()}"
            android:clickable="@{viewModel.isLoading ? false : true}"
            android:text="Refresh"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="1.0" />

    </android.support.constraint.ConstraintLayout>

</layout>

我們刪除了一些之前創(chuàng)建的 TextView 元素琢锋,并且按鈕現(xiàn)在觸發(fā)的是 loadRepositories 而不是 refresh:

<Button
    android:id="@+id/refresh_button"
    android:onClick="@{() -> viewModel.loadRepositories()}" 
    ...
    />

刪掉 MainViewModel 中的 refresh 和 RepoModel 中的 refreshData 函數(shù)辕漂。

現(xiàn)在,為 RecyclerView 添加一個(gè)適配器:

class RepositoryRecyclerViewAdapter(private var items: ArrayList<Repository>,
                                    private var listener: OnItemClickListener)
    : RecyclerView.Adapter<RepositoryRecyclerViewAdapter.ViewHolder>() {

    override fun onCreateViewHolder(parent: ViewGroup?, viewType: Int): ViewHolder {
        val layoutInflater = LayoutInflater.from(parent?.context)
        val binding = RvItemRepositoryBinding.inflate(layoutInflater, parent, false)
        return ViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int)
            = holder.bind(items[position], listener)

    override fun getItemCount(): Int = items.size

    interface OnItemClickListener {
        fun onItemClick(position: Int)
    }

    class ViewHolder(private var binding: RvItemRepositoryBinding) :
            RecyclerView.ViewHolder(binding.root) {

        fun bind(repo: Repository, listener: OnItemClickListener?) {
            binding.repository = repo
            if (listener != null) {
                binding.root.setOnClickListener({ _ -> listener.onItemClick(layoutPosition) })
            }

            binding.executePendingBindings()
        }
    }

}

ViewHolder 接受 RvItemRepositoryBinding 類型的實(shí)例吴超,而不是 View 類型钉嘹,這樣我們就能在 ViewHolder 中為每一項(xiàng)實(shí)現(xiàn)數(shù)據(jù)綁定。同時(shí)鲸阻,別被下面一行函數(shù)給弄迷糊了:

override fun onBindViewHolder(holder: ViewHolder, position: Int)            = holder.bind(items[position], listener)

它只是這種形式的縮寫(xiě):

override fun onBindViewHolder(holder: ViewHolder, position: Int){
    return holder.bind(items[position], listener)
}

并且 items[position] 實(shí)現(xiàn)了索引操作跋涣,和 items.get(position) 是一樣的。

還有一行可能會(huì)迷惑的代碼:

binding.root.setOnClickListener({ _ -> listener.onItemClick(layoutPosition) })

你可以用_來(lái)代替參數(shù)赘娄,如果你不需要用它的話仆潮。

我們添加了適配器,但在 MainActivity 中還沒(méi)有把它設(shè)置到 RecyclerView 中:

class MainActivity : AppCompatActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener {

    lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
        binding.viewModel = viewModel
        binding.executePendingBindings()

        binding.repositoryRv.layoutManager = LinearLayoutManager(this)
        binding.repositoryRv.adapter = RepositoryRecyclerViewAdapter(viewModel.repositories, this)

    }

    override fun onItemClick(position: Int) {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }
}

讓我們來(lái)運(yùn)行試試:


很奇怪遣臼。發(fā)生了啥性置?

  • Activity 被創(chuàng)建了,所以新的適配器也創(chuàng)建了揍堰,但里面的 repositories 實(shí)際上是空的

  • 我們點(diǎn)擊了按鈕

  • 調(diào)用了 loadRepositories 函數(shù)鹏浅,顯示了進(jìn)度條

  • 2 秒后嗅义,我們拿到了倉(cāng)庫(kù)列表,隱藏了進(jìn)度條隐砸,但倉(cāng)庫(kù)列表沒(méi)顯示之碗。因?yàn)闆](méi)有調(diào)用 notifyDataSetChanged

  • 一旦我們旋轉(zhuǎn)屏幕,新的 Activity 被創(chuàng)建季希,帶有倉(cāng)庫(kù)參數(shù)的新的適配器也被創(chuàng)建了褪那,所以實(shí)際上 viewModel 是有數(shù)據(jù)的。

那么式塌,MainViewModel 該怎樣才能通知 MainActivity 更新了項(xiàng)目博敬,好讓我們可以調(diào)用 notifyDataSetChanged 呢?

不應(yīng)該這樣做峰尝。

這點(diǎn)非常重要:MainViewModel 不應(yīng)該知道任何關(guān)于MainActivity的東西偏窝。

MainActivity 才擁有 MainViewModel實(shí)例,所以應(yīng)該讓它來(lái)監(jiān)聽(tīng)數(shù)據(jù)變化并通知Adapter武学。那怎么做祭往?

我們可以觀察repositories,這樣一旦數(shù)據(jù)改變了火窒,我們就能改變我們的 adapter硼补。

這個(gè)方案中可能出錯(cuò)的地方?

我們先來(lái)看看下面的場(chǎng)景:

  • 在 MainActivity 中沛鸵,我們觀察了 repositories:一旦改變括勺,我們調(diào)用 notifyDataSetChanged

  • 我們點(diǎn)擊了按鈕

  • 當(dāng)我們等待數(shù)據(jù)改變時(shí),MainActivity 可能會(huì)因?yàn)?strong>配置改變而被重新創(chuàng)建

  • 我們的 MainViewModel 依然存在

  • 2 秒后曲掰,我們的 repositories 獲得新的數(shù)據(jù)疾捍,然后通知觀察者數(shù)據(jù)已經(jīng)改變

  • 觀察者嘗試調(diào)用不再存在的 adapternotifyDataSetChanged,因?yàn)? MainActivity 已經(jīng)重新創(chuàng)建了

所以栏妖,我們的方案還不夠好乱豆。

介紹 LiveData

LiveData 是另一個(gè)生命周期感知的組件。它能觀察 View 的生命周期吊趾。這樣一來(lái)宛裕,一旦 Activity 因?yàn)榕渲酶淖兌讳N(xiāo)毀,LiveData 就能夠知道论泛,它也就能夠從被銷(xiāo)毀的 Activity 中回收觀察者揩尸。

讓我們?cè)?MainViewModel 中實(shí)現(xiàn)它:

class MainViewModel : ViewModel() {
    var repoModel: RepoModel = RepoModel()

    val text = ObservableField("old data")

    val isLoading = ObservableField(false)

    var repositories = MutableLiveData<ArrayList<Repository>>()

    fun loadRepositories() {
        isLoading.set(true)
        repoModel.getRepositories(object : OnRepositoryReadyCallback {
            override fun onDataReady(data: ArrayList<Repository>) {
                isLoading.set(false)
                repositories.value = data
            }
        })
    }
}

然后在 MainActivity 中觀察改動(dòng):

class MainActivity : LifecycleActivity(), RepositoryRecyclerViewAdapter.OnItemClickListener {

    private lateinit var binding: ActivityMainBinding
    private val repositoryRecyclerViewAdapter = RepositoryRecyclerViewAdapter(arrayListOf(), this)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        val viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
        binding.viewModel = viewModel
        binding.executePendingBindings()

        binding.repositoryRv.layoutManager = LinearLayoutManager(this)
        binding.repositoryRv.adapter = repositoryRecyclerViewAdapter
        viewModel.repositories.observe(this,
                Observer<ArrayList<Repository>> { it?.let{ repositoryRecyclerViewAdapter.replaceData(it)} })

    }

    override fun onItemClick(position: Int) {
        TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
    }
}

it關(guān)鍵字是什么意思呢?如果某個(gè)函數(shù)只有一個(gè)參數(shù)屁奏,那么那個(gè)參數(shù)就可以用it來(lái)代替岩榆。假設(shè)我們有個(gè)乘以 2 的 lambda 表達(dá)式:

((a) -> 2 * a) 

我們可以替換成這樣:

(it * 2)

如果你現(xiàn)在運(yùn)行,你會(huì)看到一切都正常工作了:


為什么相比 MVP 我更傾向于 MVVM?

  • 沒(méi)有 View 的無(wú)聊的接口勇边,因?yàn)?ViewModel 沒(méi)有 View 的引用犹撒。

  • 沒(méi)有 Presenter 的無(wú)聊的接口,因?yàn)楦静恍枰?/p>

  • 更容易處理配置改動(dòng)粒褒。

  • 使用 MVVM识颊,Activity,F(xiàn)ragment 里的代碼更少奕坟。

存儲(chǔ)庫(kù)模式

image

我之前說(shuō)過(guò)祥款,Model 是準(zhǔn)備數(shù)據(jù)的抽象層。通常执赡,它包括存儲(chǔ)和數(shù)據(jù)類镰踏。每個(gè)實(shí)體(數(shù)據(jù))類都應(yīng)該對(duì)應(yīng)存儲(chǔ)類。例如沙合,如果我們有個(gè) UserPost 數(shù)據(jù)類,我們應(yīng)該也有 UserRepositoryPostRepository 類跌帐。所有的數(shù)據(jù)都應(yīng)該直接從它們中獲取首懈。我們永遠(yuǎn)不應(yīng)該在 View 或者 ViewModel 中調(diào)用 Shared Preferences 或者 DB 實(shí)例。

所以谨敛,我們可以重命名我們的 RepoModel 為 GitRepoRepository究履,GitRepo 從 GitHub 倉(cāng)庫(kù)中獲取,Repository 從存儲(chǔ)庫(kù)模式中獲取脸狸。

class GitRepoRepository {

    fun getGitRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) {
        var arrayList = ArrayList<Repository>()
        arrayList.add(Repository("First", "Owner 1", 100, false))
        arrayList.add(Repository("Second", "Owner 2", 30, true))
        arrayList.add(Repository("Third", "Owner 3", 430, false))

        Handler().postDelayed({ onRepositoryReadyCallback.onDataReady(arrayList) }, 2000)
    }
}

interface OnRepositoryReadyCallback {
    fun onDataReady(data: ArrayList<Repository>)
}

MainViewModelGitRepoRepsitories 獲取 GitHub 倉(cāng)庫(kù)列表最仑,但 GitRepoRepositories 又是從哪來(lái)的呢?你可以在 repository 中調(diào)用
client 或者 DB 實(shí)例直接去拿炊甲,但這仍然不是最佳實(shí)踐泥彤。你必須盡可能地模塊化你的 app。如果你用不同的客戶端卿啡,用 Retrofit 替代 Volley 呢吟吝?如果你在里面寫(xiě)了一點(diǎn)邏輯,你很難去重構(gòu)它颈娜。你的 repository 不需要知道你正在使用哪一個(gè)客戶端來(lái)獲取遠(yuǎn)程數(shù)據(jù)剑逃。

  • repository 需要知道的唯一一件事是數(shù)據(jù)從遠(yuǎn)程還是本地獲取的。不需要知道我們是如何從遠(yuǎn)程或者本地獲取官辽。

  • ViewModel 需要的唯一一件事是數(shù)據(jù)

  • View 需要做的唯一一件事就是展示數(shù)據(jù)

我剛開(kāi)始開(kāi)發(fā) Android 時(shí)蛹磺,我曾經(jīng)想知道應(yīng)用時(shí)如何離線工作的,如何同步數(shù)據(jù)同仆。好的應(yīng)用架構(gòu)允許我們讓這些變得簡(jiǎn)單萤捆。例如,當(dāng) ViewModel 中的 loadRepositories 被調(diào)用時(shí),如果有連接網(wǎng)絡(luò)鳖轰,GitRepoRepositories 就能從遠(yuǎn)程數(shù)據(jù)源中獲取數(shù)據(jù)清酥,然后保存到本地。一旦手機(jī)處于離線模式蕴侣,GitRepoRepository 就能從本地?cái)?shù)據(jù)源獲取數(shù)據(jù)焰轻。這樣一來(lái),Repositories 就應(yīng)該有 RemoteDataSourceLocalDataSource 的實(shí)例昆雀,以及處理數(shù)據(jù)從哪里來(lái)的邏輯辱志。

讓我們先來(lái)添加本地?cái)?shù)據(jù)源:

class GitRepoLocalDataSource {

    fun getRepositories(onRepositoryReadyCallback: OnRepoLocalReadyCallback) {
        var arrayList = ArrayList<Repository>()
        arrayList.add(Repository("First From Local", "Owner 1", 100, false))
        arrayList.add(Repository("Second From Local", "Owner 2", 30, true))
        arrayList.add(Repository("Third From Local", "Owner 3", 430, false))

        Handler().postDelayed({ onRepositoryReadyCallback.onLocalDataReady(arrayList) }, 2000)
    }

    fun saveRepositories(arrayList: ArrayList<Repository>){
        //todo save repositories in DB
    }
}

interface OnRepoLocalReadyCallback {
    fun onLocalDataReady(data: ArrayList<Repository>)
}

我們有兩個(gè)方法:首先返回偽造的本地?cái)?shù)據(jù),其次就是保存數(shù)據(jù)狞膘。

現(xiàn)在來(lái)添加遠(yuǎn)程數(shù)據(jù)源:

class GitRepoRemoteDataSource {

    fun getRepositories(onRepositoryReadyCallback: OnRepoRemoteReadyCallback) {
        var arrayList = ArrayList<Repository>()
        arrayList.add(Repository("First from remote", "Owner 1", 100, false))
        arrayList.add(Repository("Second from remote", "Owner 2", 30, true))
        arrayList.add(Repository("Third from remote", "Owner 3", 430, false))

        Handler().postDelayed({ onRepositoryReadyCallback.onRemoteDataReady(arrayList) }, 2000)
    }
}

interface OnRepoRemoteReadyCallback {
    fun onRemoteDataReady(data: ArrayList<Repository>)
}

這個(gè)只有一個(gè)方法返回偽造的遠(yuǎn)程數(shù)據(jù)揩懒。

現(xiàn)在可以在我們的 repository 中添加一些邏輯了:

class GitRepoRepository {

    val localDataSource = GitRepoLocalDataSource()
    val remoteDataSource = GitRepoRemoteDataSource()

    fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) {
       remoteDataSource.getRepositories( object : OnRepoRemoteReadyCallback {
           override fun onDataReady(data: ArrayList<Repository>) {
               localDataSource.saveRepositories(data)
               onRepositoryReadyCallback.onDataReady(data)
           }

       })
    }
}

interface OnRepositoryReadyCallback {
    fun onDataReady(data: ArrayList<Repository>)
}

所以,分離數(shù)據(jù)源可以讓我們更容易把數(shù)據(jù)保存到本地挽封。

如果你只需要從網(wǎng)絡(luò)獲取數(shù)據(jù)已球,你仍需要存儲(chǔ)庫(kù)模式嗎?是的辅愿。這會(huì)讓你的代碼更容易測(cè)試智亮,其他開(kāi)發(fā)者也能更好地理解你的代碼,你也可以更快地維護(hù)点待。:)

Android 管理封裝器

如果你想要在 GitRepoRepository 中檢查網(wǎng)絡(luò)連接阔蛉,這樣你就可以知道用哪個(gè)數(shù)據(jù)源獲取數(shù)據(jù)呢?我們已經(jīng)說(shuō)過(guò)我們不應(yīng)該在 ViewModelsModels里放任何 Android 相關(guān)的代碼癞埠,那么怎么處理這個(gè)問(wèn)題呢状原?

讓我們來(lái)創(chuàng)造一個(gè)網(wǎng)絡(luò)連接的封裝器:

class NetManager(private var applicationContext: Context) {
    private var status: Boolean? = false

    val isConnectedToInternet: Boolean?
        get() {
            val conManager = applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
            val ni = conManager.activeNetworkInfo
            return ni != null && ni.isConnected
        }
}

如果我們?cè)?manifest 中添加權(quán)限的話上面的代碼就可以起作用了:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

但是因?yàn)槲覀儧](méi)有 context,如何在 Repository 中創(chuàng)建實(shí)例呢苗踪?我們可以在構(gòu)造器中得到:

class GitRepoRepository (context: Context){

    val localDataSource = GitRepoLocalDataSource()
    val remoteDataSource = GitRepoRemoteDataSource()
    val netManager = NetManager(context)

    fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) {
        remoteDataSource.getRepositories(object : OnRepoRemoteReadyCallback {
            override fun onDataReady(data: ArrayList<Repository>) {
                localDataSource.saveRepositories(data)
                onRepositoryReadyCallback.onDataReady(data)
            }

        })
    }
}

interface OnRepositoryReadyCallback {
    fun onDataReady(data: ArrayList<Repository>)
}

我們之前在 ViewModel 中創(chuàng)建了 GitRepoRepository 的實(shí)例颠区,因?yàn)槲覀兊?NetManager 需要一個(gè) Context,我們?cè)鯓釉?ViewModel 中拿到徒探?你可以從生命周期感知的組件庫(kù)中拿到 AndroidViewModel瓦呼,它有一個(gè) context。這個(gè) context 是應(yīng)用的上下文测暗,而不是 Activity 的:

class MainViewModel : AndroidViewModel  {

    constructor(application: Application) : super(application)

    var gitRepoRepository: GitRepoRepository = GitRepoRepository(NetManager(getApplication()))

    val text = ObservableField("old data")

    val isLoading = ObservableField(false)

    var repositories = MutableLiveData<ArrayList<Repository>>()

    fun loadRepositories() {
        isLoading.set(true)
        gitRepoRepository.getRepositories(object : OnRepositoryReadyCallback {
            override fun onDataReady(data: ArrayList<Repository>) {
                isLoading.set(false)
                repositories.value = data
            }
        })
    }
}

這一行:

constructor(application: Application) : super(application)

我們?yōu)?MainViewModel 定義了一個(gè)構(gòu)造器央串。這是必要的,因?yàn)?AndroidViewModel 在它的構(gòu)造器中請(qǐng)求了 Application 實(shí)例碗啄。所以在我們的構(gòu)造器中可以調(diào)用 super 方法质和,這樣被我們繼承的 AndroidViewModel 的構(gòu)造器就會(huì)被調(diào)用。

注意:我們可以用一行代碼來(lái)表示:

class MainViewModel(application: Application) : AndroidViewModel(application) {
... 
}

現(xiàn)在稚字,我們?cè)?GitRepoRepository 中有了 NetManager 實(shí)例饲宿,我們就可以檢查網(wǎng)絡(luò)連接了:

class GitRepoRepository(val netManager: NetManager) {

    val localDataSource = GitRepoLocalDataSource()
    val remoteDataSource = GitRepoRemoteDataSource()

    fun getRepositories(onRepositoryReadyCallback: OnRepositoryReadyCallback) {

        netManager.isConnectedToInternet?.let {
            if (it) {
                remoteDataSource.getRepositories(object : OnRepoRemoteReadyCallback {
                    override fun onRemoteDataReady(data: ArrayList<Repository>) {
                        localDataSource.saveRepositories(data)
                        onRepositoryReadyCallback.onDataReady(data)
                    }
                })
            } else {
                localDataSource.getRepositories(object : OnRepoLocalReadyCallback {
                    override fun onLocalDataReady(data: ArrayList<Repository>) {
                        onRepositoryReadyCallback.onDataReady(data)
                    }
                })
            }
        }

    }
}

interface OnRepositoryReadyCallback {
    fun onDataReady(data: ArrayList<Repository>)
}

如果我們連接了網(wǎng)絡(luò)厦酬,我們就獲取遠(yuǎn)程數(shù)據(jù)然后保存到本地。否則瘫想,我們就從本地拿數(shù)據(jù)仗阅。

Kotlin 筆記let 操作符會(huì)檢查是否為空并返回一個(gè) it 值。

接下來(lái)的文章中国夜,我會(huì)介紹依賴注入减噪,為什么在 ViewModel 中創(chuàng)建 repository 實(shí)例是不好的,以及如何避免使用 AndroidViewModel车吹。

英文原文:Modern Android development with Kotlin (Part 2)
舊文推薦:
用 Kotlin 開(kāi)發(fā)現(xiàn)代 Android 項(xiàng)目 Part 1
Kotlin 讓使用 Android API 變得輕松
“Effective Java” 可能對(duì) Kotlin 的設(shè)計(jì)造成了怎樣的影響——第一部分

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末筹裕,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子窄驹,更是在濱河造成了極大的恐慌朝卒,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,590評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件乐埠,死亡現(xiàn)場(chǎng)離奇詭異抗斤,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)丈咐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,157評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)豪治,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人扯罐,你說(shuō)我怎么就攤上這事》骋拢” “怎么了歹河?”我有些...
    開(kāi)封第一講書(shū)人閱讀 169,301評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)花吟。 經(jīng)常有香客問(wèn)我秸歧,道長(zhǎng),這世上最難降的妖魔是什么衅澈? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 60,078評(píng)論 1 300
  • 正文 為了忘掉前任键菱,我火速辦了婚禮,結(jié)果婚禮上今布,老公的妹妹穿的比我還像新娘经备。我一直安慰自己,他們只是感情好部默,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,082評(píng)論 6 398
  • 文/花漫 我一把揭開(kāi)白布侵蒙。 她就那樣靜靜地躺著,像睡著了一般傅蹂。 火紅的嫁衣襯著肌膚如雪纷闺。 梳的紋絲不亂的頭發(fā)上算凿,一...
    開(kāi)封第一講書(shū)人閱讀 52,682評(píng)論 1 312
  • 那天,我揣著相機(jī)與錄音犁功,去河邊找鬼氓轰。 笑死,一個(gè)胖子當(dāng)著我的面吹牛浸卦,可吹牛的內(nèi)容都是我干的署鸡。 我是一名探鬼主播,決...
    沈念sama閱讀 41,155評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼镐躲,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼储玫!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起萤皂,我...
    開(kāi)封第一講書(shū)人閱讀 40,098評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤撒穷,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后裆熙,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體端礼,經(jīng)...
    沈念sama閱讀 46,638評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,701評(píng)論 3 342
  • 正文 我和宋清朗相戀三年入录,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了蛤奥。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,852評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡僚稿,死狀恐怖凡桥,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情蚀同,我是刑警寧澤缅刽,帶...
    沈念sama閱讀 36,520評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站蠢络,受9級(jí)特大地震影響衰猛,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜刹孔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,181評(píng)論 3 335
  • 文/蒙蒙 一啡省、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧髓霞,春花似錦卦睹、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,674評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至薪捍,卻和暖如春笼痹,著一層夾襖步出監(jiān)牢的瞬間配喳,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,788評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工凳干, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留晴裹,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 49,279評(píng)論 3 379
  • 正文 我出身青樓救赐,卻偏偏與公主長(zhǎng)得像涧团,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子经磅,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,851評(píng)論 2 361

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