【译】使用 Kotlin 从零开始写一个现代 Android- 项目 -Part1(1)
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" />
<TextViewandroid: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 fileapply plugin: 'kotlin-kapt'
android {//other things that we already useddataBinding.enabled = true}dependencies {//other dependencies that we usedkapt "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.ConstraintLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"tools:context="me.mladenrakonjac.modernandroidapp.MainActivity">
<TextViewandroid: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" />
<TextViewandroid: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" />
<TextViewandroid:id="@+id/repository_owner"android:layout_width="0dp"android:layout_height="wrap_content"android:layout_marginBottom="8dp"android:layout_marginE
nd="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" />
<TextViewandroid: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 = 1set(value) { field = Math.abs(value) }
field
?? 这又是个什么东西?为了更清楚明白,请看下面代码:
var a = 1set(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><variablename="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 = repositorybinding.executePendingBindings()
如果你运行 APP,你会看到 TextView 上显示的是:“Medium Android Repository Article”
,非常棒的功能,是吧?
但是,如果我们像下面这样改一下呢?
Handler().postDelayed({repository.repositoryName="New Name"},?2000)
新的文本将会在 2000ms 后显示吗?不会的,你必须重新设置一次repository
,像这样:
Handler().postDelayed({repository.repositoryName="New Name"binding.repository = repositorybinding.executePendingBindings()}, 2000)
但是,如果我们每次更改一个属性都要这么写的话,那就非常蛋疼了,这里有一个更好的方案叫做Property Observer
。
让我们首先解释一下什么是观察者模式,因为在 rxJava 部分中我们也将需要它:
可能你已经听说过?http://androidweekly.net/
,这是一个关于 Android 开发的周刊。如果您想接收它,则必须订阅它并提供您的电子邮件地址。过了一段时间,如果你不想看了,你可以去网站上取消订阅。
这就是一个观察者/被观察者
的模式,在这个例子中, Android 周刊是被观察者
,它每周都会发布新闻通讯。读者是观察者
,因为他们订阅了它,一旦订阅就会收到数据,如果不想读了,则可以停止订阅。
Property Observer
在这个例子中就是 xml layout,它将会监听Repository
实例的变化。因此,Repository
是被观察者
,例如,一旦在 Repository 类的实例中更改了 repository nane 属性后,xml 不调用下面的代码也会更新:
binding.repository = repositorybinding.executePendingBindings()
如何让它使用 Data Binding 库呢?,Data Binding 库提供了一个BaseObservable
类,我们的 Repostory 类必须继承它。
class Repository(repositoryName : String, var repositoryOwner: String?, var numberOfStars: Int?, var hasIssues: Boolean = false) : BaseObservable(){
@get:Bindablevar repositoryName : String = ""set(value) {field = valuenotifyPropertyChanged(BR.repositoryName)}
}
当我们使用了 Bindable 注解时,就会自动生成 BR 类。你会看到,一旦设置新值,就会通知它更新。现在运行 app 你将看到仓库的名字在 2 秒后改变而不必再次调用?executePendingBindings()
。
以上就是这一节的所有内容,下一节将会讲 MVVM+Repository 模式的使用。敬请期待!感谢阅读。
评论