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

簡評:目前吟吝,在 Android 開發(fā)中找到一個覆蓋所有的新技術(shù)的項(xiàng)目難如登天篡九,所以作者決定自己寫一個问窃。本文所以使用的技術(shù)包括:
0. Android Studio 3, beta1
1. Kotlin 語言
2. 構(gòu)建變體
3. ConstraintLayout
4. 數(shù)據(jù)綁定庫
5. MVVM 架構(gòu) + 存儲庫模式(使用映射器)+ Android Manager Wrappers(Part 2)
6. RxJava2 及它如何在架構(gòu)中起作用
7. Dagger 2.11万俗,什么是依賴注入康二,為什么需要它
8. 改造(使用 Rx Java2)
9. Room(使用 Rx Java2)

我們的 app 看起來是什么樣的嫡纠?

我們的 app 將會是最簡單的懒叛,將使用所有上面提到的技術(shù)丸冕,只用一個功能:拉取 GitHub 上的所有 google 案例倉庫,把這些數(shù)據(jù)保存到本地?cái)?shù)據(jù)庫并展示給用戶薛窥。

我將盡可能地解釋每一行代碼胖烛。你可以從 github 上跟進(jìn)我提交的代碼。

讓我們一起動手:

0. Android Studio

要安裝 Android Studio 3 beta1(現(xiàn)在已發(fā)布正式版)诅迷,你要進(jìn)入這個頁面佩番。

注意:如果你想要和之前安裝的某個版本共存,在 Mac 上你應(yīng)該在應(yīng)用文件夾中重命名舊的版本竟贯,如“Android Studio Old”答捕。你可以在這里找到更多信息,包括 Windows 和 Linux屑那。

Android Studio 現(xiàn)已支持 Kotlin拱镐。去創(chuàng)建 Android 項(xiàng)目,你會發(fā)現(xiàn)新東西:支持 Kotlin 的標(biāo)簽可選框持际。它是默認(rèn)選中的沃琅。按兩下 next,然后選擇 Empty Activity蜘欲,這樣就完成了益眉。

1. Kotlin

看看 MainActivity.kt:

package me.fleka.modernandroidapp

import android.support.v7.app.AppCompatActivity
import android.os.Bundle

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

kt 后綴意味著 Kotlin 文件。

MainActivity: AppCompatActivity() 意味著我們正在繼承 AppCompatActivity。

此外郭脂,所有的方法都有一個 fun 關(guān)鍵字年碘,在 Kotlin 中你可以不必使用,取決于你的喜好展鸡。你必須使用
override 關(guān)鍵字屿衅,而不是注解。

那么莹弊,savedInstanceState: Bundle? 中的 涤久? 表示什么意思呢?意味著 savedInstanceState 參數(shù)可能是 Bundle 類型或者為 null忍弛。Kotlin 是空安全的語言响迂。如果你定義了:

var a : String

你將得到一個編譯錯誤,因?yàn)?a 必須被初始化细疚,它不能為 null 蔗彤。意味著你必須這樣寫:

var a : String = "Init value"

如果你像下面這樣寫,同樣你將得到一個編譯錯誤:

a = null

要讓 a 成為可空的疯兼,你必須這樣:

var a : String?

為什么這是 Kotlin 語言的一個重要功能呢幕与?因?yàn)樗鼛臀覀儽苊饬?a target="_blank" rel="nofollow">空指針異常。Android 開發(fā)者受夠了空指針異常镇防。即便是 null 的創(chuàng)造者啦鸣,Tony Hoare 先生,也為發(fā)明出 null 而道歉了来氧。假設(shè)我們有一個可空的 nameTextView诫给。以下代碼將會造成 NPE,如果它是 null 的話:

nameTextView.setEnabled(true)

而 Kotlin 將不允許我們做類似這樣的事啦扬。它強(qiáng)制我們使用 ? 或者 !! 操作符中狂,如果我們使用 ? 操作符:

nameTextView?.setEnabled(true)

這行代碼僅當(dāng) nameTextView 不為 null 才會執(zhí)行。換句話說扑毡,如果我們使用了 !! 操作符:

