用 Kotlin 開發現代 Android 項目 Part 1

clipboard.png

簡評:目前,在 Android 開發中找到一個覆蓋全部的新技術的項目難如登天,因此做者決定本身寫一個。本文因此使用的技術包括:html

  1. Android Studio 3, beta1android

  2. Kotlin 語言git

  3. 構建變體github

  4. ConstraintLayout算法

  5. 數據綁定庫數據庫

  6. MVVM 架構 + 存儲庫模式(使用映射器)+ Android Manager Wrappers(Part 2)api

  7. RxJava2 及它如何在架構中起做用安全

  8. Dagger 2.11,什麼是依賴注入,爲何須要它服務器

  9. 改造(使用 Rx Java2)架構

  10. Room(使用 Rx Java2)

咱們的 app 看起來是什麼樣的?

咱們的 app 將會是最簡單的,將使用全部上面提到的技術,只用一個功能:拉取 GitHub 上的全部 google 案例倉庫,把這些數據保存到本地數據庫並展現給用戶。

我將盡量地解釋每一行代碼。你能夠從 github 上跟進我提交的代碼。

讓咱們一塊兒動手:

0. Android Studio

要安裝 Android Studio 3 beta1(如今已發佈正式版),你要進入這個頁面

注意:若是你想要和以前安裝的某個版本共存,在 Mac 上你應該在應用文件夾中重命名舊的版本,如「Android Studio Old」。你能夠在這裏找到更多信息,包括 Windows 和 Linux。

Android Studio 現已支持 Kotlin。去建立 Android 項目,你會發現新東西:支持 Kotlin 的標籤可選框。它是默認選中的。按兩下 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 關鍵字,在 Kotlin 中你能夠沒必要使用,取決於你的喜愛。你必須使用
override 關鍵字,而不是註解。

那麼,savedInstanceState: Bundle? 中的 ? 表示什麼意思呢?意味着 savedInstanceState 參數多是 Bundle 類型或者爲 null。Kotlin 是空安全的語言。若是你定義了:

var a : String

你將獲得一個編譯錯誤,由於 a 必須被初始化,它不能爲 null 。意味着你必須這樣寫:

var a : String = "Init value"

若是你像下面這樣寫,一樣你將獲得一個編譯錯誤:

a = null

要讓 a 成爲可空的,你必須這樣:

var a : String?

爲何這是 Kotlin 語言的一個重要功能呢?由於它幫咱們避免了空指針異常。Android 開發者受夠了空指針異常。即使是 null 的創造者,Tony Hoare 先生,也爲發明出 null 而道歉了。假設咱們有一個可空的 nameTextView。如下代碼將會形成 NPE,若是它是 null 的話:

nameTextView.setEnabled(true)

而 Kotlin 將不容許咱們作相似這樣的事。它強制咱們使用 ? 或者 !! 操做符,若是咱們使用 ? 操做符:

nameTextView?.setEnabled(true)

這行代碼僅當 nameTextView 不爲 null 纔會執行。換句話說,若是咱們使用了 !! 操做符:

nameTextView!!.setEnabled(true)

若是 nameTextView 爲 null,它將報 NPE。想冒險的人才會用 :)

這只是有關 Kotlin 的一點小小的介紹,隨着咱們深刻,後面再也不介紹其餘 Kotlin 特性代碼。

2. 構建變體

在開發中,你一般會有不一樣的環境。最多見的就是測試和生產環境。這些環境在服務器 url,圖標,名字,目標 api 上等等有所不一樣。在 fleka,咱們的每個項目都要遵照:

  • finalProduction, 在 Google Play 商店中發佈

  • demoProduction,這個版本有着生產服務器 url 和新功能,可是不會在 Google Play 商店中上線。咱們的客戶會和 Google Play 發佈的版本一塊兒安裝,他們會測試這個版本並給咱們反饋。

  • demoTesting,和 demoProduction 同樣,可是使用的是測試服務器 url。

  • mock,對於開發者和設計者來講頗有用。有時候咱們的設計準備好了,可是 API 還沒準備好。等待 API 準備好才進行開發不是一個很好的選擇。這個版本會使用假數據,這樣設計團隊就能夠測試它,並給予咱們反饋。一旦 API 準備好了,咱們就會切換到 demoTesting 環境。

在這個應用中,咱們將會用上述全部的環境。它們有不一樣的名字和 applicationId。在 gradle 3.0.0 中有一個新的 api 叫 flavorDimension,容許你混合不一樣的開發環境,這樣你能夠混合 demo 和 minApi23。在咱們的 app 中,咱們將使用默認的 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 字符串,這樣就沒有衝突了。而後點擊 Sync。若是你打開Build Variants 界面,你會看到四種不一樣的變體,每一個都有兩種構建類型:Debug 和 Release。切換到demoProduction,而後運行,接着切換到另外一個,而後運行。你應該會看到兩個不一樣名字的應用。

