简评:目前,在 Android 开发中找到一个覆盖全部的新技术的项目难如登天,因此做者决定本身写一个。本文因此使用的技术包括:html
Android Studio 3, beta1android
Kotlin 语言git
构建变体github
ConstraintLayout算法
数据绑定库数据库
MVVM 架构 + 存储库模式(使用映射器)+ Android Manager Wrappers(Part 2)api
RxJava2 及它如何在架构中起做用安全
Dagger 2.11,什么是依赖注入,为何须要它服务器
改造(使用 Rx Java2)架构
Room(使用 Rx Java2)
咱们的 app 看起来是什么样的?
咱们的 app 将会是最简单的,将使用全部上面提到的技术,只用一个功能:拉取 GitHub 上的全部 google 案例仓库,把这些数据保存到本地数据库并展现给用户。
我将尽量地解释每一行代码。你能够从 github 上跟进我提交的代码。
让咱们一块儿动手:
0. Android Studio
要安装 Android Studio 3 beta1(如今已发布正式版),你要进入这个页面。
注意:若是你想要和以前安装的某个版本共存,在 Mac 上你应该在应用文件夹中重命名旧的版本,如“Android Studio Old”。你能够在这里找到更多信息,包括 Windows 和 Linux。
Android Studio 现已支持 Kotlin。去建立 Android 项目,你会发现新东西:支持 Kotlin 的标签可选框。它是默认选中的。按两下 next,而后选择 Empty Activity,这样就完成了。
1. Kotlin
看看 MainActivity.kt:
package me.fleka.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() 意味着咱们正在继承 AppCompatActivity。
此外,全部的方法都有一个 fun 关键字,在 Kotlin 中你能够没必要使用,取决于你的喜爱。你必须使用
override 关键字,而不是注解。
那么,savedInstanceState: Bundle? 中的 ? 表示什么意思呢?意味着 savedInstanceState 参数多是 Bundle 类型或者为 null。Kotlin 是空安全的语言。若是你定义了:
var a : String
你将获得一个编译错误,由于 a 必须被初始化,它不能为 null 。意味着你必须这样写:
var a : String = "Init value"
若是你像下面这样写,一样你将获得一个编译错误:
a = null
要让 a 成为可空的,你必须这样:
var a : String?
为何这是 Kotlin 语言的一个重要功能呢?由于它帮咱们避免了空指针异常。Android 开发者受够了空指针异常。即使是 null 的创造者,Tony Hoare 先生,也为发明出 null 而道歉了。假设咱们有一个可空的 nameTextView。如下代码将会形成 NPE,若是它是 null 的话:
nameTextView.setEnabled(true)
而 Kotlin 将不容许咱们作相似这样的事。它强制咱们使用 ? 或者 !! 操做符,若是咱们使用 ? 操做符:
nameTextView?.setEnabled(true)
这行代码仅当 nameTextView 不为 null 才会执行。换句话说,若是咱们使用了 !! 操做符:
nameTextView!!.setEnabled(true)
若是 nameTextView 为 null,它将报 NPE。想冒险的人才会用 :)
这只是有关 Kotlin 的一点小小的介绍,随着咱们深刻,后面再也不介绍其余 Kotlin 特性代码。
2. 构建变体
在开发中,你一般会有不一样的环境。最多见的就是测试和生产环境。这些环境在服务器 url,图标,名字,目标 api 上等等有所不一样。在 fleka,咱们的每个项目都要遵照:
finalProduction, 在 Google Play 商店中发布
demoProduction,这个版本有着生产服务器 url 和新功能,可是不会在 Google Play 商店中上线。咱们的客户会和 Google Play 发布的版本一块儿安装,他们会测试这个版本并给咱们反馈。
demoTesting,和 demoProduction 同样,可是使用的是测试服务器 url。
mock,对于开发者和设计者来讲颇有用。有时候咱们的设计准备好了,可是 API 还没准备好。等待 API 准备好才进行开发不是一个很好的选择。这个版本会使用假数据,这样设计团队就能够测试它,并给予咱们反馈。一旦 API 准备好了,咱们就会切换到 demoTesting 环境。
在这个应用中,咱们将会用上述全部的环境。它们有不一样的名字和 applicationId。在 gradle 3.0.0 中有一个新的 api 叫 flavorDimension,容许你混合不一样的开发环境,这样你能够混合 demo 和 minApi23。在咱们的 app 中,咱们将使用默认的 flavorDimension。打开 build.gradle,而后在 android{} 中插入下面的代码:
flavorDimensions "default" productFlavors { finalProduction { dimension "default" applicationId "me.fleka.modernandroidapp" resValue "string", "app_name", "Modern App" } demoProduction { dimension "default" applicationId "me.fleka.modernandroidapp.demoproduction" resValue "string", "app_name", "Modern App Demo P" } demoTesting { dimension "default" applicationId "me.fleka.modernandroidapp.demotesting" resValue "string", "app_name", "Modern App Demo T" } mock { dimension "default" applicationId "me.fleka.modernandroidapp.mock" resValue "string", "app_name", "Modern App Mock" } }
打开 string.xml,删除 app_name 字符串,这样就没有冲突了。而后点击 Sync。若是你打开Build Variants 界面,你会看到四种不一样的变体,每一个都有两种构建类型:Debug 和 Release。切换到demoProduction,而后运行,接着切换到另外一个,而后运行。你应该会看到两个不一样名字的应用。
3. ConstraintLayout
若是你打开 activity_main.xml,你应该会看到 ConstrainLayout 布局。若是你写过 iOS 应用,你应该知道 AutoLayout。ConstrainsLayout 和它很是类似。它们甚至使用了相同的 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.fleka.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" /> </android.support.constraint.ConstraintLayout>
约束帮助咱们描述视图之间的关系。每一个视图都有 4 个约束,每边一个。上面的代码中,咱们的视图每一边都被约束到父视图。
若是你在 Design 选项卡中把 Hello World 文本视图往上挪动一点点,在 Text 选项卡中会出现一行新代码:
app:layout_constraintVertical_bias="0.28"
Design 和 Text 选项卡是同步的。咱们在 Design 上的移动影响了 Text 选项卡中的 xml,反之亦然。垂直误差描述了视图在它的约束中的垂直的趋势。若是你想要视图垂直居中,你应该使用:
app:layout_constraintVertical_bias="0.28"
让咱们的 Activity 仅仅显示一个仓库。它将会有一个仓库名,关注数,拥有者以及会显示仓库有没有问题。
要得到这样的布局,xml 是这样的:
<?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.fleka.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 的一些小介绍。你能够在 Google 代码实验室中找到,在 github 中也有关于ConstraintLayout 的文档。
4. 数据绑定库
当我据说数据绑定库时,我问我本身的第一件事就是,我为何要用 Butterknife ?而在我学习了更多数据绑定的知识后,我发现它真的很是好用。
ButterKnife 能够帮到咱们什么?
ButterKnife 帮助咱们摆脱枯燥的 findViewById。若是你有 5 个视图,没有 ButterKnife,你会有 5 + 5 行代码来绑定你的视图。用了 ButterKnife,你只须要用 5 行代码。
ButterKnife 的缺点是什么?
ButterKnife 依旧没有解决维护代码的问题。当我使用 ButterKnife 时,常常获得一个运行时异常,由于我在 xml 中删除了一个视图,且在 activity/fragment 中忘了删除绑定代码。一样地,当你在 xml 中添加了一个视图,你必须从新绑定一次。这至关麻烦。你在维护绑定时浪费了时间。
什么是数据绑定库?
使用数据绑定库,你只须要用一行代码就能够绑定你的视图!接下来展现一下它是如何工做的。首先添加依赖:
// 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 按钮。打开 activity_main.xml 而后用 layout 标签包裹住 ConstraintLayout:
<?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.fleka.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>
把全部的 xmlns 移动到 layout 标签。而后点击 Build 按钮,或者使用快捷键 Cmd + F9. 咱们须要构建项目,这样数据绑定库可以生成 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)
就这样!你成功地绑定了本身的视图。如今你能够访问它并作一些改动。例如,让咱们把仓库的名字改成“Modern Android Medium Article”:
binding.repositoryName.text = "Modern Android Medium Article"
你能够看到咱们能够经过 binding 变量来访问 activity_main.xml 中的全部视图(固然是有 id 的那些)。这就是为何数据绑定比 ButterKnife 更好的缘由。
5. Kotlin 中的 Getters 和 setters
可能你已经注意到了,Kotlin 没有像 Java 中的 .setText() 方法。我会在这里解释一下与 Java 相比,Kotlin 中的 getters 和 setters 是如何工做的。
首先,你应该知道为何咱们要用 setters 和 getters。咱们用它来隐藏类中的变量,仅容许使用方法来访问这些变量,这样咱们就能够向用户隐藏类中的细节,并禁止用户直接修改咱们的类。假设咱们用 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() 方法,咱们禁止用户把 a 设置为负数,由于正方形的边不为负数。咱们把 a 设置为 private,这样它就不能直接被设置。一样意味着咱们这个类的用户不能直接地拿到 a,因此咱们提供了 getter。getter 返回 a。若是你有 10 个变量,相似地,你要提供 10 个 getters。写这些不经思考的代码很无聊。
Kotlin 让咱们开发者的生活更加简单,若是你调用:
var side: Int = square.a
这并不意味着你直接地访问 a,而是相似这样的:
int side = square.getA();
Kotlin 自动生成默认的 getter 和 setter,除非你须要特殊的 setter 和 getter,你须要定义它们 :
var a = 1 set(value) { field = Math.abs(value) }
field ? 这又是什么?为了看起来更清楚,咱们来看看下面的代码:
var a = 1 set(value) { a = Math.abs(value) }
这意味着你你在 set 方法中调用了 set 方法,由于在 Kotlin 中,你不能直接访问属性。这会形成无穷递归,当你调用 a = something 时,它自动调用了 set 方法。如今你应该知道为何要使用 field 关键字了。
回到咱们的代码,我将向你展现 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 = "Fleka" numberOfStarts.text = "1000 stars" } } }
apply 容许你调用一个实例的多个方法。咱们尚未完成数据绑定,还有更棒的事情。让咱们先为仓库(这是 GitHub 仓库的 UI 模型类,存放了咱们要展现的数据,别和仓库模式搞混了)定义一个 ui 模型类。点击 New -> Kotlin File/Class 来 建立 Kotlin 类:
class Repository(var repositoryName: String?,var repositoryOwner: String?,var numberOfStars: Int? ,var hasIssues: Boolean = false)
在 Kotlin 中,首要构造函数是类的头部的一部分。若是你不提供第二个构造函数,这样就好了,你建立类的工做完成了。没有构造函数参数赋值,也没有 getter 和 setter,所有的类只用一行代码!
回到 MainActivity.kt,建立一个 Repository 类的实例:
var repository = Repository("Medium Android Repository Article", "Fleka", 1000, true)
能够看到,在对象建立中没有 new 关键字。如今打开 activity_main.xml,而后添加一个 data 标签:
<data> <variable name="repository" type="me.fleka.modernandroidapp.uimodels.Repository" /> </data>
咱们能够在 layout 中访问咱们的 Repository 类型的 repository 变量。例如,咱们能够在 TextView 中使用repositoryName:
android:text="@{repository.repositoryName}"
这个 TextView 将会展现从 repository 变量中获得的 repositoryName 属性。最后剩下的就是绑定 xml 中的repository 和 MainActivity.kt 中的repository 变量。点击 Build 按钮,让数据绑定库生成所需的类,而后回到 MainActivity 添加下面的代码:
binding.repository = repository binding.executePendingBindings()
若是你运行 app,你会看到 TextView 展现 “Medium Android Repository Article”。很棒的功能,对吧?:)
但若是咱们这样作:
Handler().postDelayed({repository.repositoryName="New Name"}, 2000)
新的文本会在 2000 毫秒后显示出来吗?并不会。你须要从新设置 repository。像这样:
Handler().postDelayed({repository.repositoryName="New Name" binding.repository = repository binding.executePendingBindings()}, 2000)
若是咱们每次都这样作就很是无趣了,有一个更好的解决方案叫属性观察者。让咱们先来描述一下什么是观察者模式,由于咱们在 RxJava 章节中须要它。
可能你已经据说过 androidweekly 。它是个关于 Android 开发的每周时事资讯。当你想收到资讯,你须要在给定的邮箱地址中订阅它。一段时间后,你可能决定取消订阅。
这就是一个观察者/可观察的模式的例子。这个例子中,Android Weekly 是可观察的,它每周放出资讯,读者是观察者,由于他们在上面订阅了,等待新资讯发送,一旦他们收到了,他们就能够阅读。若是某些人不喜欢,他/她就能够中止监听。
咱们所用的属性观察者就是 xml 布局,他们会监听 Repository 实例的变化。因此,Repository 是可观察的。例如,一旦 Repository 实例的仓库名字这个属性变化了,xml 就可以更新而没必要调用:
binding.repository = repository binding.executePendingBindings()
怎样才能作到?数据绑定库给咱们提供了 BaseObservable 类,Repository 类应该实现这个类:
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()。
英文原文:Modern Android development with Kotlin (September 2017) Part 1