使用Kotlin進行Android開發-第一部分

weixin_34120274發表於2017-09-28

要想找到一個專案覆蓋android開發中的新技術真的是太難了,所以我決定自己寫一個,在這篇文章中就會涵蓋以下內容:

  • Android Studio 3,beta 1
  • Kotlin language
  • Build Variants
  • ConstraintLayout
  • Data binding library
  • MVVM 架構
  • RxJava2
  • Dagger 2.11
  • Retrofit(使用RxJava2)
  • Room(使用RxJava2)

Android Studio

安裝Android Studio3.0版本,如果你想在mac上保留老的版本,只需要在Applications中將老的版本的資料夾重新命名即可。

Android Studio支援Kotlin,在建立Android專案的時候,你會看到一個核取方塊為是否支援Kotlin,預設是被選中的,點選兩次下一步,然後選擇Empty Activity,然後完成,恭喜你,你已經成功使用Kotlin建立了一個專案。

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。

savedInstanceState:Bundle?意思是savedInstanceState可以是Bundle型別或者是null。

我們平時都比較厭倦空指標異常,但是kotlin就不會存在這個問題,比如說我們不對下面的textview進行非空判斷的話,就有可能丟擲空指標異常:

nameTextView.setEnabled(true)

但是對於kotlin來說,他會強制我們使用?和!!操作符,如果我們使用?操作符的話:

nameTextView?.setEnabled(true)

這行程式碼只有在nameTextView不為null的時候才會執行,另一種情況下,我們使用!!操作符:

nameTextView!!.setEnabled(true)

如果nameTextview為null,就會丟擲空指標異常。

2. Build Variants

在日常開發中,我們會配置很多的環境,最常見的就是測試和生產環境,這些環境可能在伺服器URL,圖示,名字,目標api方面會不同,在開始的每個專案中,我們都會有以下幾點:

  • finalProduction 要發到應用市場的版本
  • demoProduction 具有生產環境的url的版本,但是還需要釋出到其他的地方讓一部分使用者來進行測試,從而給我們反饋bug
  • demoTesting 和demoProduction一樣,但是配置的事測試伺服器的url
  • mock 這個用來幫助我們在只有設計圖,沒有api介面的情況下,為了不耽誤時間,可以自己製造資料用來編寫功能,當api提供的時候,可以迅速切換到demoTesting環境來進行測試

在這個專案中,我們將包含以上所有的環境配置,他們只會有名字和applicationId不同,3.0.0提供了一個新的api,flavorDimension,允許你混合不同的flavors,所以你可以合併demo和minApi23的flavors,在我們的應用程式中,我們將只使用“default”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字串,不然的話會有衝突,然後重新build專案,如果你去看Android Studio左下角的Build Variants,你將會看到四個不同的build variants,每一個還有兩個型別,debug和release,切換到demoProduction,然後執行它,你將會看到兩個不同名字的應用。


Build Variants

3. ConstraintLayout

當你開啟activity_main.xml檔案的時候,你看到的就是ConstraintLayout。

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

ConstraintLayout幫助我們描述view之間的關係,對於每個檢視,您應該有4個約束,每個檢視一個。在這種情況下,我們的觀點僅限於雙方的父view。
如果你想移動Hello World向上一點,那麼就需要在文字皮膚加入下面這行程式碼:

app:layout_constraintVertical_bias="0.28"
constraint Layout

設計皮膚和文字皮膚是同步的,我們在“設計”選項卡中的移動會影響“文字”選項卡和副本中的xml。垂直偏差描述了檢視對他的約束的垂直傾向。如果要使檢視垂直居中,應該使用:

app:layout_constraintVertical_bias="0.28"

讓我們來設計我們的列表中的一個條目,程式碼如下所示:

<?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可以適配不同的螢幕大小。

佈局

這是ConstraintLayout的一個小介紹。您可以在這裡找到Google程式碼實驗室,並且有關於github上的CL的文件.

4. Data Binding

當我聽說資料繫結庫時,首先我自問的是,ButterKnife對我來說真的很好。此外,我正在使用一個外掛來幫助我從xml獲取檢視。為什麼要改變呢?“一旦我學到了更多關於資料繫結的資訊,我就像我當初第一次使用ButterKnife時一樣。

ButterKnife能幫助我們什麼?

ButterKnife能把我們從findViewById中解救出來,如果沒有ButterKnife的話,你有5個view的話,你繫結view需要些十行程式碼,但是有了ButterKnife,你就只需要寫5行程式碼就行。

ButterKnife的缺點