clipboard.png

3. ConstraintLayout

若是你打開 activity_main.xml,你應該會看到 ConstrainLayout 佈局。若是你寫過 iOS 應用,你應該知道 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>

約束幫助咱們描述視圖之間的關係。每一個視圖都有 4 個約束,每邊一個。上面的代碼中,咱們的視圖每一邊都被約束到父視圖。

若是你在 Design 選項卡中把 Hello World 文本視圖往上挪動一點點,在 Text 選項卡中會出現一行新代碼:

app:layout_constraintVertical_bias="0.28"

clipboard.png

Design 和 Text 選項卡是同步的。咱們在 Design 上的移動影響了 Text 選項卡中的 xml,反之亦然。垂直誤差描述了視圖在它的約束中的垂直的趨勢。若是你想要視圖垂直居中,你應該使用:

app:layout_constraintVertical_bias="0.28"

讓咱們的 Activity 僅僅顯示一個倉庫。它將會有一個倉庫名,關注數,擁有者以及會顯示倉庫有沒有問題。

clipboard.png

要得到這樣的佈局,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>

不要由於 tools:text 而困惑,它僅僅是讓咱們的佈局預覽更好看。

咱們能夠注意到咱們的佈局是扁平的。沒有嵌套的佈局。你應該儘量地避免使用嵌套的佈局,由於它會影響性能。能夠在這裏找到更多信息。一樣的,ConstraintLayout 在不一樣的屏幕尺寸上也能很好的工做。

clipboard.png

這樣一來,能夠至關快地獲得我想要的界面。這就是 ConstraintLayout 的一些小介紹。你能夠在 Google 代碼實驗室中找到,在 github 中也有關於ConstraintLayout 的文檔

4. 數據綁定庫

當我據說數據綁定庫時,我問我本身的第一件事就是,我爲何要用 Butterknife ?而在我學習了更多數據綁定的知識後,我發現它真的很是好用。

  • ButterKnife 能夠幫到咱們什麼?

ButterKnife 幫助咱們擺脫枯燥的 findViewById。若是你有 5 個視圖,沒有 ButterKnife,你會有 5 + 5 行代碼來綁定你的視圖。用了 ButterKnife,你只須要用 5 行代碼。

  • ButterKnife 的缺點是什麼?

ButterKnife 依舊沒有解決維護代碼的問題。當我使用 ButterKnife 時,常常獲得一個運行時異常,由於我在 xml 中刪除了一個視圖,且在 activity/fragment 中忘了刪除綁定代碼。一樣地,當你在 xml 中添加了一個視圖,你必須從新綁定一次。這至關麻煩。你在維護綁定時浪費了時間。

  • 什麼是數據綁定庫?

使用數據綁定庫,你只須要用一行代碼就能夠綁定你的視圖!接下來展現一下它是如何工做的。首先添加依賴:

// 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"
}

注意:上面的數據綁定庫的編譯器和你的項目的 build.gradle 中的 gradle 版本需一致:

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

如今點擊 Sync 按鈕。打開 activity_main.xml 而後用 layout 標籤包裹住 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 標籤。而後點擊 Build 按鈕,或者使用快捷鍵 Cmd + F9. 咱們須要構建項目,這樣數據綁定庫可以生成 ActivityMainBinding 類,咱們將在 MainActivity 中使用它。

若是你不構建項目,那麼你看不到 ActivityMainBinding 類,由於它是在編譯時生成的。咱們尚未完成綁定,咱們只是定義了一個非空的 ActivityMainBinding 類型的變量。你會注意到我沒有把 ? 放在ActivityMainBinding 的後面,並且也沒有初始化它。這怎麼可能?

lateinit 關鍵字容許咱們使用非空的等待被初始化的變量。和 ButterKnife 相似,初始化綁定須要在 onCreate 方法中進行,在咱們的佈局準備完成後。此外,你不該該在 onCreate 方法中聲明綁定,由於你頗有可能在 onCreate 方法外使用它。咱們的 binding 不能爲空,因此這就是咱們使用 lateinit 的緣由。使用 lateinit 修飾,咱們不須要在每次訪問它的時候檢查 binding 變量是否爲空。

讓咱們來初始化咱們的 binding 變量,你應該把這句:

setContentView(R.layout.activity_main)

替換成:

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

