【译】使用Kotlin从零开始写一个现代Android 项目-Part1

前言

常常在medium.com上看到一些高质量的技术帖子,可是因为国内的上网环境或者有的同窗对于看英文比较排斥,错过了很多好文章。所以,西哥决定弄一个《优质译文专栏》,花一些时间翻译一些优质技术文给你们。这篇文章是一个小系列,用Kotlin开发现代Android APP,总共四篇,后面的会陆续翻译!如下是正文。html

如今,真的很难找到一个涵盖全部Android新技术的项目,所以我决定本身来写一个,在本文中,咱们将用到以下技术:java

  • 0 、Android Studio
  • 一、Kotlin 语言
  • 二、构建变体
  • 三、ConstraintLayout
  • 四、DataBinding库
  • 五、MVVM+repository+Android Manager架构模式
  • 六、RxJava2及其对架构的帮助
  • 七、Dagger 2.11,什么是依赖注入?为何要使用它?
  • 八、Retrofit + RxJava2 实现网络请求
  • 九、RooM + RxJava2 实现储存
咱们的APP最终是什么样子?

咱们的APP是一个很是简单的应用程序,它涵盖了上面提到的全部技术。只有一个简单的功能:从Github 获取googlesamples用户下的全部仓库,将数据储存到本地数据库,而后在界面展现它。android

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

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

让咱们开始吧。面试

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)算法

一、Kotlin

在刚才新建的项目中,你能够看到一个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文件api

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,可以让您混合不一样的产品风味,所以您能够混合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构建变体并运行它。而后切换到另外一个并运行它。您就能够看到两个名称不一样的应用程序。

三、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 = 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进阶路线、面试指导和简历模板送给你

相关文章
相关标签/搜索