前言
經常在medium.com上看到一些高質量的技術帖子,但是由於國內的上網環境或者有的同學對於看英文比較排斥,錯過了不少好文章。因此,西哥決定弄一個《優質譯文專欄》,花一些時間翻譯一些優質技術文給大家。這篇文章是一個小系列,用Kotlin開發現代Android APP,總共四篇,後面的會陸續翻譯!以下是正文。
現在,真的很難找到一個涵蓋所有Android新技術的專案,因此我決定自己來寫一個,在本文中,我們將用到如下技術:
- 0 、Android Studio
- 1、Kotlin 語言
- 2、構建變體
- 3、ConstraintLayout
- 4、DataBinding庫
- 5、MVVM+repository+Android Manager架構模式
- 6、RxJava2及其對架構的幫助
- 7、Dagger 2.11,什麼是依賴注入?為什麼要使用它?
- 8、Retrofit + RxJava2 實現網路請求
- 9、RooM + RxJava2 實現儲存
我們的APP最終是什麼樣子?
我們的APP是一個非常簡單的應用程式,它涵蓋了上面提到的所有技術。只有一個簡單的功能:從Github 獲取googlesamples
使用者下的所有倉庫,將資料儲存到本地資料庫,然後在介面展示它。
我將嘗試解釋更多的程式碼,你也可以看看你Github上的程式碼提交。
Github:https://github.com/mladenrako...
讓我們開始吧。
0、Android Studio
首先安卓Android Studio 3 beta 1(注:現在最新版為Android Studio 4.0),Android Studio 已經支援Kotlin,去到Create Android Project
介面,你將在此處看到新的內容:帶有標籤的核取方塊include Kotlin support
。預設情況下選中。按兩次下一步,然後選擇EmptyActivity
,然後完成了。 恭喜!你用Kotlin開發了第一個Android app)
1、Kotlin
在剛才新建的專案中,你可以看到一個MainActivity.kt
:
package me.mladenrakonjac.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()
表示我們的MainActivity
繼承自AppCompatActivity
。
此外,所有的方法都必須有一個關鍵字fun
,在Kotlin 中,你不能使用@override
註解,如果你要表明方法是複寫父類或者介面的方法的話,直接使用override
關鍵字,注意:它和Java不一樣,不是一個註解了。
然後,savedInstanceState: Bundle?
中的?
代表什麼呢?它代表了savedInstanceState
這個引數可以是Bundle
或者null。Kotlin是一門null 安全語言,如果你像下面這樣寫:
var a : String
你將會得到一個編譯錯誤。因為a
變數必須被初始化,並且不能為null,因此你要像這樣寫:
var a : String = "Init value"
並且,如果你執行以下操作,也會報編譯錯誤:
a = null
要想使a
變數為null ,你必須這樣寫:
var a : String?
為什麼這是Kotlin語言的一個重要功能呢?因為它幫我們避免了NPE,Androd開發者已經對NPE感到厭倦了,甚至是null的發明者-Tony Hoare
先生,也為發明它而道歉。假設我們有一個可以為空的nameTextView
。如果為null,以下程式碼將會發生NPE:
nameTextView.setEnabled(true)
但實際上,Kotlin做得很好,它甚至不允許我們做這樣的事情。它會強制我們使用?
或者!!
操作符。如果我們使用?
操作符:
nameTextView?.setEnabled(true)
僅當nameTextView
不為null時,這行程式碼才會繼續執行。另一種情況下,如果我們使用!!
操作符:
nameTextView!!.setEnabled(true)
如果nameTextView
為null,它將為我們提供NPE。它只適合喜歡冒險的傢伙)
這是對Kotlin的一些介紹。我們繼續進行,我將停止描述其他Kotlin特定程式碼。
2、構建變體
通常,在開發中,如果你有兩套環境,最常見的是測試環境和生產環境。這些環境在伺服器URL
,圖示
,名稱
,目標api
等方面可能有所不同。通常,在開始的每個專案中我都有以下內容:
finalProduction
: 上傳Google Play 使用demoProduction
:該版本使用生產環境伺服器Url,並且它有著GP上的版本沒有的新功能,使用者可以在Google play 旁邊安裝,然後可以進行新功能測試和提供反饋。demoTesting
:和demoProduction一樣,只不過它用的是測試地址mock
: 對於我來說,作為開發人員和設計師而言都是很有用的。有時我們已經準備好設計,而我們的API仍未準備好。等待API準備就緒後再開始開發可不是好的解決方案。此構建變體為提供有mock資料,因此設計團隊可以對其進行測試並提供反饋。對於保證專案進度真的很有幫助,一旦API準備就緒,我們便將開發轉移到demoTesting環境。
在此應用程式中,我們將擁有所有這些變體。它們的applicationId和名稱不同。 gradle 3.0.0 flavourDimension
中有一個新的api
,可讓您混合不同的產品風味,因此您可以混合demo
和minApi23
風味。在我們的應用程式中,我們將僅使用“預設” 的flavorDimension
。早app的build.gradle
中,將此程式碼插入android {}
下:
flavorDimensions "default"
productFlavors {
finalProduction {
dimension "default"
applicationId "me.mladenrakonjac.modernandroidapp"
resValue "string", "app_name", "Modern App"
}
demoProduction {
dimension "default"
applicationId "me.mladenrakonjac.modernandroidapp.demoproduction"
resValue "string", "app_name", "Modern App Demo P"
}
demoTesting {
dimension "default"
applicationId "me.mladenrakonjac.modernandroidapp.demotesting"
resValue "string", "app_name", "Modern App Demo T"
}
mock {
dimension "default"
applicationId "me.mladenrakonjac.modernandroidapp.mock"
resValue "string", "app_name", "Modern App Mock"
}
}
開啟string.xml
檔案,刪掉app_name
string資源,因此,我們才不會發生資源衝突,然後點選Sync Now
,如果轉到螢幕左側的“構建變體”
,則可以看到4個不同的構建變體,其中每個都有兩種構建型別:“Debug”和“Release”,切換到demoProduction
構建變體並執行它。然後切換到另一個並執行它。您就可以看到兩個名稱不同的應用程式。
3、ConstraintLayout
如果你開啟activity_main.xml
,你可以看到跟佈局是ConstraintLayout
,如果你開發過iOS應用程式,你可能知道AutoLayout
,ConstraintLayout
和它非常的相似,他們甚至用了相同的 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.mladenrakonjac.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" />
Constraints可以幫我們描述View之間的關係。對於每一個View來說,應該有4個約束,每一邊一個約束,在這種情況下,我們的View就被約束在了父檢視的每一邊了。
在Design Tab中,如果你將Hello World
文字稍微向上移動,則在Text
Tab中將增加下面這行程式碼:
app:layout_constraintVertical_bias="0.28"
Design
tab 和 Text
tab是同步的,我們在Design中移動檢視,則會影響Text中的xml
,反之亦然。垂直偏差描述了檢視對其約束的垂直趨勢。如果要使檢視垂直居中,則應使用:
app:layout_constraintVertical_bias="0.28"
我們讓Activity
只顯示一個倉庫,它有倉庫的名字,star的數量,作者,並且還會顯示是否有issue
要得到上面的佈局設計,程式碼如下所示:
<?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.mladenrakonjac.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
的少部分介紹,你也可以看一下關於ConstraintLayout
使用的google code lab: https://codelabs.developers.g...
4. Data binding library
當我聽到Data binding 庫的時候,我的第一反應是:Butterknife已經很好了,再加上,我現在使用一個外掛來從xml中獲取View,我為啥要改變,來使用Data binding呢?但當我對Data binding有了更多的瞭解之後,我的它的感覺就像我第一次見到Butterknife一樣,無法自拔。
Butterknife能幫我們做啥?
ButterKnife幫助我們擺脫無聊的findViewById
。因此,如果您有5個檢視,而沒有Butterknife,則你有5 + 5行程式碼來繫結您的檢視。使用ButterKnife,您只有我行程式碼就搞定。就是這樣。
Butterknife的缺點是什麼?
Butterknife仍然沒有解決程式碼可維護問題,使用ButterKnife時,我經常發現自己遇到執行時異常,這是因為我刪除了xml中的檢視,而沒有刪除Activity/Fragment類中的繫結程式碼。另外,如果要在xml中新增檢視,則必須再次進行繫結。真的很不好維護。你將浪費大量時間來維護View繫結。
那與之相比,Data Binding 怎麼樣呢?
有很多好處,使用Data Binding,你可以只用一行程式碼就搞定View的繫結,讓我們看看它是如何工作的,首先,先將Data Binding 新增到專案:
// 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 Now
,開啟activity_main.xml
,將Constraint Layout
用layout標籤包裹
<?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.mladenrakonjac.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>
注意,你需要將所有的xml移動到layout 標籤下面,然後點選Build
圖示或者使用快捷鍵Cmd + F9
,我們需要構建專案來使Data Binding庫為我們生成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)
就是這樣,你成功的繫結了所有View,現在你可以訪問它並且做一些更改,例如,我們將倉庫名字改為Modern Android Medium Article
:
binding.repositoryName.text = "Modern Android Medium Article"
如你所見,現在我們可以通過bingding
變數來訪問main_activity.xml
的所有View了(前提是它們有id),這就是Data Binding 比ButterKnife 好用的原因。
kotlin的 Getters 和 setters
大概,你已經注意到了,我們沒有像Java那樣使用.setText()
,我想在這裡暫停一下,以說明與Java相比,Kotlin中的getter和setter方法如何工作的。
首先,你需要知道,我們為什麼要使用getters和setters,我們用它來隱藏類中的變數,僅允許使用方法來訪問這些變數,這樣我們就可以向使用者隱藏類中的細節,並禁止使用者直接修改我們的類。假設我們用 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()
方法,我們禁止了使用者向Square
類的a
變數設定一個負數,因為正方形的邊長一定是正數,要使用這種方法,我們必須將其設為私有,因此不能直接設定它。這也意味著我們不能直接獲得a
,需要給它定一個get方法來返回a
,如果有10個變數,那麼我們就得定義10個相似的get方法,寫這樣無聊的樣板程式碼,通常會影響我們的心情。
Kotling使我們的開發人員更輕鬆了。如果你呼叫下面的程式碼:
var side: Int = square.a
這並不意味著你是在直接訪問a變數,它和Java中呼叫getA()
是相同的
int side = square.getA();
因為Kotlin自動生成預設的getter和setter。在Kotlin中,只有當您有特殊的setter或getter時,才應指定它。否則,Kotlin會為您自動生成:
var a = 1
set(value) { field = Math.abs(value) }
field
? 這又是個什麼東西?為了更清楚明白,請看下面程式碼:
var a = 1
set(value) { a = Math.abs(value) }
這表明你在呼叫set方法中的set(value){}
,因為Kotlin的世界中,沒有直接訪問屬性,這就會造成無限遞迴,當你呼叫a = something
,會自動呼叫set方法。使用filed就能避免無限遞迴,我希望這能讓你明白為什麼要用filed關鍵字,並且瞭解getters和setters是如何工作的。
回到程式碼中繼續,我將向你介紹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 = "Mladen Rakonjac"
numberOfStarts.text = "1000 stars"
}
}
}
apply 允許你在一個例項上呼叫多個方法,我們仍然還沒有完成資料繫結,還有更棒的事兒,讓我們為倉庫定義一個UI模型(這個是github倉庫的資料模型Repository,它持有要展示的資料,請不要和Repository模式的中的Repository搞混淆了哈),要建立一個Kotlin class,點選New -> Kotlin File/Class :
class Repository(var repositoryName: String?,var repositoryOwner: String?,var numberOfStars: Int? ,var hasIssues: Boolean = false)
在Kotlin中,主建構函式是類頭的一部分,如果你不想定義次建構函式,那就是這樣了,資料類到此就完成了,建構函式沒有引數分配給欄位,沒有setters和getters,整個類就一行程式碼。
回到MainActivity.kt
,為Repository
建立一個例項。
var repository = Repository("Medium Android Repository Article",
"Mladen Rakonjac", 1000, true)
你應該注意到了,建立類例項,沒有用new
現在,我們在activity_main.xml
中新增data標籤。
<data>
<variable
name="repository"
type="me.mladenrakonjac.modernandroidapp.uimodels.Repository"
/>
</data>
我們可以在佈局中訪問儲存的變數repository
,例如,我們可以如下使用id是repository_name
的TextView,如下:
android:text="@{repository.repositoryName}"
repository_name文字檢視將顯示從repository變數的屬性repositoryName
獲取的文字。剩下的唯一事情就是將repository
變數從xml繫結到MainActivity.kt
中的repository。
點選Build使DataBinding 為我們生成類,然後在MainActivity中新增兩行程式碼:
binding.repository = repository
binding.executePendingBindings()
如果你執行APP,你會看到TextView上顯示的是:“Medium Android Repository Article”
,非常棒的功能,是吧?
但是,如果我們像下面這樣改一下呢?
Handler().postDelayed({repository.repositoryName="New Name"}, 2000)
新的文字將會在2000ms後顯示嗎?不會的,你必須重新設定一次repository
,像這樣:
Handler().postDelayed({repository.repositoryName="New Name"
binding.repository = repository
binding.executePendingBindings()}, 2000)
但是,如果我們每次更改一個屬性都要這麼寫的話,那就非常蛋疼了,這裡有一個更好的方案叫做Property Observer
。
讓我們首先解釋一下什麼是觀察者模式,因為在rxJava部分中我們也將需要它:
可能你已經聽說過 http://androidweekly.net/
,這是一個關於Android開發的週刊。如果您想接收它,則必須訂閱它並提供您的電子郵件地址。過了一段時間,如果你不想看了,你可以去網站上取消訂閱。
這就是一個觀察者/被觀察者
的模式,在這個例子中, Android 週刊是被觀察者
,它每週都會發布新聞通訊。讀者是觀察者
,因為他們訂閱了它,一旦訂閱就會收到資料,如果不想讀了,則可以停止訂閱。
Property Observer
在這個例子中就是 xml layout,它將會監聽Repository
例項的變化。因此,Repository
是被觀察者
,例如,一旦在Repository類的例項中更改了repository nane 屬性後,xml不呼叫下面的程式碼也會更新:
binding.repository = repository
binding.executePendingBindings()
如何讓它使用Data Binding 庫呢?,Data Binding庫提供了一個BaseObservable
類,我們的Repostory類必須繼承它。
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()
。
以上就是這一節的所有內容,下一節將會講MVVM+Repository 模式的使用。敬請期待!感謝閱讀。
作者 | Mladen Rakoajc
譯者 | 依然範特稀西
編輯 | 依然範特稀西
原文地址:https://proandroiddev.com/mod...
本系列已更新完畢:
【譯】使用Kotlin從零開始寫一個現代Android 專案-Part1
【譯】使用Kotlin從零開始寫一個現代Android 專案-Part2
【譯】使用Kotlin從零開始寫一個現代Android 專案-Part3
【譯】使用Kotlin從零開始寫一個現代Android 專案-Part4
文章首發於公眾號:「 技術最TOP 」
,每天都有乾貨文章持續更新,可以微信搜尋「 技術最TOP 」
第一時間閱讀,回覆【思維導圖】【面試】【簡歷】有我準備一些Android進階路線、面試指導和簡歷模板送給你