nameTextView!!.setEnabled(true)

如果 nameTextView 為 null胃榕,它將報(bào) NPE。想冒險(xiǎn)的人才會用 :)

這只是有關(guān) Kotlin 的一點(diǎn)小小的介紹瞄摊,隨著我們深入勋又,后面不再介紹其他 Kotlin 特性代碼。

2. 構(gòu)建變體

在開發(fā)中换帜,你通常會有不同的環(huán)境楔壤。最常見的就是測試生產(chǎn)環(huán)境。這些環(huán)境在服務(wù)器 url惯驼,圖標(biāo)蹲嚣,名字递瑰,目標(biāo) api 上等等有所不同。在 fleka隙畜,我們的每一個項(xiàng)目都要遵守:

  • finalProduction, 在 Google Play 商店中發(fā)布
  • demoProduction抖部,這個版本有著生產(chǎn)服務(wù)器 url 和新功能,但是不會在 Google Play 商店中上線议惰。我們的客戶會和 Google Play 發(fā)布的版本一起安裝您朽,他們會測試這個版本并給我們反饋。
  • demoTesting换淆,demoProduction 一樣,但是使用的是測試服務(wù)器 url几颜。
  • mock倍试,對于開發(fā)者和設(shè)計(jì)者來說很有用。有時候我們的設(shè)計(jì)準(zhǔn)備好了蛋哭,但是 API 還沒準(zhǔn)備好县习。等待 API 準(zhǔn)備好才進(jìn)行開發(fā)不是一個很好的選擇。這個版本會使用假數(shù)據(jù)谆趾,這樣設(shè)計(jì)團(tuán)隊(duì)就可以測試它躁愿,并給予我們反饋。一旦 API 準(zhǔn)備好了沪蓬,我們就會切換到 demoTesting 環(huán)境彤钟。

在這個應(yīng)用中,我們將會用上述所有的環(huán)境跷叉。它們有不同的名字和 applicationId逸雹。在 gradle 3.0.0 中有一個新的 api 叫 flavorDimension,允許你混合不同的開發(fā)環(huán)境云挟,這樣你可以混合 demominApi23梆砸。在我們的 app 中,我們將使用默認(rèn)的 flavorDimension园欣。打開 build.gradle帖世,然后在 android{} 中插入下面的代碼:

flavorDimensions "default"

productFlavors {

    finalProduction {
        dimension "default"
        applicationId "me.fleka.modernandroidapp"
        resValue "string", "app_name", "Modern App"
    }

    demoProduction {
        dimension "default"
        applicationId "me.fleka.modernandroidapp.demoproduction"
        resValue "string", "app_name", "Modern App Demo P"
    }

    demoTesting {
        dimension "default"
        applicationId "me.fleka.modernandroidapp.demotesting"
        resValue "string", "app_name", "Modern App Demo T"
    }

    mock {
        dimension "default"
        applicationId "me.fleka.modernandroidapp.mock"
        resValue "string", "app_name", "Modern App Mock"
    }
}

打開 string.xml,刪除 app_name 字符串沸枯,這樣就沒有沖突了日矫。然后點(diǎn)擊 Sync。如果你打開Build Variants 界面绑榴,你會看到四種不同的變體搬男,每個都有兩種構(gòu)建類型:DebugRelease。切換到demoProduction彭沼,然后運(yùn)行缔逛,接著切換到另一個,然后運(yùn)行。你應(yīng)該會看到兩個不同名字的應(yīng)用褐奴。

3. ConstraintLayout

如果你打開 activity_main.xml按脚,你應(yīng)該會看到 ConstrainLayout 布局。如果你寫過 iOS 應(yīng)用敦冬,你應(yīng)該知道 AutoLayout辅搬。ConstrainsLayout 和它非常相似。它們甚至使用了相同的 Cassowary 算法脖旱。

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="me.fleka.modernandroidapp.MainActivity">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout> 

約束幫助我們描述視圖之間的關(guān)系堪遂。每個視圖都有 4 個約束,每邊一個萌庆。上面的代碼中溶褪,我們的視圖每一邊都被約束到父視圖。

