Kotlin-a modern programming language that makes developers happier. 。。。聊一聊。。。。
Google的决心与改变-Android first
Android 自 08 年诞生之后的多年间 SDK 变化一直不大,开发方式较为固定。13 年起技术更新逐渐加速,特别是 17 年之后, 随着 Kotlin 及 Jetpack 等新技术的出现 Android 开发方式发生了很大变化,Jetpack Compose 更是将这种变化推向了新阶段。旧有的低效的开发方式逐步将被替换。
快速入门。。。。。。
。。。。。。
Kotlin语言和JVM虚拟机
Kotlin的全栈语言之路
跨平台 Kotlin Multiplatform Mobile
变量、函数、类、与Java100%互操作性
kotlin如何解决Java开发的痛点?
//三引号
private static final String HTML = "<!DOCTYPE HTML>\n"
+ "<HTML>\n"
+ "<head>\n"
+ "<meta charset=\"utf-8\">\n"
+ "<title>测试</title>\n"
+ "</head>\n" + "<body>\n"
+ " <h1>测试</h1>\n"
+ " <p>\"测试\"的段落1</p>\n"
+ "</body>\n" + "</HTML>"; }
val HTML = """ <!DOCTYPE html>
<html> <head>
<meta charset="utf-8">
<title>测试</title>
</head> <body> <h1>测试</h1>
<p>"测试"的段落</p>
</body> </html>
"""
//字符串模板
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age + '}';
}
override
fun toString(): String {
return "User{name='$name', age=$age}" }
//默认参数
public class CustomTextView extends AppCompatTextView {
public CustomTextView(Context context) {
super(context);
}
public CustomTextView(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public CustomTextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
}
class KotlinTextView : AppCompatTextView {
constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = android.R.attr.textViewStyle
) : super(context, attrs, defStyleAttr){
}
}
//习惯使用Handler、AsyncTask、rxjava 等类来实现线程切换,用协程是真香
//切换线程
GlobalScope.launch(Dispatchers.IO){ //Dispatchers.Main Dispatchers.Default
//todo
}
//== ===
//智能类型转换
if(obj instanceof Closeable){
((Closeable) obj).close();
}
if (obj is Closeable) {
obj.close()//obj已经是一个Closeable类型了
}
// if、when表达式
//解构申明
for (Map.Entry<String, Integer> entry : map.entrySet()) {
System.out.println(entry.getKey() + "->" + entry.getValue());
}
val map = mapOf("one" to 1, "tow" to 2)
for ((key, value) in map) {//通过解构申明来遍历Map
println("$key->$value")
}
//定义数据类
data class User constructor(var name: String,var age: Int)
fun main(){
var person: User = User("Jone", 20)
var (name, age) = person
Log.d(TAG,"$name $age")
}
- 内置函数
- 回调函数的Kotin的lambda的简化
- 理解参数it、this、返回值的不同
//java
mView.setEventListener(new EventListener(){
public void onSuccess(Data data){
//todo
}
});
//KT接口单一方法,仅有一个参数省略(),最后一个参数是函数可以写在{}
//仅支持在Java中定义接口支持SAM转换
mView.setEventListener{ data ->
//todo
};
- let 作用域函数,定义一个变量在一个特定的作用域范围内
object.let{
it.todo()//在函数体内使用it替代object对象去访问其公有的属性和方法
...
}
//另一种用途 判断object为null的操作
object?.let{//表示object不为null的条件下,才会去执行let函数体
it.todo()
}
mVideoPlayer?.setVideoView(activity.course_video_view)
mVideoPlayer?.setVideoView(activity.course_video_view)
mVideoPlayer?.setVideoView(activity.course_video_view)
//let的优雅
mVideoPlayer?.let {
it.setVideoView(activity.course_video_view)
it.setControllerView(activity.course_video_controller_view)
it.setCurtainView(activity.course_video_curtain_view)
}
- with 可以省略 this,访问传入object的属性跟方法
val result = with(user, {
println("my name is $name, I am $age years old, my phone number is $phoneNum")
1000
})
override fun onBindViewHolder(holder: ViewHolder, position: Int){
val item = getItem(position)?: return
with(item){
holder.tvName.text = name
holder.tvSummary.text = summary
...
}
}
- run函数是let,with的结合体,可以像with函数省略this,直接访问实例的公有属性和方法,又弥补了with函数传入对象判空问题
override fun onBindViewHolder(holder: ViewHolder, position: Int){
getItem(position)?.run{
holder.tvName.text = name
holder.tvSummary.text = summary
...
}
}
- apply 持有this,同run函数,唯一不同的返回值的是它传入的对象本身,所以经常用于对象的初始化赋值,属性多级判空,同also
//返回值
val result = user.apply {
println("my name is $name, I am $age years old, my phone number is $phoneNum") 1000 }
println("result: $result")
//初始化
var song = item.apply{
id = "123"
name = "name"
}
//java
if(response != null){
if(response.getData != null){
if(response.getData.getDataList() != null){
//todo
}
}
}
//kotlin
var dataList = response?.apply{}.data?.apply{}.dataList?.apply{}
- also函数 持有it,同apply函数
- takeif|takeUnless函数,满足条件返回this,否则返回null
- 具名函数与匿名函数
with("with的使用",::show) with("with的使用",{ //this }) fun show(str: String) = println(str)
高级用法
- 延迟初始化-
lateinit, lazy
class User {
private lateinit var name: String
private val password: String by lazy {
println("lazy init") "admin"
}
fun setName(name: String) {
this.name = name
}
fun show() {
println("name = $name")
println("--------------------")
println("第一次访问 password = $password")
println("第二次访问 password = $password")
}
}
fun main() {
val user = User()
user.setName("tomcat")
user.show() }
//输出结果
name = tomcat
--------------------
lazy init
第一次访问 password = admin
第二次访问 password = admin
-
委托 by
- 类委托,类不需要实现接口的方法,属性委托,委托对象重写
set/get方法或则ReadOnlyProperty/ReadWriteProperty接口
- 类委托,类不需要实现接口的方法,属性委托,委托对象重写
-
高阶函数
一个函数可以将另一个函数当作参数。将其他函数用作参数的函数称为“高阶函数”。此模式对组件之间的通信(其方式与在 Java 中使用回调接口相同)很有用。
下面是一个高阶函数的示例:
fun stringMapper(str: String, mapper: (String) -> Int): Int {
// Invoke function
return mapper(str)
}
stringMapper("Android", { input ->
input.length
})
SAM 转换 单一抽象方法转换(只适用于接口,抽象类不适用)
loginButton.setOnClickListener {
val authSuccessful: Boolean = viewModel.authenticate(
usernameEditText.text.toString(),
passwordEditText.text.toString()
)
if (authSuccessful) {
// Navigate to next screen
} else {
statusTextView.text = requireContext().getString(R.string.auth_failed)
}
}
Android KTX
- 利用了 Kotlin 的语言功能,包含在 Android Jetpack以及其他库中的Kotlin扩展程序
implementation("androidx.core:core-ktx:1.7.0")
implementation ("androidx.fragment:fragment-ktx:1.3.4")
implementation("androidx.collection:collection-ktx:1.2.0")
......
Kotlin 协程
协程它仅仅是个线程框架。在过去几年间,协程这个概念发展势头迅猛,现已经被诸多主流编程语言采用,比如 Javascript、C# 、Python、Ruby 以及 Go 等。Kotlin 的协程是基于来自其他语言的既定概念
-
Kotlin Coroutines 生态
在 Android 平台上,协程主要用来解决两个问题:
-
处理耗时任务 (Long running tasks) ,这种任务常常会阻塞住主线程;
suspend用于暂停执行当前协程,并保存所有局部变量resume用于让已挂起的协程从挂起处继续执行
暂时切走,完成后再切回来
- 保证主线程安全 (Main-safety) ,即确保安全地从主线程调用任何 suspend 函数
- Dispatchers.Main - 使用此调度程序可在 Android 主线程上运行协程。此调度程序只能用于与界面交互和执行快速工作。示例包括调用
suspend函数,运行 Android 界面框架操作,以及更新LiveData对象。 - Dispatchers.IO - 此调度程序经过了专门优化,适合在主线程之外执行磁盘或网络 I/O。示例包括使用 Room 组件、从文件中读取数据或向文件中写入数据,以及运行任何网络操作。
- Dispatchers.Default - 此调度程序经过了专门优化,适合在主线程之外执行占用大量 CPU 资源的工作。用例示例包括对列表排序和解析 JSON。 一句话概括 同步的方式写异步的代码,且不阻塞主线程
- Dispatchers.Main - 使用此调度程序可在 Android 主线程上运行协程。此调度程序只能用于与界面交互和执行快速工作。示例包括调用
suspend fun fetchDocs() { // Dispatchers.Main
val result = get("developer.android.com") // Dispatchers.Main
show(result) // Dispatchers.Main
}
suspend fun get(url: String) = // Dispatchers.Main
withContext(Dispatchers.IO) { // Dispatchers.IO (main-safety block)
/* perform network IO here */ // Dispatchers.IO (main-safety block)
} // Dispatchers.Main
}
当主线程上的所有协程都挂起时,主线程就可以自由地执行其他的任务。
协程会在主线程上运行,suspend并不意味着后台运行。
协程的使用
- 会创建、会切线程、懂四种调度模式,基本就ok了,就能满足基本开发要求
- Coroutine 启动
package com.tip.coroutines
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.*
const val TAG: String = "TestCoroutines"
class MainActivity : AppCompatActivity() {
private val mScope = MainScope()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
testLaunch()
testAsync()
val coroutineContext = Job() + Dispatchers.Default + CoroutineName("myContext")
Log.d(TAG,"$coroutineContext,${coroutineContext[CoroutineName]}")
val newCoroutineContext = coroutineContext.minusKey(CoroutineName)
Log.d(TAG,"$newCoroutineContext")
}
//获取返回值和并发
private fun testAsync() {
mScope.launch {
Log.d(TAG, "testAsync() begin " + "ThreadName=" + Thread.currentThread().name)
var job1 = async(Dispatchers.IO) {
delay(1000)
Log.d(TAG, "await() " + "ThreadName=" + Thread.currentThread().name)
"返回结果1"
}
var job2 = async(Dispatchers.IO) {
delay(1000)
Log.d(TAG, "await() " + "ThreadName=" + Thread.currentThread().name)
"返回结果2"
}
var job3 = async(Dispatchers.IO) {
delay(1000)
Log.d(TAG, "await() " + "ThreadName=" + Thread.currentThread().name)
"返回结果3"
}
var result1 = job1.await()
var result2 = job2.await()
var result3 = job3.await()
job1.invokeOnCompletion {
Log.d(TAG, "invokeOnCompletion " + it?.let {
Log.d(TAG,"非空")
})
}
//合并结果
Log.d(TAG, "testAsync() end "+"$result1 $result2 $result3 " + "ThreadName=" + Thread.currentThread().name)
}
}
private fun testLaunch() {
mScope.launch(Dispatchers.IO) {
Log.d(TAG, "testLaunch() begin " + "ThreadName=" + Thread.currentThread().name)
val userInfo = getUserInfo()
withContext(Dispatchers.Main) {
Log.d(TAG, "Thread name=" + Thread.currentThread().name + " " + userInfo)
}
Log.d(TAG, "testLaunch() end " + "ThreadName=" + Thread.currentThread().name)
}
}
private suspend fun getUserInfo(): String {
return withContext(Dispatchers.IO) {
// 延迟1000毫秒 delay是一个挂起函数
// 在这1000毫秒内该协程所处的线程不会阻塞
// 协程将线程的执行权交出去,该线程该干嘛干嘛,到时间后会恢复至此继续向下执行
delay(1000)
Log.d(TAG, "getUserInfo() " + "ThreadName=" + Thread.currentThread().name)
"返回结果"
}
}
override fun onDestroy() {
super.onDestroy()
mScope.cancel()
}
}
Coroutine 深入
-
CoroutineContext 即协程的上下文,是 Kotlin 协程的一个基本结构单元。巧妙的运用协程上下文是至关重要的,以此来实现正确的线程行为、生命周期、异常以及调试。它包含用户定义的一些数据集合,这些数据与协程密切相关。它是一个有索引的
Element实例集合。这个有索引的集合类似于一个介于set和 map之间的数据结构。每个element在这个集合有一个唯一的 Key 。当多个element的 key 的引用相同,则代表属于集合里同一个element。它由如下几项构成:- Job: 控制协程的生命周期;
- CoroutineDispatcher: 向合适的线程分发任务;
- CoroutineName: 协程的名称,调试的时候很有用;
- CoroutineExceptionHandler: 处理未被捕捉的异常
//如何创建CoroutineScope
// 参考MainScope()
// public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
val coroutineContext = Job() + Dispatchers.Default + CoroutineName("myContext")
val scope1 = CoroutineScope(coroutineContext)
val scope2 = MainScope() + CoroutineName("MainActivity")
val coroutineScope = MainScope() + coroutineContext
Log.d(TAG,"$coroutineContext,${coroutineContext[CoroutineName]}")
val newCoroutineContext = coroutineContext.minusKey(CoroutineName)
Log.d(TAG,"$newCoroutineContext")
运行后的结果
2022-05-12 14:58:14.649 11329-11329/com.tip.coroutines D/TestCoroutines: [JobImpl{Active}@e7e6242, CoroutineName(myContext), Dispatchers.Default],CoroutineName(myContext)
2022-05-12 14:58:14.649 11329-11329/com.tip.coroutines D/TestCoroutines: [JobImpl{Active}@e7e6242, Dispatchers.Default]
CoroutineContext接口的定义如下
请注意: CoroutineContext 可以使用 " + " 运算符进行合并。由于 CoroutineContext 是由一组元素组成的,所以加号右侧的元素会覆盖加号左侧的元素,进而组成新创建的 CoroutineContext。比如,(Dispatchers.Main, "name") + (Dispatchers.IO) = (Dispatchers.IO, "name")。
public interface CoroutineContext {
public operator fun <E : Element> get(key: Key<E>): E?
public fun <R> fold(initial: R, operation: (R, Element) -> R): R
public operator fun plus(context: CoroutineContext): CoroutineContext{...}
public fun minusKey(key: Key<*>): CoroutineContext
public interface Key<E : Element>
public interface Element : CoroutineContext {...} }
Element接口的定义如下
public interface Element : CoroutineContext {
public val key: Key<*>
public override operator fun <E : Element> get(key: Key<E>): E? =
if (this.key == key) this as E else null
public override fun <R> fold(initial: R, operation: (R, Element) -> R): R =
operation(initial, this)
public override fun minusKey(key: Key<*>): CoroutineContext =
if (this.key == key) EmptyCoroutineContext else this
}
Job & Deferred - 任务
-
Job:用于处理协程。对于每一个所创建的协程 (通过launch或者async),它会返回一个Job实例(异步的任务),该实例是协程的唯一标识,并且负责管理协程的生命周期,Job可以包含多个子Job,父Job需要等待子Job全部执行完成后才会执行,子Job异常会导致父Job取消,这种默认的行为可以通过SupervisorJob来修改。 -
Job状态:一个任务可以包含一系列状态: 新创建 (New)、活跃 (Active)、完成中 (Completing)、已完成 (Completed)、取消中 (Cancelling) 和已取消 (Cancelled)。虽然我们无法直接访问这些状态,但是我们可以访问Job的属性:isActive、isCancelled和isCompleted -
Deferred:通过使用async创建协程可以得到一个有返回值Deferred,Deferred接口继承自Job接口,额外提供了获取Coroutine返回结果的方法Deferred接口定义
public interface Deferred<out T> : Job {
public val onAwait: SelectClause1<T>
public suspend fun await(): T @ExperimentalCoroutinesApi
public fun getCompleted(): T @ExperimentalCoroutinesApi
public fun getCompletionExceptionOrNull(): Throwable? }
MainScope() 的实现就使用了 SupervisorJob 和一个 Main Dispatcher:
/**
* Creates the main [CoroutineScope] for UI components.
*
* Example of use:
* ```
* class MyAndroidActivity {
* private val scope = MainScope()
*
* override fun onDestroy() {
* super.onDestroy()
* scope.cancel()
* }
* }
* ```
*
* The resulting scope has [SupervisorJob] and [Dispatchers.Main] context elements.
* If you want to append additional elements to the main scope, use [CoroutineScope.plus] operator:
* `val scope = MainScope() + CoroutineName("MyActivity")`.
*/
@Suppress("FunctionName")
public fun MainScope(): CoroutineScope = ContextScope(SupervisorJob() + Dispatchers.Main)
CoroutineDispatcher调度器
Android上的 Kotlin Coroutines
dependencies {
dependencies {
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.32"
// 协程核心库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.3"
// 协程Android支持库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3"
// 协程Java8支持库
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.4.3"
// lifecycle对于协程的扩展封装
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.2.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.2.0" }
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
}
- 轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。**挂起比阻塞节省内存,且支持多个并行操作。
- 内存泄漏更少:使用结构化并发机制在一个作用域内执行多项操作。**
- 内置取消支持:取消操作会自动在运行中的整个协程层次结构内传播。
- Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发。
Kotlin 数据流
简单的异步场景,我们可以直接使用挂起函数、launch、async;至于复杂的异步场景,我们就可以使用 Flow
- 冷流Flow以协程为基础构建,类似于RxJava通过操作符控制
flow.map { ... } // Will be executed in IO
.flowOn(Dispatchers.IO) // This one takes precedence
.flowOn(Dispatchers.Default)
- 热流Channel
一个并发安全的队列,它可以用来连接协程,实现不同协程之间的通信
Android KTX
coroutines + Jetpack + mvvm- Jetpack的视图模型
- 一组可直接与协程配合使用的 KTX 扩展。例如
lifecycle-viewmodel-ktx库
- 一组可直接与协程配合使用的 KTX 扩展。例如
- Jetpack的视图模型
class LoginRepository(...) {
...
suspend fun makeLoginRequest(
jsonBody: String
): Result<LoginResponse> {
// Move the execution of the coroutine to the I/O dispatcher
return withContext(Dispatchers.IO) {
// Blocking network request code
}
}
}
withContext(Dispatchers.IO)调度线程,将协程的执行操作移到IO线程中,确保主线程安全
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun login(username: String, token: String) {
// Create a new coroutine on the UI thread
viewModelScope.launch {
val jsonBody = "{ username: "$username", token: "$token"}"
// Make the network call and suspend execution until it finishes
val result = try {
loginRepository.makeLoginRequest(jsonBody)
} catch(e: Exception) {
Result.Error(Exception("Network request failed"))
}
// Display result of the network request to the user
when (result) {
is Result.Success<LoginResponse> -> // Happy path
else -> // Show error in UI
}
}
}
}
- viewModelScope