译:使用kotlin来开发一个现代Android项目(一)
Android
21423 人阅读
|
1 人回复
|
2020-09-14
|
市面上很难找到一个能够覆盖所有Android新事物的项目,所以我要自己写一个,它使用一下几项技术:
- Android Studio
- Kotlin
- 构建变种版本
- 约束布局
- 数据绑定
- MVVM+repository+Android Manager架构模式
- RxJava
- Dagger依赖注入
- Retrofit+RxJava2实现网络请求
- RooM+RxJava实现存储
在以上几项技术中,Android studio并不算稀奇,因为目前绝大多数Android开发者都在使用Android Studio来做开发工具,所以还是要学习下面9种技术。其中,kotlin语言我已经接触过了,它的写法更加简单,另外可以非常简单的解决异步线程的问题;构建变种版本就是为同一个应用创建不同的版本;约束布局,一种新的布局,用法上类似相对布局;数据绑定,让实现MVVM非常的简单,剩下的RxJava等技术就不做概念上的介绍了,这个定义起来实在是太难了。
0、Android Studio
下载安装Android Studio的过程就直接省略掉了,直接创建一个新的项目,进入到select a Project Template,选择Empty Activity,点击next,名字和包名自己写一下,Language 需要选择Kotlin,finish。
接下面我们只需要静静的等待Android Studio为我们构建完成,然后我们就可以在实体机或者虚拟机内运行这个app。当然,如果有报错的话可以看看右下方有没有提示信息,比如要你安装kotlin 1.4,安装即可。
1、Kotlin
我们可以看到这个MainActivity的后缀是kt,这说明它是一个kotlin文件,MainActivity : AppCompatActivity() 表示这个MainActivity继承自AppCompatActivity。
另外,在kotlin种,所有的方法都需要有fun关键字,并且不可以使用@override注解,如果需要复写方法的话可以直接使用override关键字。
savedInstanceState: Bundle? 这个?代表什么呢,它代表了这个Bundle可以为空,即NULL。kotlin是一门null安全语言。
举个例子:在编译器中写入这段代码之后,会得到一个编译错误,因为a的值必须被初始化,就像下面这样:- var a : String = "Init value"
复制代码 如果把null赋值给a的话,也会得到一个编译错误:想要使a的值可以为空,可以这样写:通过这样的写法,来帮助我们避免空指针,例如我们有一个可以为空并且已经为空的nameTextView,需要设置它的值,那么我们使用下面的代码赋值将会产生空指针异常:- nameTextView.setText("Hello World")
复制代码 但是如果使用Kotlin,它将不允许我们这样做,我们必须要使用?或者!!,如果我们使用?
- nameTextView?.setText("Hello World")
复制代码 只有当nameTextView不为空时,才会执行赋值操作,这也就避免了空指针异常。不过就和所有的其他事物一样,当我们想要强制使用的时候,同样可以通过!!来使用,例如:- nameTextView!!.setText("Hello World")
复制代码 这时,就很容易发生空指针异常了。
2. 构建变体
一般在开发过程中,你有不同的环境,最常见的就是测试环境和生产环境,这些环境的URL地址、图标、名字、API等都有可能不同,通常在每个项目开始的时候,都会有一下几点:
- finalProduction:上传到Google play商店
- demoProduction: 这是一个带有新功能的版本,但是Google商店中还没有更新,用户可以安装这个版本,然后可以测试新功能和提供反馈。
- demoTesting:和demoProduction一样,不过它的地址为测试地址。
- mock:对开发和设计都很有用,可以提供假数据,这也就可以在api没有准备好的时候开始开发。一旦API准备好之后,我们就可以把开发转移到demoTesting环境中。
在应用中,我们将会拥有这些变体。他们将提供不同的包名和名称。gradle 3.0.0中有一个新的api:flavorDimension,它允许你混合不同的产品,所以你可以混合demo和minApi23。在我们的app中,我们将仅仅使用默认的flavorDimension,去生成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"
- }
- }
复制代码 打开strings.xml并删除app_name,这也才不会发生资源冲突,然后点击Sync Now(右上角大象),在屏幕的左下角,可以点击构建变体,然后就可以看到4个不同的构建变体,其中每个都有两种构建类型:Debug和Release,切换到demoProduction构建变体并运行它,然后再切换到另外一个并运行,就可以看到两个名称不同的应用程序。
构建变体
3. 约束布局
如果你打开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" />
- </android.support.constraint.ConstraintLayout>
复制代码 Constraints帮助我们去描述view之间的关系,对于每一个view来说, 你应该有4个约束,一边一个。在这种情况下,我们的视图被父布局的每一边所约束。
如果你在设计选项卡中向上移动一点Hello World这个TextView,在文本选项卡中将出现一行新的代码:
- app:layout_constraintVertical_bias="0.28"
复制代码
设计选项卡和文本选项卡时同步的(就是Code、Split、Design那些),修改时会同步修改,垂直偏差描述了视图对其约束的垂直趋势,如果希望视图垂直居中,则应使用:
- app:layout_constraintVertical_bias="0.5"
复制代码 这个代码原文应该写错了,因为要居中,所以值应该是0.5。
要获得这也的布局,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="com.android.newkotlin.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困惑,它仅仅是帮助我们更好的预览布局。
我们可以注意到我们的布局都是扁平的,没有嵌套布局。你应该尽可能的减少使用嵌套布局因为这会影响性能。另外,约束布局可以在不同大小的屏幕下工作的很好。
4. 数据绑定 Data binding library当我听到数据绑定之后的第一件事就是问自己:Butterknife已经很好了,加上我使用一个插件帮助我去从xml中获取视图。我为什么要改变去使用Data binding library呢?当我对Data binding有了更多的了解之后,我对它的感觉就像是第一次见到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:4.0.0-beta1"
- }
复制代码 注意,Data Binding 编译器的版本与项目 build.gradle 文件中的 gradle 版本相同:- classpath 'com.android.tools.build:gradle:4.0.0-beta1'
复制代码 点击 Sync Now,转到 activity _ main.xml class,用 Layout 标签包装 Constraint 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">
- <androidx.constraintlayout.widget.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="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" />
- </androidx.constraintlayout.widget.ConstraintLayout>
- </layout>
复制代码 注意,您必须将所有 xmlns 移动到 layout 标记。然后按下构建图标或使用快捷键 Cmd + F9。我们需要构建项目,以便数据绑定库能够生成类 ActivityMainBinding,我们将在 MainActivity 类中使用该类。
如果不执行项目的 Build,您将看不到 ActivityMainBinding 类,因为它是在编译时生成的。我们仍然没有完成绑定,我们只是说我们有一个非 null 变量,它是 ActivityMainBinding 类型。还有,你注意到我没有把?在 ActivityMainBinding 的末尾,我没有初始化它。这怎么可能?Lateinit 修饰符允许我们有非空变量等待初始化。与 ButterKnife 类似,绑定初始化应该在 onCreate 方法中完成,一旦布局就绪。此外,您不应该在 onCreate 方法中声明绑定,因为您可能会在 onCreate 方法范围之外使用它。我们的绑定不应该是 null,这就是为什么我们使用 lateinit。使用 lateinit 修饰符,我们不必每次访问绑定变量时都使用 null 检查。
让我们初始化绑定变量,你应该替换:
- //setContentView(R.layout.activity_main)
- var binding = DataBindingUtil.setContentView<ActivityMainBinding>(this,R.layout.activity_main)
复制代码 就是这样!您成功地绑定了您的视图。现在您可以访问它并应用一些更改。例如,让我们将存储库名称改为“ Modern Android Medium Article” :
- binding.repositoryName.text = "Modern Android Medium Article"
复制代码 正如您所看到的,我们可以通过绑定变量访问来自 activity _ main.xml 的所有视图(当然是具有 id 的视图)。这就是为什么数据绑定优于 ButterKnife 的原因。
Data binding library
也许你已经注意到我们没有像在java中一样使用setText ()方法。我想在这里停下来解释一下 getters 和 setters 在 Kotlin 与 Java 相比是如何工作的。
首先,您应该知道我们为什么使用 setters 和 getter。我们使用它来隐藏类的变量,只允许通过方法访问,这样我们就可以隐藏类的客户端的类的细节,并禁止同样的客户端直接更改我们的类。假设我们在 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;
- }
-
- }
复制代码 原文实在是不知道怎么翻译,反正就是getter和setter这样写太麻烦了,我们都懂。
而使用Kotlin使我们的开发人员更加的容易,你可以这样写:
这样写的代码并不等于:
- int side = square.getA();
复制代码 因为 Kotlin 自动生成默认的 getter 和 setter。在 Kotlin,只有当你有特殊的 setter 或 getter 时,你才应该指定它。否则,Kotlin 会为你自动生成:
- var a = 1
- set(value) { field = Math.abs(value) }
复制代码 现在这是什么? 为了说清楚,让我们看看这个代码:
- var a = 1
- set(value) { a = Math.abs(value) }
复制代码 这意味着您在 set 方法中调用 set 方法,因为在 Kotlin 世界中不能直接访问该属性。这将产生无限递归。当您调用 a = 某个值时,它会自动调用 set method。我现在希望您清楚为什么必须使用 field 关键字以及 setter 和 getter 是如何工作的。
ps: 这一段不理解的话可以测试一下,不使用field的话会无限递归。
让我们回到我们的代码。我想向你们展示 Kotlin 语言的另一个重要特征:
- 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 允许您对一个实例调用多个方法。
我们仍然没有完成数据绑定,还有更多的伟大的事情。让我们为 Repository 创建 UI 模型类(这是用于 Github Repository 的 UI Model 类,保存应该显示的数据,不要将其与 Repository 模式混淆)。要制作 Kotlin 类,你应该去 New-> 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",
- "Mladen Rakonjac", 1000, true)
复制代码
正如您所注意到的,对象构造没有new的关键字。
现在让我们转到 activity_main.xml 并添加 data tag:
- <data>
- <variable
- name="repository"
- type="com.android.newkotlin.Repository"
- />
- </data>
复制代码 我们可以访问布局中属于 Repository 类型的这个存储库变量。例如,我们可以使用 repository _ name id 在 TextView 中执行以下操作:
- android:text="@{repository.repositoryName}"
复制代码 Repository_name 文本视图将显示从 repository 变量的 repositoryName 属性获得的文本。剩下的唯一工作就是将库变量从 xml 绑定到 MainActivity.kt。按下 Build 来创建数据绑定库来生成所需的类,然后返回到 MainActivity 并添加以下两行:- binding.repository = repository
- binding.executePendingBindings()
复制代码
如果你运行这个应用程序,你会看到文本视图将显示“ Medium Android Repository Article”
但是如果我们这样做:
- Handler().postDelayed({repository.repositoryName="New Name"}, 2000)
复制代码 2秒以后会显示新的文本吗?不,不会的。您必须再次设置存储库。类似这样的东西会起作用:
- Handler().postDelayed({repository.repositoryName="New Name"
- binding.repository = repository
- binding.executePendingBindings()}, 2000)
复制代码
但是,如果每次我们改变一些属性的时候都要这样做的话,那就太无聊了。有一个更好的解决方案叫做属性观察者。首先让我们描述一下什么是观察者模式/java,因为我们在 rxJava 部分也需要它: http://androidweekly.net/
也许,你已经听说了几个 http://androidweekly.net/了。这是一份关于 Android 开发的周刊。如果你想收到它,你必须订阅它给你的电子邮件地址。一段时间后,如果你决定了,你可以停止在他们的网站上的退订选项。
这是观察者/可观察模式的一个例子。在这种情况下,Android 周刊是可观察的,它每周发布新闻通讯。读者是观察者,因为他们订阅它,等待新的信息发出,一旦他们收到信息,他们就读它,如果他们中的一些人认为她不喜欢它,她/他就可以停止听它。
在我们的示例中,属性观察器是 xml 布局,它将侦听 Repository 实例中的更改。所以,仓库是可观察的。例如,一旦在 Repository 类的实例中更改了 Repository name 属性,就应该更新 xml 而不调用:
- binding.repository = repository
- binding.executePendingBindings()
复制代码
如何使用数据绑定库?数据绑定库为我们提供了库类应该实现的 BaseObservable 类:
- 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)
- }
- }
复制代码
BR 是一个在使用可绑定注释时自动生成一次的类。正如您所看到的,在设置新值时,我们将通知它。现在您可以运行应用程序,您将看到存储库名称将在2秒钟后更改,而不再调用 executePendingBindings ()。
这部分就到此为止了。在下一部分,我将写 MVVM 模式和存储库模式,我将写 Android 包装器管理器。
|
|
|
|
|
|
|
LazyGirl
发表于 2020-9-16 10:03:08
|
显示全部楼层
|
|
|
|
|