如果你在 Design 選項(xiàng)卡中把 Hello World 文本視圖往上挪動一點(diǎn)點(diǎn)践险,在 Text 選項(xiàng)卡中會出現(xiàn)一行新代碼:

app:layout_constraintVertical_bias="0.28"

Design 和 Text 選項(xiàng)卡是同步的猿妈。我們在 Design 上的移動影響了 Text 選項(xiàng)卡中的 xml,反之亦然巍虫。垂直偏差描述了視圖在它的約束中的垂直的趨勢彭则。如果你想要視圖垂直居中,你應(yīng)該使用:

app:layout_constraintVertical_bias="0.28"

讓我們的 Activity 僅僅顯示一個倉庫占遥。它將會有一個倉庫名俯抖,關(guān)注數(shù),擁有者以及會顯示倉庫有沒有問題瓦胎。

要獲得這樣的布局蚌成,xml 是這樣的:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="me.fleka.modernandroidapp.MainActivity">

    <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: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"
        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:layout_marginTop="8dp"
        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"
        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>

不要因?yàn)?tools:text 而困惑,它僅僅是讓我們的布局預(yù)覽更好看凛捏。

我們可以注意到我們的布局是扁平的担忧。沒有嵌套的布局。你應(yīng)該盡可能地避免使用嵌套的布局坯癣,因?yàn)樗鼤绊懶阅芷渴ⅰ?梢栽?a target="_blank" rel="nofollow">這里找到更多信息示罗。同樣的惩猫,ConstraintLayout 在不同的屏幕尺寸上也能很好的工作。

這樣一來蚜点,可以相當(dāng)快地得到我想要的界面轧房。這就是 ConstraintLayout 的一些小介紹。你可以在 Google 代碼實(shí)驗(yàn)室中找到绍绘,在 github 中也有關(guān)于ConstraintLayout 的文檔奶镶。

4. 數(shù)據(jù)綁定庫

當(dāng)我聽說數(shù)據(jù)綁定庫時迟赃,我問我自己的第一件事就是,我為什么要用 Butterknife 厂镇?而在我學(xué)習(xí)了更多數(shù)據(jù)綁定的知識后纤壁,我發(fā)現(xiàn)它真的非常好用。

  • ButterKnife 可以幫到我們什么捺信?

ButterKnife 幫助我們擺脫枯燥的 findViewById酌媒。如果你有 5 個視圖,沒有 ButterKnife迄靠,你會有 5 + 5 行代碼來綁定你的視圖秒咨。用了 ButterKnife,你只需要用 5 行代碼掌挚。

  • ButterKnife 的缺點(diǎn)是什么雨席?

ButterKnife 依舊沒有解決維護(hù)代碼的問題。當(dāng)我使用 ButterKnife 時疫诽,經(jīng)常得到一個運(yùn)行時異常,因?yàn)槲以?xml 中刪除了一個視圖旦委,且在 activity/fragment 中忘了刪除綁定代碼奇徒。同樣地,當(dāng)你在 xml 中添加了一個視圖缨硝,你必須重新綁定一次摩钙。這相當(dāng)麻煩。你在維護(hù)綁定時浪費(fèi)了時間查辩。

  • 什么是數(shù)據(jù)綁定庫胖笛?

使用數(shù)據(jù)綁定庫,你只需要用一行代碼就可以綁定你的視圖宜岛!接下來展示一下它是如何工作的长踊。首先添加依賴:

// at the top of file 
apply plugin: 'kotlin-kapt'

android {
    //other things that we already used
    dataBinding.enabled = true
}
dependencies {
    //other dependencies that we used
    kapt "com.android.databinding:compiler:3.0.0-beta1"
}

注意:上面的數(shù)據(jù)綁定庫的編譯器和你的項(xiàng)目的 build.gradle 中的 gradle 版本需一致:

classpath 'com.android.tools.build:gradle:3.0.0-beta1'

現(xiàn)在點(diǎn)擊 Sync 按鈕。打開 activity_main.xml 然后用 layout 標(biāo)簽包裹住 ConstraintLayout:

<?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">

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

        <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: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"
            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:layout_marginTop="8dp"
            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"
            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>

