经常在medium.com上看到一些高质量的技术帖子,但是由于国内的上网环境或者有的同学对于看英文比较排斥,错过了不少好文章。因此,西哥决定弄一个《优质译文专栏》,花一些时间翻译一些优质技术文给大家。这篇文章是一个小系列,用Kotlin开发现代Android APP,总共四篇,后面的会陆续翻译!以下是正文。
现在,真的很难找到一个涵盖所有Android新技术的项目,因此我决定自己来写一个,在本文中,我们将用到如下技术:
我们的APP是一个非常简单的应用程序,它涵盖了上面提到的所有技术。只有一个简单的功能:从Github 获取googlesamples
用户下的所有仓库,将数据储存到本地数据库,然后在界面展示它。
我将尝试解释更多的代码,你也可以看看你Github上的代码提交。
Github:https://github.com/mladenrako...
让我们开始吧。
首先安卓Android Studio 3 beta 1(注:现在最新版为Android Studio 4.0),Android Studio 已经支持Kotlin,去到Create Android Project
界面,你将在此处看到新的内容:带有标签的复选框include Kotlin support
。默认情况下选中。按两次下一步,然后选择EmptyActivity
,然后完成了。 恭喜!你用Kotlin开发了第一个Android app)
在刚才新建的项目中,你可以看到一个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特定代码。
通常,在开发中,如果你有两套环境,最常见的是测试环境和生产环境。这些环境在服务器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
构建变体并运行它。然后切换到另一个并运行它。您就可以看到两个名称不同的应用程序。
如果你打开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...
当我听到Data binding 库的时候,我的第一反应是:Butterknife已经很好了,再加上,我现在使用一个插件来从xml中获取View,我为啥要改变,来使用Data binding呢?但当我对Data binding有了更多的了解之后,我的它的感觉就像我第一次见到Butterknife一样,无法自拔。
ButterKnife帮助我们摆脱无聊的findViewById
。因此,如果您有5个视图,而没有Butterknife,则你有5 + 5行代码来绑定您的视图。使用ButterKnife,您只有我行代码就搞定。就是这样。
Butterknife仍然没有解决代码可维护问题,使用ButterKnife时,我经常发现自己遇到运行时异常,这是因为我删除了xml中的视图,而没有删除Activity/Fragment类中的绑定代码。另外,如果要在xml中添加视图,则必须再次进行绑定。真的很不好维护。你将浪费大量时间来维护View绑定。
有很多好处,使用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 好用的原因。
大概,你已经注意到了,我们没有像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进阶路线、面试指导和简历模板送给你