ButterKnife仍然不能解決程式碼維護的問題。當我使用ButterKnife時,我經常發現自己得到一個執行時異常,因為我刪除xml中的檢視,我沒有刪除活動/片段類中的繫結程式碼。另外,如果要在xml中新增檢視,則必須再次執行繫結。真的很無聊你正在耗費維護繫結的時間。

Data Binding是什麼呢?

他有很多的優點,使用Data Binding的話,繫結View只使用一行程式碼就搞定了,讓我來向你展示一下他是如何工作的,讓我們開始新增資料繫結的庫到專案中。

// 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'

點選立刻同步,然後去到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>

注意需要把名稱空間都移動到layout標籤內,然後點選build按鈕開始build專案,我們之所以需要build專案是因為Data Binding Library會生成一個ActivityMainBinding的檔案,然後會在我們的MainActivity中使用到。
讓我們開始初始化我們的繫結變數,替換下面的程式碼:

setContentView(R.layout.activity_main)

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

你已經成功繫結了你的view,現在你可以訪問他並做一些更改,

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

你可以看到我們可以通過繫結變數來訪問activity_main.xml中所有的view(當然有id)。這就是為什麼資料繫結比ButterKnife更好。

也許你已經注意到了我們沒有像在java中的.setText方法,這裡需要停下來解釋一下java和kotlin中的getters和setters的對比。

首先,你需要知道我們為什麼要使用getters和setters,我們使用它是想隱藏類中的變數,以防客戶端直接改變我們的類,下面有一個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()方法,我們禁止類的客戶端設定負值,因為方形的邊不能為負。使用這種方法,我們必須私有,所以不能直接設定。這也意味著客戶端不能直接得到,所以我們必須提供一個getter。那個getter返回a。如果您有10個具有相似要求的變數,則必須提供10個getters。寫這樣的程式碼是無聊的事情,我們通常不會用我們的思想。

kotlin使我們的開發生活變得更加的簡單:

var side:Int = square.a

他並不意味著你能直接訪問a,而是如下所示:

int side = square.getA()

因為kotlin會自動幫我們生成getters和setters,在kotlin中,只有你需要定義getter和setter的時候,你才會定義它,否則,kotlin會自動為你生成如下程式碼:

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

這意味著您在set方法中呼叫set方法,因為在Kotlin世界中沒有對該屬性的直接訪問。這將使無限遞迴。當你呼叫a =某些它會自動呼叫set方法。 我希望現在很清楚為什麼你必須使用欄位關鍵字以及setter和getter如何工作。
讓我們現在回到我們的程式碼,讓我在向你介紹一個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允許你在一個例項上呼叫多個方法。
我們還沒有結束Data Binding部分,還有很多事情需要做讓我們為Repository編寫ui模型類(這是Github Repository的UI Model類,儲存應該顯示的資料)。要建立Kotlin類,你應該去New -> Kotlin File/Class:

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>

我們可以在我們的佈局中訪問Repository型別的Repository變數。例如,我們可以在id為repository_name的TextView中執行以下操作.

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

rrepository_name將顯示從repository變數的repositoryName屬性獲得的文字。剩下的唯一的事情是將Repository變數從xml繫結到從MainActivity.kt到Repository。 按構建以使資料繫結庫生成所需的類並返回到主要活動並新增這兩行:

binding.repository = repository
binding.executePendingBindings()

如果你執行,你就會看到Textview顯示了剛才設定的值,如果我們執行如下程式碼:

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

這個程式碼會在延遲2秒鐘之後顯示值嗎,答案是不會的,如果你想實現這樣的功能,需要做如下操作:

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

但是如果你每次修改屬性都要進行如上的操作的話,會是厭煩的,有一個更好的解決方法就是使用屬性觀察者。
屬性Observer在我們的例子中是xml佈局,它將監聽Repository例項中的更改。所以,Repository是可觀察的。例如,一旦在Repository類的例項中更改儲存庫名稱屬性時,應該更新xml而不進行呼叫:

binding.repository = repository
binding.executePendingBindings()

Data Binding Library為我們提供了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)
    }

}

BR是使用Bindable註釋時自動生成的類。如你所見,一旦設定了新的值,我們就會通知它。現在可以執行應用程式,您將看到Repository名稱將在2秒後更改,而不會再次呼叫executePendingBindings().

這就是第一部分的所有內容,第二部分我們將介紹MVVM。

原文地址

https://proandroiddev.com/modern-android-development-with-kotlin-september-2017-part-1-f976483f7bd6

相關文章