</layout>

把所有的 xmlns 移動到 layout 標(biāo)簽萍倡。然后點(diǎn)擊 Build 按鈕身弊,或者使用快捷鍵 Cmd + F9. 我們需要構(gòu)建項(xiàng)目,這樣數(shù)據(jù)綁定庫能夠生成 ActivityMainBinding 類列敲,我們將在 MainActivity 中使用它阱佛。

如果你不構(gòu)建項(xiàng)目,那么你看不到 ActivityMainBinding 類戴而,因?yàn)樗窃诰幾g時生成的凑术。我們還沒有完成綁定,我們只是定義了一個非空的 ActivityMainBinding 類型的變量所意。你會注意到我沒有把 ? 放在ActivityMainBinding 的后面淮逊,而且也沒有初始化它催首。這怎么可能?

lateinit 關(guān)鍵字允許我們使用非空的等待被初始化的變量壮莹。和 ButterKnife 類似翅帜,初始化綁定需要在 onCreate 方法中進(jìn)行,在我們的布局準(zhǔn)備完成后命满。此外涝滴,你不應(yīng)該在 onCreate 方法中聲明綁定,因?yàn)槟愫苡锌赡茉?onCreate 方法外使用它胶台。我們的 binding 不能為空歼疮,所以這就是我們使用 lateinit 的原因。使用 lateinit 修飾诈唬,我們不需要在每次訪問它的時候檢查 binding 變量是否為空韩脏。

讓我們來初始化我們的 binding 變量,你應(yīng)該把這句:

setContentView(R.layout.activity_main)

替換成:

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

就這樣铸磅!你成功地綁定了自己的視圖∩氖福現(xiàn)在你可以訪問它并做一些改動。例如阅仔,讓我們把倉庫的名字改為“Modern Android Medium Article”:

binding.repositoryName.text = "Modern Android Medium Article"

你可以看到我們可以通過 binding 變量來訪問 activity_main.xml 中的所有視圖(當(dāng)然是有 id 的那些)吹散。這就是為什么數(shù)據(jù)綁定比 ButterKnife 更好的原因。

5. Kotlin 中的 Getters 和 setters

可能你已經(jīng)注意到了八酒,Kotlin 沒有像 Java 中的 .setText() 方法空民。我會在這里解釋一下與 Java 相比,Kotlin 中的 getters 和 setters 是如何工作的羞迷。

首先界轩,你應(yīng)該知道為什么我們要用 setters 和 getters。我們用它來隱藏類中的變量衔瓮,僅允許使用方法來訪問這些變量浊猾,這樣我們就可以向用戶隱藏類中的細(xì)節(jié),并禁止用戶直接修改我們的類热鞍。假設(shè)我們用 Java 寫了一個 Square 類:

public class Square {
  private int a;

  Square(){
    a = 1;
  }

  public void setA(int a){
    this.a = Math.abs(a);
  }

  public int getA(){
    return this.a;
  }

}

使用 setA() 方法与殃,我們禁止用戶把 a 設(shè)置為負(fù)數(shù),因?yàn)檎叫蔚倪叢粸樨?fù)數(shù)碍现。我們把 a 設(shè)置為 private幅疼,這樣它就不能直接被設(shè)置。同樣意味著我們這個類的用戶不能直接地拿到 a昼接,所以我們提供了 getter爽篷。getter 返回 a。如果你有 10 個變量慢睡,類似地逐工,你要提供 10 個 getters铡溪。寫這些不經(jīng)思考的代碼很無聊。

Kotlin 讓我們開發(fā)者的生活更加簡單泪喊,如果你調(diào)用:

var side: Int = square.a

這并不意味著你直接地訪問 a棕硫,而是類似這樣的:

int side = square.getA();

Kotlin 自動生成默認(rèn)的 getter 和 setter,除非你需要特殊的 setter 和 getter袒啼,你需要定義它們 :

var a = 1
   set(value) { field = Math.abs(value) }

field ? 這又是什么哈扮?為了看起來更清楚,我們來看看下面的代碼:

var a = 1
   set(value) { a = Math.abs(value) }