就這樣!你成功地綁定了本身的視圖。如今你能夠訪問它並作一些改動。例如,讓咱們把倉庫的名字改成「Modern Android Medium Article」:

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

你能夠看到咱們能夠經過 binding 變量來訪問 activity_main.xml 中的全部視圖(固然是有 id 的那些)。這就是爲何數據綁定比 ButterKnife 更好的緣由。

5. Kotlin 中的 Getters 和 setters

可能你已經注意到了,Kotlin 沒有像 Java 中的 .setText() 方法。我會在這裏解釋一下與 Java 相比,Kotlin 中的 getters 和 setters 是如何工做的。

首先,你應該知道爲何咱們要用 setters 和 getters。咱們用它來隱藏類中的變量,僅容許使用方法來訪問這些變量,這樣咱們就能夠向用戶隱藏類中的細節,並禁止用戶直接修改咱們的類。假設咱們用 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 設置爲負數,由於正方形的邊不爲負數。咱們把 a 設置爲 private,這樣它就不能直接被設置。一樣意味着咱們這個類的用戶不能直接地拿到 a,因此咱們提供了 getter。getter 返回 a。若是你有 10 個變量,相似地,你要提供 10 個 getters。寫這些不經思考的代碼很無聊。

Kotlin 讓咱們開發者的生活更加簡單,若是你調用:

var side: Int = square.a

這並不意味着你直接地訪問 a,而是相似這樣的:

int side = square.getA();

Kotlin 自動生成默認的 getter 和 setter,除非你須要特殊的 setter 和 getter,你須要定義它們 :

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

field ? 這又是什麼?爲了看起來更清楚,咱們來看看下面的代碼:

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

這意味着你你在 set 方法中調用了 set 方法,由於在 Kotlin 中,你不能直接訪問屬性。這會形成無窮遞歸,當你調用 a = something 時,它自動調用了 set 方法。如今你應該知道爲何要使用 field 關鍵字了。

回到咱們的代碼,我將向你展現 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 容許你調用一個實例的多個方法。咱們尚未完成數據綁定,還有更棒的事情。讓咱們先爲倉庫(這是 GitHub 倉庫的 UI 模型類,存放了咱們要展現的數據,別和倉庫模式搞混了)定義一個 ui 模型類。點擊 New -> Kotlin File/Class 來 建立 Kotlin 類:

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

在 Kotlin 中,首要構造函數是類的頭部的一部分。若是你不提供第二個構造函數,這樣就好了,你建立類的工做完成了。沒有構造函數參數賦值,也沒有 getter 和 setter,所有的類只用一行代碼!

回到 MainActivity.kt,建立一個 Repository 類的實例:

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

能夠看到,在對象建立中沒有 new 關鍵字。如今打開 activity_main.xml,而後添加一個 data 標籤:

<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 變量。點擊 Build 按鈕,讓數據綁定庫生成所需的類,而後回到 MainActivity 添加下面的代碼:

binding.repository = repository
binding.executePendingBindings()

若是你運行 app,你會看到 TextView 展現 「Medium Android Repository Article」。很棒的功能,對吧?:)

但若是咱們這樣作:

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

新的文本會在 2000 毫秒後顯示出來嗎?並不會。你須要從新設置 repository。像這樣:

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

若是咱們每次都這樣作就很是無趣了,有一個更好的解決方案叫屬性觀察者。讓咱們先來描述一下什麼是觀察者模式,由於咱們在 RxJava 章節中須要它。

可能你已經據說過 androidweekly 。它是個關於 Android 開發的每週時事資訊。當你想收到資訊,你須要在給定的郵箱地址中訂閱它。一段時間後,你可能決定取消訂閱。

這就是一個觀察者/可觀察的模式的例子。這個例子中,Android Weekly 是可觀察的,它每週放出資訊,讀者是觀察者,由於他們在上面訂閱了,等待新資訊發送,一旦他們收到了,他們就能夠閱讀。若是某些人不喜歡,他/她就能夠中止監聽。

咱們所用的屬性觀察者就是 xml 佈局,他們會監聽 Repository 實例的變化。因此,Repository 是可觀察的。例如,一旦 Repository 實例的倉庫名字這個屬性變化了,xml 就可以更新而沒必要調用:

binding.repository = repository
binding.executePendingBindings()

怎樣才能作到?數據綁定庫給咱們提供了 BaseObservable 類,Repository 類應該實現這個類:

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 類。你會看到,一旦新的值設置後,咱們就通知它。如今運行 app 你將看到倉庫的名字在 2 秒後改變而沒必要再次調用executePendingBindings()。

英文原文:Modern Android development with Kotlin (September 2017) Part 1

相關文章
相關標籤/搜索