本文概述
在一个完美的Android世界中, Java的主要语言确实是现代, 清晰和优雅的。你可以通过做更多的事情来减少编写, 并且只要出现新功能, 开发人员就可以通过在Gradle中增加版本来使用它。然后, 在创建一个非常漂亮的应用程序时, 它似乎可以完全测试, 扩展和维护。我们的活动不是太大也不复杂, 我们可以将数据源从数据库更改为Web, 而不会产生很多差异, 依此类推。听起来不错吧?不幸的是, Android世界并非如此。 Google仍在追求完美, 但我们都知道理想世界并不存在。因此, 我们必须在Android世界的那段伟大旅程中自助。
Kotlin是Android世界中颇受欢迎的新玩家。但是它能代替Java吗?
鸣叫
什么是Kotlin, 为什么要使用它?
因此, 第一语言。我认为Java并不是优雅或清晰的大师, 它既不是现代的也不是表现力强的(我想你同意)。缺点是, 在Android N以下, 我们仍然仅限于Java 6(包括Java 7的一些小部分)。开发人员还可以附加RetroLambda以在其代码中使用lambda表达式, 这在使用RxJava时非常有用。在Android N之上, 我们可以使用Java 8的一些新功能, 但是仍然是旧的, 沉重的Java。我经常听到Android开发人员说:”我希望Android支持更好的语言, 就像iOS与Swift一样。”如果我告诉过你, 你可以使用一种非常漂亮, 简单的语言, 并具有null安全性, lambda和其他许多不错的新功能, 该怎么办?欢迎来到科特林。
什么是科特林?
Kotlin是由JetBrains团队开发的一种新语言(有时称为Android的Swift), 现在是其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’
空安全
如果变量可以为null, 则除非我们强迫它们进行编译, 否则代码将不会编译。以下代码将出现错误-nullableVar可能为null:
var nullableVar: String? = "";
nullableVar.length;
要进行编译, 我们必须检查它是否不为null:
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
之后, 编译器将不允许我们将其重新分配为不可变的。
Lambdas
我们都知道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, 范围等), 数组运算符(获取, 设置, 获取范围, 设置范围)以及等于和一元运算符(+ a, -a等)的重载。
资料类别
你需要多少行代码才能在Java中实现具有三个属性的用户类: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:id="@+id/loginBtn"
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的主要目标-它旨在轻松地从代码创建布局。因此, 如果你需要以编程方式创建布局, 那么这是最好的方法。
这只是科特林的一小部分。我建议阅读Antonio Leiva的博客和他的著作-Android开发者专用的Kotlin, 当然还要阅读Kotlin的官方网站。
什么是MVP, 为什么?
好的, 功能强大且清晰的语言是不够的。在没有良好架构的情况下, 用各种语言编写混乱的应用程序非常容易。 Android开发人员(大多数是入门人员, 但也包括更高级的开发人员)通常将Activity责任交给周围的所有人。活动(或片段或其他视图)下载数据, 发送以保存, 呈现它们, 响应用户交互, 编辑数据, 管理所有子视图。 。 。甚至更多。对于诸如活动或片段之类的不稳定对象来说, 这太过分了(足以旋转屏幕, 并且活动说”再见…”。)。
一个很好的想法是将责任与观点隔离开来, 并使它们尽可能地愚蠢。视图(活动, 片段, 自定义视图或任何在屏幕上显示数据的视图)应仅负责管理其子视图。视图应该有演示者, 演示者将与模型进行交流, 并告诉他们应该做什么。简而言之, 这就是Model-View-Presenter模式(对我来说, 应该命名为Model-Presenter-View以显示层之间的连接)。
“嘿, 我知道这样的东西, 它叫做MVC!” -你没想到吗?不, MVP与MVC不同。在MVC模式中, 你的视图可以与模型进行通信。使用MVP时, 你不允许在这两层之间进行任何通信-View与Model进行通信的唯一方法是通过Presenter。 View唯一了解Model的就是数据结构。 View知道例如显示用户的方式, 但不知道何时显示。这是一个简单的示例:
View知道”我正在活动, 我有两个EditText和一个Button。当有人单击该按钮时, 我应该将其告知演示者, 然后将其传递给EditTexts的值。就是这样, 我可以一直睡觉, 直到下次单击或演示者告诉我该怎么做为止。”
演示者知道某个地方是一个View, 并且他知道该View可以执行哪些操作。他还知道, 当他收到两个字符串时, 应该从这两个字符串中创建用户, 并将数据发送到模型以进行保存, 如果保存成功, 请告诉视图”显示成功信息”。
模型只知道数据在哪里, 应该将它们保存在哪里, 以及应该对数据执行哪些操作。
用MVP编写的应用程序易于测试, 维护和重用。纯粹的演示者应该对Android平台一无所知。它应该是纯Java类(在我们的例子中为Kotlin)。因此, 我们可以在其他项目中重用演示者。我们还可以轻松编写单元测试, 分别测试Model, View和Presenter。
MVP模式通过将用户界面和业务逻辑真正分开来实现更好, 更简单的代码库。
有点题外话:MVP应该是Bob叔叔的Clean Architecture的一部分, 以使应用程序更加灵活且结构良好。下次我会尝试写些有关。
具有MVP和Kotlin的示例应用
理论已经足够了, 让我们看一些代码!好的, 让我们尝试创建一个简单的应用。该应用程序的主要目标是创建用户。第一个屏幕将具有两个EditTexts(名称和姓氏)和一个Button(保存)。输入名称和姓氏并单击”保存”后, 应用程序应显示”用户已保存”, 然后转到下一个屏幕, 其中显示已保存的名称和姓氏。如果名称或姓氏为空, 则该应用程序不应保存用户, 并显示错误提示。
创建Android Studio项目后的第一件事是配置Kotlin。你应该安装Kotlin插件, 重新启动后, 在”工具”>” Kotlin”中, 你可以单击”在项目中配置Kotlin”。 IDE会将Kotlin依赖项添加到Gradle。如果你有任何现有代码, 则可以通过以下方式轻松地将其转换为Kotlin:(Ctrl + Shift + Alt + K或”代码”>”将Java文件转换为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
基本Presenter的界面应具有View类型的属性, 至少应具有on方法(例如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?)
是一个构造函数, 我们用它来分配在接口中定义的view属性。
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), 由延迟委托创建, 它将在第一次需要时分配。此外, 我们确保它不会为null(定义后没有问号)。
当用户单击”保存”按钮时, View将使用EditTexts值将信息发送到Presenter。发生这种情况时, 应该保存用户, 因此我们必须在Presenter中实现saveUser方法(以及某些模型的功能):
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的攻击。可以在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非常简单。和以前一样, 我们有一个通过延迟加载创建的presenter对象。要显示的用户应通过意图传递。现在有一个小问题, 我们将在稍后解决。当我们有意图的用户时, 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接口。这是非常”肮脏”的工作。就个人而言, 由于所有CREATOR, 属性, 保存, 还原等原因, 我都不愿意这样做。幸运的是, 有一个合适的插件Parcelable for Kotlin。安装后, 我们只需单击一下即可生成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 */
}
就这样。
我们有一个简单的应用程序可以保存用户(实际上, 它不会保存, 但是我们可以添加此功能而无需触摸演示者或视图)并将其显示在屏幕上。将来, 如果我们想更改用户在屏幕上的显示方式, 例如从两个活动更改为一个活动中的两个片段或两个自定义视图, 则更改将仅在View类中进行。当然, 如果我们不更改功能或模型的结构。演示者不知道View到底是什么, 就不需要进行任何更改。
你的Android应用有性能问题吗?查看这些优化技巧和技术。
下一步是什么?
在我们的应用程序中, 每次创建活动时都会创建Presenter。如果Presenter应该在活动实例之间持续存在, 则这种方法或与之相反的方法是Internet上很多讨论的主题。对我而言, 这取决于应用程序, 其需求和开发人员。有时最好销毁主持人, 有时则不行。如果你决定坚持一个, 那么非常有趣的技术是使用LoaderManager。
如前所述, MVP应该是Bob叔叔的Clean架构的一部分。此外, 优秀的开发人员应使用Dagger将演示者对活动的依赖项注入。它还有助于将来维护, 测试和重用代码。目前, Kotlin与Dagger(在正式发行之前还不那么容易)和其他有用的Android库配合得很好。
本文总结
对我来说, 科特林是一门很棒的语言。它是现代的, 清晰的和富有表现力的, 同时仍然由伟大的人们开发。我们可以在任何Android设备和版本上使用任何新版本。无论是什么让我对Java感到生气, Kotlin都会有所改进。
当然, 正如我所说, 没有什么是理想的。 Kotlin也有一些缺点。最新的gradle插件版本(主要来自alpha或beta)不适用于该语言。许多人抱怨构建时间比纯Java要长一些, 而apk则有一些额外的MB。但是Android Studio和Gradle仍在改进, 手机拥有越来越多的应用程序空间。这就是为什么我认为Kotlin对于每个Android开发人员来说都是一种很好的语言。只需尝试一下, 然后在你的想法下方的评论部分中分享。
示例应用程序的源代码可在Github上找到:github.com/tomaszczura/AndroidMVPKotlin
评论前必须登录!
注册