這意味著你你在 set 方法中調(diào)用了 set 方法蚓再,因?yàn)樵?Kotlin 中滑肉,你不能直接訪問屬性。這會造成無窮遞歸摘仅,當(dāng)你調(diào)用 a = something 時靶庙,它自動調(diào)用了 set 方法。現(xiàn)在你應(yīng)該知道為什么要使用 field 關(guān)鍵字了娃属。

回到我們的代碼六荒,我將向你展示 Kotlin 語言中更棒的功能:apply:

class MainActivity : AppCompatActivity() {

    lateinit var binding: ActivityMainBinding

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

        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        binding.apply {
            repositoryName.text = "Medium Android Repository Article"
            repositoryOwner.text = "Fleka"
            numberOfStarts.text = "1000 stars"

        }
    }
}

apply 允許你調(diào)用一個實(shí)例的多個方法。我們還沒有完成數(shù)據(jù)綁定矾端,還有更棒的事情掏击。讓我們先為倉庫(這是 GitHub 倉庫的 UI 模型類,存放了我們要展示的數(shù)據(jù)须床,別和倉庫模式搞混了)定義一個 ui 模型類铐料。點(diǎn)擊 New -> Kotlin File/Class 來 創(chuàng)建 Kotlin 類:

class Repository(var repositoryName: String?,var repositoryOwner: String?,var numberOfStars: Int? ,var hasIssues: Boolean = false)

在 Kotlin 中渐裂,首要構(gòu)造函數(shù)是類的頭部的一部分豺旬。如果你不提供第二個構(gòu)造函數(shù),這樣就行了柒凉,你創(chuàng)建類的工作完成了族阅。沒有構(gòu)造函數(shù)參數(shù)賦值,也沒有 getter 和 setter膝捞,全部的類只用一行代碼坦刀!

回到 MainActivity.kt,創(chuàng)建一個 Repository 類的實(shí)例:

var repository = Repository("Medium Android Repository Article",
        "Fleka", 1000, true)

可以看到蔬咬,在對象創(chuàng)建中沒有 new 關(guān)鍵字±鹨#現(xiàn)在打開 activity_main.xml,然后添加一個 data 標(biāo)簽:

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

我們可以在 layout 中訪問我們的 Repository 類型的 repository 變量林艘。例如盖奈,我們可以在 TextView 中使用repositoryName:

android:text="@{repository.repositoryName}"

這個 TextView 將會展示從 repository 變量中得到的 repositoryName 屬性。最后剩下的就是綁定 xml 中的repository 和 MainActivity.kt 中的repository 變量狐援。點(diǎn)擊 Build 按鈕钢坦,讓數(shù)據(jù)綁定庫生成所需的類究孕,然后回到 MainActivity 添加下面的代碼:

binding.repository = repository
binding.executePendingBindings()

如果你運(yùn)行 app,你會看到 TextView 展示 “Medium Android Repository Article”爹凹。很棒的功能厨诸,對吧?:)

但如果我們這樣做:

Handler().postDelayed({repository.repositoryName="New Name"}, 2000)

新的文本會在 2000 毫秒后顯示出來嗎禾酱?并不會微酬。你需要重新設(shè)置 repository。像這樣:

Handler().postDelayed({repository.repositoryName="New Name"
    binding.repository = repository
    binding.executePendingBindings()}, 2000)

如果我們每次都這樣做就非常無趣了宇植,有一個更好的解決方案叫屬性觀察者得封。讓我們先來描述一下什么是觀察者模式,因?yàn)槲覀冊?RxJava 章節(jié)中需要它指郁。

可能你已經(jīng)聽說過 androidweekly 忙上。它是個關(guān)于 Android 開發(fā)的每周時事資訊。當(dāng)你想收到資訊闲坎,你需要在給定的郵箱地址中訂閱它疫粥。一段時間后,你可能決定取消訂閱腰懂。

這就是一個觀察者/可觀察的模式的例子梗逮。這個例子中,Android Weekly 是可觀察的绣溜,它每周放出資訊慷彤,讀者是觀察者,因?yàn)樗麄冊谏厦嬗嗛喠瞬烙鳎却沦Y訊發(fā)送底哗,一旦他們收到了,他們就可以閱讀锚沸。如果某些人不喜歡跋选,他/她就可以停止監(jiān)聽。

