共计 15473 个字符,预计需要花费 39 分钟才能阅读完成。
前言
常常在 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.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 特定代码。
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
,可让您混合不同的产品风味,因而您能够混合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
构建变体并运行它。而后切换到另一个并运行它。您就能够看到两个名称不同的应用程序。
3、ConstraintLayout
如果你关上 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…
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 进阶路线、面试领导和简历模板送给你