簡評:目前,在 Android 開發中找到一個覆蓋全部的新技術的項目難如登天,因此做者決定本身寫一個。本文因此使用的技術包括:html
Android Studio 3, beta1android
Kotlin 語言git
構建變體github
ConstraintLayout算法
數據綁定庫數據庫
MVVM 架構 + 存儲庫模式(使用映射器)+ Android Manager Wrappers(Part 2)api
RxJava2 及它如何在架構中起做用安全
Dagger 2.11,什麼是依賴注入,爲何須要它服務器
改造(使用 Rx Java2)架構
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,而後運行,接着切換到另外一個,而後運行。你應該會看到兩個不一樣名字的應用。
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"
Design 和 Text 選項卡是同步的。咱們在 Design 上的移動影響了 Text 選項卡中的 xml,反之亦然。垂直誤差描述了視圖在它的約束中的垂直的趨勢。若是你想要視圖垂直居中,你應該使用:
app:layout_constraintVertical_bias="0.28"
讓咱們的 Activity 僅僅顯示一個倉庫。它將會有一個倉庫名,關注數,擁有者以及會顯示倉庫有沒有問題。
要得到這樣的佈局,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 在不一樣的屏幕尺寸上也能很好的工做。
這樣一來,能夠至關快地獲得我想要的界面。這就是 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