拥抱Kotlin,才能拥有未来

155 阅读6分钟

Kotlin-a modern programming language that makes developers happier. 。。。聊一聊。。。。

Google的决心与改变-Android first

image.png Android 自 08 年诞生之后的多年间 SDK 变化一直不大,开发方式较为固定。13 年起技术更新逐渐加速,特别是 17 年之后, 随着 Kotlin 及 Jetpack 等新技术的出现 Android 开发方式发生了很大变化,Jetpack Compose 更是将这种变化推向了新阶段。旧有的低效的开发方式逐步将被替换。

image.png

快速入门。。。。。。

image.png

。。。。。。

Kotlin语言和JVM虚拟机

image.png

Kotlin的全栈语言之路

image.png

跨平台 Kotlin Multiplatform Mobile

image.png

变量、函数、类、与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 协程

协程它仅仅是个线程框架。在过去几年间,协程这个概念发展势头迅猛,现已经被诸多主流编程语言采用,比如 JavascriptC#PythonRuby 以及 Go 等。Kotlin 的协程是基于来自其他语言的既定概念

image.png

image.png

  • Kotlin Coroutines 生态 image.png 在 Android 平台上,协程主要用来解决两个问题:

  • 处理耗时任务 (Long running tasks) ,这种任务常常会阻塞住主线程;

    • suspend 用于暂停执行当前协程,并保存所有局部变量
    • resume 用于让已挂起的协程从挂起处继续执行

暂时切走,完成后再切回来

  • 保证主线程安全 (Main-safety)  ,即确保安全地从主线程调用任何 suspend 函数
    • Dispatchers.Main - 使用此调度程序可在 Android 主线程上运行协程。此调度程序只能用于与界面交互和执行快速工作。示例包括调用 suspend 函数,运行 Android 界面框架操作,以及更新 LiveData 对象。
    • Dispatchers.IO - 此调度程序经过了专门优化,适合在主线程之外执行磁盘或网络 I/O。示例包括使用 Room 组件、从文件中读取数据或向文件中写入数据,以及运行任何网络操作。
    • Dispatchers.Default - 此调度程序经过了专门优化,适合在主线程之外执行占用大量 CPU 资源的工作。用例示例包括对列表排序和解析 JSON。 一句话概括 同步的方式写异步的代码,且不阻塞主线程
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
}

20201011110523927.gif

当主线程上的所有协程都挂起时,主线程就可以自由地执行其他的任务。

协程会在主线程上运行,suspend并不意味着后台运行。

协程的使用

  • 会创建、会切线程、懂四种调度模式,基本就ok了,就能满足基本开发要求
  • Coroutine 启动
    • launch 可启动新协程而不将结果返回给调用方。
    • async会启动一个新的协程,并允许您使用一个名为 await 的挂起函数返回结果。
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 的属性: isActiveisCancelledisCompleted

  • Deferred:通过使用async创建协程可以得到一个有返回值DeferredDeferred 接口继承自 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通过操作符控制

image.png

flow.map { ... } // Will be executed in IO
   .flowOn(Dispatchers.IO) // This one takes precedence
   .flowOn(Dispatchers.Default)
  • 热流Channel 一个并发安全的队列,它可以用来连接协程,实现不同协程之间的通信 image.png

Android KTX

  • coroutines + Jetpack + mvvm

image.png

 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