前言

常常在medium.com上看到一些高质量的技术帖子,然而因为国内的上网环境或者有的同学对于看英文比拟排挤,错过了不少好文章。因而,西哥决定弄一个《优质译文专栏》,花一些工夫翻译一些优质技术文给大家。这篇文章是一个小系列,用Kotlin开发古代Android APP,总共四篇,前面的会陆续翻译!以下是注释。

当初,真的很难找到一个涵盖所有Android新技术的我的项目,因而我决定本人来写一个,在本文中,咱们将用到如下技术:

  • 0 、Android Studio
  • 1、Kotlin 语言
  • 2、构建变体
  • 3、ConstraintLayout
  • 4、DataBinding库
  • 5、MVVM+repository+Android Manager架构模式
  • 6、RxJava2及其对架构的帮忙
  • 7、Dagger 2.11,什么是依赖注入?为什么要应用它?
  • 8、Retrofit + RxJava2 实现网络申请
  • 9、RooM + RxJava2 实现贮存
咱们的APP最终是什么样子?

咱们的APP是一个非常简单的应用程序,它涵盖了下面提到的所有技术。只有一个简略的性能:从Github 获取googlesamples用户下的所有仓库,将数据贮存到本地数据库,而后在界面展现它。

我将尝试解释更多的代码,你也能够看看你Github上的代码提交。

Github:https://github.com/mladenrako...

让咱们开始吧。

0、Android Studio

首先安卓Android Studio 3 beta 1(注:当初最新版为Android Studio 4.0),Android Studio 曾经反对Kotlin,去到Create Android Project界面,你将在此处看到新的内容:带有标签的复选框include Kotlin support。默认状况下选中。按两次下一步,而后抉择EmptyActivity,而后实现了。 祝贺!你用Kotlin开发了第一个Android app)

1、Kotlin

在方才新建的我的项目中,你能够看到一个MainActivity.kt:

package me.mladenrakonjac.modernandroidappimport android.support.v7.app.AppCompatActivityimport android.os.Bundleclass 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特定代码。

2、构建变体

通常,在开发中,如果你有两套环境,最常见的是测试环境和生产环境。这些环境在服务器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,可让您混合不同的产品风味,因而您能够混合demominApi23风味。在咱们的应用程序中,咱们将仅应用“默认” 的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_namestring资源,因而,咱们才不会产生资源抵触,而后点击Sync Now,如果转到屏幕左侧的“构建变体”,则能够看到4个不同的构建变体,其中每个都有两种构建类型:“Debug”和“Release”,切换到demoProduction构建变体并运行它。而后切换到另一个并运行它。您就能够看到两个名称不同的应用程序。

3、ConstraintLayout

如果你关上activity_main.xml ,你能够看到跟布局是ConstraintLayout,如果你开发过iOS应用程序,你可能晓得AutoLayoutConstraintLayout和它十分的类似,他们甚至用了雷同的 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文本略微向上挪动,则在TextTab中将减少上面这行代码:

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...

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 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 好用的起因。

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 = 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 = 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 = repository    binding.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: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进阶路线、面试领导和简历模板送给你