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!” - 你不觉得吗? 不,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 模式通过保持用户界面和业务逻辑真正分离来产生更好、更简单的代码库。

有点题外话: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 上可用的应用程序代码。

Kotlin 不仅与 Java 框架和库具有良好的互操作性,它还允许您继续使用您已经熟悉的大多数相同工具。

现在我们有了一个项目,让我们从创建第一个视图开始 - 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 */ }

就这样。

在 Kotlin 中演示 Android 应用程序

我们有一个简单的应用程序可以保存用户(实际上它没有保存,但我们可以在不触摸演示者或视图的情况下添加此功能)并将其呈现在屏幕上。 未来,如果我们想改变用户在屏幕上的呈现方式,比如从两个activity变为一个activity中的两个fragment,或者两个自定义视图,那么只会在View类中进行更改。 当然,如果我们不改变功能或模型的结构。 不知道 View 到底是什么的 Presenter 不需要任何更改。

在您的 Android 应用程序中遇到性能问题? 查看这些优化技巧和技术。

下一步是什么?

在我们的应用程序中,每次创建活动时都会创建 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