我們所用的屬性觀察者就是 xml 布局哗蜈,他們會監(jiān)聽 Repository 實(shí)例的變化前标。所以,Repository 是可觀察的距潘。例如炼列,一旦 Repository 實(shí)例的倉庫名字這個屬性變化了,xml 就能夠更新而不必調(diào)用:

binding.repository = repository
binding.executePendingBindings()

怎樣才能做到音比?數(shù)據(jù)綁定庫給我們提供了 BaseObservable 類俭尖,Repository 類應(yīng)該實(shí)現(xiàn)這個類:

class Repository(repositoryName : String, var repositoryOwner: String?, var numberOfStars: Int?
                 , var hasIssues: Boolean = false) : BaseObservable(){

    @get:Bindable
    var repositoryName : String = ""
    set(value) {
        field = value
        notifyPropertyChanged(BR.repositoryName)
    }

}

一旦使用了 Bindable 注解,就會自動生成 BR 類硅确。你會看到目溉,一旦新的值設(shè)置后明肮,我們就通知它。現(xiàn)在運(yùn)行 app 你將看到倉庫的名字在 2 秒后改變而不必再次調(diào)用executePendingBindings()缭付。

英文原文:Modern Android development with Kotlin (September 2017) Part 1
舊文推薦:
Kotlin 讓使用 Android API 變得輕松
“Effective Java” 可能對 Kotlin 的設(shè)計(jì)造成了怎樣的影響——第一部分

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末柿估,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子陷猫,更是在濱河造成了極大的恐慌秫舌,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,590評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件绣檬,死亡現(xiàn)場離奇詭異足陨,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)娇未,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,157評論 3 399
  • 文/潘曉璐 我一進(jìn)店門墨缘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人零抬,你說我怎么就攤上這事镊讼。” “怎么了平夜?”我有些...
    開封第一講書人閱讀 169,301評論 0 362
  • 文/不壞的土叔 我叫張陵蝶棋,是天一觀的道長。 經(jīng)常有香客問我忽妒,道長玩裙,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,078評論 1 300
  • 正文 為了忘掉前任段直,我火速辦了婚禮吃溅,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘坷牛。我一直安慰自己罕偎,他們只是感情好很澄,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,082評論 6 398
  • 文/花漫 我一把揭開白布京闰。 她就那樣靜靜地躺著,像睡著了一般甩苛。 火紅的嫁衣襯著肌膚如雪蹂楣。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,682評論 1 312
  • 那天讯蒲,我揣著相機(jī)與錄音痊土,去河邊找鬼。 笑死墨林,一個胖子當(dāng)著我的面吹牛赁酝,可吹牛的內(nèi)容都是我干的犯祠。 我是一名探鬼主播,決...
    沈念sama閱讀 41,155評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼酌呆,長吁一口氣:“原來是場噩夢啊……” “哼衡载!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起隙袁,我...
    開封第一講書人閱讀 40,098評論 0 277
  • 序言:老撾萬榮一對情侶失蹤痰娱,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后菩收,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體梨睁,經(jīng)...
    沈念sama閱讀 46,638評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,701評論 3 342
  • 正文 我和宋清朗相戀三年娜饵,在試婚紗的時候發(fā)現(xiàn)自己被綠了坡贺。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,852評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡箱舞,死狀恐怖拴念,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情褐缠,我是刑警寧澤政鼠,帶...
    沈念sama閱讀 36,520評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站队魏,受9級特大地震影響公般,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜胡桨,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,181評論 3 335
  • 文/蒙蒙 一官帘、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧昧谊,春花似錦刽虹、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,674評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至尚镰,卻和暖如春阀圾,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背狗唉。 一陣腳步聲響...
    開封第一講書人閱讀 33,788評論 1 274
  • 我被黑心中介騙來泰國打工初烘, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 49,279評論 3 379
  • 正文 我出身青樓肾筐,卻偏偏與公主長得像哆料,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子吗铐,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,851評論 2 361

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