Kotlin 簡介:面向人類的 Android 編程
已發表: 2022-03-11在一個完美的 Android 世界中,Java 的主要語言確實是現代、清晰和優雅的。 你可以通過做更多的事情來減少編寫,並且每當出現新功能時,開發人員只需在 Gradle 中增加版本即可使用它。 然後在創建一個非常好的應用程序時,它看起來是完全可測試的、可擴展的和可維護的。 我們的活動並不太龐大和復雜,我們可以將數據源從數據庫更改為網絡,而沒有大量差異,等等。 聽起來不錯,對吧? 不幸的是,Android 世界並非如此理想。 谷歌仍在追求完美,但我們都知道理想世界並不存在。 因此,我們必須在 Android 世界的偉大旅程中幫助自己。
什麼是 Kotlin,為什麼要使用它?
所以,第一語言。 我認為 Java 不是優雅或清晰的大師,它既不現代也不富有表現力(我猜你同意)。 缺點是在Android N以下,我們仍然受限於Java 6(包括Java 7的一些小部分)。 開發人員還可以附加 RetroLambda 以在他們的代碼中使用 lambda 表達式,這在使用 RxJava 時非常有用。 在 Android N 之上,我們可以使用 Java 8 的一些新功能,但它仍然是舊的、沉重的 Java。 我經常聽到 Android 開發人員說“我希望 Android 支持一種更好的語言,就像 iOS 支持 Swift 一樣”。 如果我告訴你,你可以使用一種非常好的、簡單的語言,具有空安全性、lambdas 和許多其他不錯的新特性,你會怎麼做? 歡迎來到 Kotlin。
什麼是科特林?
Kotlin 是一種新語言(有時稱為 Swift for Android),由 JetBrains 團隊開發,現在是 1.0.2 版本。 它在 Android 開發中的有用之處在於它可以編譯為 JVM 字節碼,也可以編譯為 JavaScript。 它與 Java 完全兼容,Kotlin 代碼可以簡單地轉換為 Java 代碼,反之亦然(JetBrains 有一個插件)。 這意味著 Kotlin 可以使用任何用 Java 編寫的框架、庫等。 在 Android 上,它由 Gradle 集成。 如果你有一個現有的 Android 應用程序,並且想在不重寫整個應用程序的情況下在 Kotlin 中實現一個新功能,那麼只需開始用 Kotlin 編寫,它就會工作。
但是什麼是“偉大的新功能”? 讓我列舉幾個:
可选和命名函數參數
fun createDate(day: Int, month: Int, year: Int, hour: Int = 0, minute: Int = 0, second: Int = 0) { print("TEST", "$day-$month-$year $hour:$minute:$second") }
我們可以用不同的方式調用方法 createDate
createDate(1,2,2016) prints: '1-2-2016 0:0:0' createDate(1,2,2016, 12) prints: '1-2-2016 12:0:0' createDate(1,2,2016, minute = 30) prints: '1-2-2016 0:30:0'
零安全
如果變量可以為空,除非我們強制它們編譯,否則代碼將無法編譯。 以下代碼將有錯誤 - nullableVar 可能為空:
var nullableVar: String? = “”; nullableVar.length;
要編譯,我們必須檢查它是否不為空:
if(nullableVar){ nullableVar.length }
或者,更短:
nullableVar?.length
這樣,如果 nullableVar 為 null,則不會發生任何事情。 否則,我們可以將變量標記為不可為空,類型後不帶問號:
var nonNullableVar: String = “”; nonNullableVar.length;
此代碼編譯,如果我們想將 null 分配給 nonNullableVar,編譯器將顯示錯誤。
還有一個非常有用的 Elvis 運算符:
var stringLength = nullableVar?.length ?: 0
然後,當 nullableVar 為 null(所以 nullableVar?.length 返回 null)時,stringLength 的值為 0。
可變和不可變變量
在上面的示例中,我在定義變量時使用了 var。 這是可變的,我們可以隨時重新分配它。 如果我們希望該變量是不可變的(在許多情況下我們應該),我們使用 val(作為值,而不是變量):
val immutable: Int = 1
之後,編譯器將不允許我們重新分配給 immutable。
拉姆達斯
我們都知道什麼是 lambda,所以在這裡我將展示如何在 Kotlin 中使用它:
button.setOnClickListener({ view -> Log.d("Kotlin","Click")})
或者如果函數是唯一或最後一個參數:
button.setOnClickListener { Log.d("Kotlin","Click")}
擴展
擴展是一個非常有用的語言特性,借助它我們可以“擴展”現有的類,即使它們是最終的或者我們無法訪問它們的源代碼。
例如,要從編輯文本中獲取字符串值,而不是每次都編寫 editText.text.toString() 我們可以編寫函數:
fun EditText.textValue(): String{ return text.toString() }
或更短:
fun EditText.textValue() = text.toString()
現在,對於 EditText 的每個實例:
editText.textValue()
或者,我們可以添加一個返回相同的屬性:
var EditText.textValue: String get() = text.toString() set(v) {setText(v)}
運算符重載
如果我們想要添加、相乘或比較對象,有時很有用。 Kotlin 允許重載二元運算符(加號、減號、plusAssign、範圍等)、數組運算符(get、set、get range、set range)以及等於和一元運算符(+a、-a 等)
數據類
你需要多少行代碼才能在 Java 中實現一個具有三個屬性的 User 類:copy、equals、hashCode 和 toString? 在 Kaotlin 中,您只需要一行:
data class User(val name: String, val surname: String, val age: Int)
該數據類提供 equals()、hashCode() 和 copy() 方法,以及 toString(),它將 User 打印為:
User(name=John, surname=Doe, age=23)
數據類還提供了一些其他有用的功能和屬性,您可以在 Kotlin 文檔中看到這些功能和屬性。
Anko 擴展
您使用 Butterknife 或 Android 擴展程序,不是嗎? 如果您甚至不需要使用這個庫,並且在 XML 中聲明視圖之後,只需通過其 ID 從代碼中使用它(就像 C# 中的 XAML):
<Button android: android:layout_width="match_parent" android:layout_height="wrap_content" />
loginBtn.setOnClickListener{}
Kotlin 有非常有用的 Anko 擴展,有了這個你不需要告訴你的活動什麼是 loginBtn,它只需要“導入”xml 就知道了:
import kotlinx.android.synthetic.main.activity_main.*
Anko 中還有很多其他有用的東西,包括開始活動、展示敬酒等等。 這不是 Anko 的主要目標——它旨在輕鬆地從代碼創建佈局。 因此,如果您需要以編程方式創建佈局,這是最好的方法。
這只是 Kotlin 的一個簡短介紹。 我推薦閱讀 Antonio Leiva 的博客和他的書 - Kotlin for Android Developers,當然還有 Kotlin 官方網站。
什麼是 MVP,為什麼?
一個好的、強大的、清晰的語言是不夠的。 如果沒有良好的架構,很容易用每種語言編寫雜亂無章的應用程序。 Android 開發人員(大多數是剛入門的開發人員,但也有更高級的開發人員)通常讓 Activity 負責他們周圍的一切。 Activity(或 Fragment 或其他視圖)下載數據、發送以保存、呈現它們、響應用戶交互、編輯數據、管理所有子視圖。 . . 而且往往更多。 對於像 Activity 或 Fragments 這樣的不穩定對象來說太多了(旋轉屏幕和 Activity 說“再見……”就足夠了)。
一個非常好的想法是將職責與視圖隔離開來,並使它們盡可能愚蠢。 視圖(活動、片段、自定義視圖或任何在屏幕上呈現數據的東西)應該只負責管理它們的子視圖。 視圖應該有演示者,他們將與模型交流,並告訴他們應該做什麼。 簡而言之,這就是 Model-View-Presenter 模式(對我來說,它應該命名為 Model-Presenter-View 以顯示層之間的連接)。
“嘿,我知道類似的東西,它叫 MVC!” - 你不覺得嗎? 不,MVP 與 MVC 不同。 在 MVC 模式中,您的視圖可以與模型通信。 在使用 MVP 時,您不允許這兩個層之間進行任何通信 - View 與 Model 通信的唯一方式是通過 Presenter。 View 唯一了解 Model 的可能是數據結構。 例如,視圖知道如何顯示用戶,但不知道何時。 這是一個簡單的例子:
View 知道“我是 Activity,我有兩個 EditText 和一個 Button。 當有人單擊按鈕時,我應該將它告訴我的演示者,並將 EditTexts 的值傳遞給他。 僅此而已,我可以睡到下一次點擊或演示者告訴我該怎麼做。”
Presenter 知道某個地方是一個 View,並且他知道這個 View 可以執行哪些操作。 他還知道,當他收到兩個字符串時,他應該從這兩個字符串中創建用戶並將數據發送到模型進行保存,如果保存成功,則告訴視圖'顯示成功信息'。
模型只知道數據在哪裡,應該保存在哪裡,以及應該對數據執行什麼操作。
用 MVP 編寫的應用程序易於測試、維護和重用。 一個純粹的演示者應該對 Android 平台一無所知。 它應該是純 Java(或 Kotlin,在我們的例子中)類。 多虧了這一點,我們可以在其他項目中重用我們的演示者。 我們還可以輕鬆編寫單元測試,分別測試 Model、View 和 Presenter。
有點題外話:MVP 應該是 Bob 叔叔的清潔架構的一部分,以使應用程序更加靈活和架構更好。 下次我會試著寫一下。
帶有 MVP 和 Kotlin 的示例應用程序
理論就夠了,讓我們看一些代碼! 好的,讓我們嘗試創建一個簡單的應用程序。 這個應用程序的主要目標是創建用戶。 第一個屏幕將有兩個 EditTexts(姓名和姓氏)和一個按鈕(保存)。 輸入姓名和姓氏並單擊“保存”後,應用程序應顯示“用戶已保存”並轉到下一個屏幕,顯示保存的姓名和姓氏。 當 name 或 surname 為空時,應用程序不應保存用戶並顯示錯誤指示錯誤。
創建 Android Studio 項目後的第一件事就是配置 Kotlin。 您應該安裝 Kotlin 插件,並在重新啟動後,在 Tools > Kotlin 中單擊“Configure Kotlin in Project”。 IDE 會將 Kotlin 依賴項添加到 Gradle。 如果您有任何現有代碼,您可以通過(Ctrl+Shift+Alt+K 或代碼 > 將 Java 文件轉換為 Kotlin)輕鬆地將其轉換為 Kotlin。 如果出現問題並且項目無法編譯,或者 Gradle 沒有看到 Kotlin,您可以查看 GitHub 上可用的應用程序代碼。
現在我們有了一個項目,讓我們從創建第一個視圖開始 - CreateUserView。 這個視圖應該具有前面提到的功能,所以我們可以為此編寫一個接口:
interface CreateUserView : View { fun showEmptyNameError() /* show error when name is empty */ fun showEmptySurnameError() /* show error when surname is empty */ fun showUserSaved() /* show user saved info */ fun showUserDetails(user: User) /* show user details */ }
如您所見,Kotlin 在聲明函數方面與 Java 類似。 所有這些都是不返回任何內容的函數,最後一個只有一個參數。 這就是區別,參數類型在名稱之後。 View 界面不是來自 Android - 它是我們簡單的空界面:
interface View
Basic Presenter 的接口應該有一個 View 類型的屬性,並且至少有一個方法(例如 onDestroy),這個屬性將被設置為 null:
interface Presenter<T : View> { var view: T? fun onDestroy(){ view = null } }
在這裡你可以看到另一個 Kotlin 特性——你可以在接口中聲明屬性,也可以在那裡實現方法。

我們的 CreateUserView 需要與 CreateUserPresenter 通信。 此 Presenter 需要的唯一附加函數是帶有兩個字符串參數的 saveUser:
interface CreateUserPresenter<T : View>: Presenter<T> { fun saveUser(name: String, surname: String) }
我們還需要模型定義——前面提到過數據類:
data class User(val name: String, val surname: String)
聲明完所有接口後,我們就可以開始實現了。
CreateUserPresenter 將在 CreateUserPresenterImpl 中實現:
class CreateUserPresenterImpl(override var view: CreateUserView?): CreateUserPresenter<CreateUserView> { override fun saveUser(name: String, surname: String) { } }
第一行,帶有類定義:
CreateUserPresenterImpl(override var view: CreateUserView?)
是一個構造函數,我們用它來分配視圖屬性,定義在接口中。
MainActivity,也就是我們的 CreateUserView 實現,需要一個 CreateUserPresenter 的引用:
class MainActivity : AppCompatActivity(), CreateUserView { private val presenter: CreateUserPresenter<CreateUserView> by lazy { CreateUserPresenterImpl(this) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) saveUserBtn.setOnClickListener{ presenter.saveUser(userName.textValue(), userSurname.textValue()) /*use of textValue() extension, mentioned earlier */ } } override fun showEmptyNameError() { userName.error = getString(R.string.name_empty_error) /* it's equal to userName.setError() - Kotlin allows us to use property */ } override fun showEmptySurnameError() { userSurname.error = getString(R.string.surname_empty_error) } override fun showUserSaved() { toast(R.string.user_saved) /* anko extension - equal to Toast.makeText(this, R.string.user_saved, Toast.LENGTH_LONG) */ } override fun showUserDetails(user: User) { } override fun onDestroy() { presenter.onDestroy() } }
在課程開始時,我們定義了我們的演示者:
private val presenter: CreateUserPresenter<CreateUserView> by lazy { CreateUserPresenterImpl(this) }
它被定義為不可變(val),並由惰性委託創建,將在第一次需要時分配。 此外,我們確定它不會為空(定義後沒有問號)。
當用戶單擊 Save 按鈕時,View 會向 Presenter 發送帶有 EditTexts 值的信息。 發生這種情況時,應該保存 User,因此我們必須在 Presenter 中實現 saveUser 方法(以及 Model 的一些功能):
override fun saveUser(name: String, surname: String) { val user = User(name, surname) when(UserValidator.validateUser(user)){ UserError.EMPTY_NAME -> view?.showEmptyNameError() UserError.EMPTY_SURNAME -> view?.showEmptySurnameError() UserError.NO_ERROR -> { UserStore.saveUser(user) view?.showUserSaved() view?.showUserDetails(user) } } }
創建用戶時,會將其發送到 UserValidator 以檢查其有效性。 然後,根據驗證結果,調用適當的方法。 when() {} 構造與 Java 中的 switch/case 相同。 但它更強大——Kotlin 不僅允許在“case”中使用 enum 或 int,還允許使用範圍、字符串或對像類型。 它必須包含所有可能性或具有 else 表達式。 在這裡,它涵蓋了所有 UserError 值。
通過使用 view?.showEmptyNameError() (在視圖後帶有問號),我們可以免受 NullPointer 的影響。 View 可以在 onDestroy 方法中為空,並且通過這種構造,什麼都不會發生。
當一個 User 模型沒有錯誤時,它告訴 UserStore 保存它,然後指示 View 顯示成功並顯示詳細信息。
如前所述,我們必須實現一些模型的東西:
enum class UserError { EMPTY_NAME, EMPTY_SURNAME, NO_ERROR } object UserStore { fun saveUser(user: User){ //Save user somewhere: Database, SharedPreferences, send to web... } } object UserValidator { fun validateUser(user: User): UserError { with(user){ if(name.isNullOrEmpty()) return UserError.EMPTY_NAME if(surname.isNullOrEmpty()) return UserError.EMPTY_SURNAME } return UserError.NO_ERROR } }
這裡最有趣的是 UserValidator。 通過使用對象詞,我們可以創建一個單例類,不用擔心線程、私有構造函數等。
接下來 - 在 validateUser(user) 方法中,有 with(user) {} 表達式。 此類塊中的代碼在對象的上下文中執行,傳入的名稱和姓氏是用戶的屬性。
還有一件小事。 以上所有代碼,從枚舉到UserValidator,定義放在一個文件中(User類的定義也在這裡)。 Kotlin 不會強迫您將每個公共類都放在單個文件中(或將類命名為文件)。 因此,如果您有一些簡短的相關代碼(數據類、擴展、函數、常量 - Kotlin 不需要函數或常量的類),您可以將它放在一個文件中,而不是分散在項目中的所有文件中。
當用戶被保存時,我們的應用程序應該顯示它。 我們需要另一個視圖——它可以是任何 Android 視圖、自定義視圖、片段或活動。 我選擇了活動。
所以,讓我們定義 UserDetailsView 接口。 它可以顯示用戶,但當用戶不存在時它也應該顯示錯誤:
interface UserDetailsView { fun showUserDetails(user: User) fun showNoUserError() }
接下來,UserDetailsPresenter。 它應該有一個用戶屬性:
interface UserDetailsPresenter<T: View>: Presenter<T> { var user: User? }
該接口將在 UserDetailsPresenterImpl 中實現。 它必須覆蓋用戶屬性。 每次分配此屬性時,都應在視圖上刷新用戶。 我們可以為此使用屬性設置器:
class UserDetailsPresenterImpl(override var view: UserDetailsView?): UserDetailsPresenter<UserDetailsView> { override var user: User? = null set(value) { field = value if(field != null){ view?.showUserDetails(field!!) } else { view?.showNoUserError() } } }
UserDetailsView 的實現,UserDetailsActivity,非常簡單。 就像之前一樣,我們有一個通過延遲加載創建的演示者對象。 要顯示的用戶應通過意圖傳遞。 目前有一個小問題,我們稍後會解決。 當我們從意圖中獲得用戶時,View 需要將其分配給他們的演示者。 之後,用戶將在屏幕上刷新,或者,如果它為 null,則會出現錯誤(並且活動將完成 - 但演示者不知道):
class UserDetailsActivity: AppCompatActivity(), UserDetailsView { private val presenter: UserDetailsPresenter<UserDetailsView> by lazy { UserDetailsPresenterImpl(this) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_user_details) val user = intent.getParcelableExtra<User>(USER_KEY) presenter.user = user } override fun showUserDetails(user: User) { userFullName.text = "${user.name} ${user.surname}" } override fun showNoUserError() { toast(R.string.no_user_error) finish() } override fun onDestroy() { presenter.onDestroy() } }
通過意圖傳遞對像要求該對象實現 Parcelable 接口。 這是非常“骯髒”的工作。 就個人而言,我討厭這樣做,因為所有的創造者、屬性、保存、恢復等等。 幸運的是,有適合 Kotlin 的 Parcelable 插件。 安裝後,我們可以一鍵生成Parcelable。
最後要做的是在我們的 MainActivity 中實現 showUserDetails(user: User):
override fun showUserDetails(user: User) { startActivity<UserDetailsActivity>(USER_KEY to user) /* anko extension - starts UserDetailsActivity and pass user as USER_KEY in intent */ }
就這樣。
我們有一個簡單的應用程序可以保存用戶(實際上它沒有保存,但我們可以在不觸摸演示者或視圖的情況下添加此功能)並將其呈現在屏幕上。 未來,如果我們想改變用戶在屏幕上的呈現方式,比如從兩個activity變為一個activity中的兩個fragment,或者兩個自定義視圖,那麼只會在View類中進行更改。 當然,如果我們不改變功能或模型的結構。 不知道 View 到底是什麼的 Presenter 不需要任何更改。
下一步是什麼?
在我們的應用程序中,每次創建活動時都會創建 Presenter。 如果 Presenter 應該跨活動實例持續存在,這種方法或相反的方法是 Internet 上許多討論的主題。 對我來說,這取決於應用程序、它的需求和開發人員。 有時最好是銷毀presenter,有時則不是。 如果你決定堅持一個,一個非常有趣的技術是為此使用 LoaderManager。
如前所述,MVP 應該是 Bob 大叔 Clean 架構的一部分。 此外,優秀的開發人員應該使用 Dagger 將 Presenter 依賴項注入到活動中。 它還有助於將來維護、測試和重用代碼。 目前,Kotlin 與 Dagger(在正式發布之前並不容易)以及其他有用的 Android 庫配合得非常好。
包起來
對我來說,Kotlin 是一門很棒的語言。 它現代、清晰、富有表現力,同時仍由偉大的人開發。 我們可以在任何 Android 設備和版本上使用任何新版本。 無論什麼讓我對 Java 感到憤怒,Kotlin 都在改進。
當然,正如我所說,沒有什麼是理想的。 Kotlin 也有一些缺點。 最新的 gradle 插件版本(主要來自 alpha 或 beta)不適用於這種語言。 許多人抱怨構建時間比純 Java 長一點,而且 apk 有一些額外的 MB。 但 Android Studio 和 Gradle 仍在改進,手機有越來越多的應用空間。 這就是為什麼我相信 Kotlin 可以成為每個 Android 開發人員非常好的語言。 試一試,並在下面的評論部分分享您的想法。
示例應用程序的源代碼可在 Github 上獲得:github.com/tomaszczura/AndroidMVPKotlin