推荐 最新
德州安卓

Kotlin Flow? 真香!

前言由于项目是使用的是MVVM架构,使用DataStore等组件时会发现返回的是Flow喂!我LiveData都还没有用熟呢,咋就开始用Flow了,既然官方这样推广,当然是道理所在,那本章就说一说Flow。为了看到第一手准确资料,我是硬着头皮看完了Google官方英语介绍的视频,虽然很多都没有听懂,不过Google的大佬做的图还是很容易理解,本章很多图都是截取视频。背景在日常开发中,尤其是APP端经常有同时发起多个请求然后进行展示内容的需求,比如我先拿到标题(因子)先展示出来,再根据因子去加载数据,拿到数据后再进行分类、处理后再进行展示,这种复杂的业务,如果使用Java原来的回调方式,那就会一层套一层,还要不断地切换线程那当然不用,前面文章也说了使用Kotlin协程可以方便地切换线程以及使用阻塞式地方式写出非阻塞式的代码,那这个问题解决了。还有一个问题就是数据处理问题,这时你当然会说LiveData也有数据处理的API,比如map等等,说的很有道理,确实也是这个道理,那为什么Google官方又要整这么一出呢?等我后面细细说来。LiveData有什么不足?什么是LiveData,它就是一个基于观察者模式的可观察的数据存储器类,同时和普通的观察者又不太一样,它结合了Android生命周期,能感知Android界面的生命周期变化,这种能力可以确保LiveData仅更新处于活跃生命周期状态的应用组件观察者。所以,LiveData是一个简单易用的组件,同时也就是因为它的简单易用,和之前同是观察者模式的老大哥RxJava比功能就欠缺了不少,所以处理比较复杂的场景会比较麻烦。由于LiveData设计比较简单,所以有以下不足:LiveData只能在主线程更新数据。LiveData操作符不够多,处理复杂数据时不够用。虽然我也是刚用Jetpack,我咋记得可以子线程中更新数据呢,不少有个postValue方法吗?是的,但是postValue方法也需要切换到主线程,再进行更新数据,所以在处理复杂业务时,我从子线程获取的数据,我就想在子线程中先展示一下,然后接着再处理再请求,不想老是切换线程,这时就可以使用Flow了。注:由于本人没有真正了解、使用过RxJava,所以关于Flow和RxJava的比较就不细说了,主要也就是RxJava入门门槛高,在Android中需要自己处理生命周期,Flow也支持线程切换、有协程Scope兜底不会出现性能浪费等。Flow简介因为关于Flow的文章挺少,我就直接看了官方的视频介绍,先不谈什么API和原理,先按照官方文档来普及一波先。问题现在有3个耗时任务,分别是A、B、C,在协程中进行,那么使用不同的返回值来接收3个任务的结果会是什么区别呢?使用List如图所示,调用foo时会切换线程,很显然这里的缺点就是必须要等3个任务都执行完,然后把结果放入到一个List中,返回给调用者,如果某个任务执行很久,则需要等待。示例代码:runBlocking{ val list = foo() for (x in list) Log.i(TAG, "initData: $x") }suspend fun foo(): List<String> = buildList { Log.i(TAG, "foo: 开始发送数据") delay(1000) add("A") delay(1000) add("B") delay(1000) add("C") Log.i(TAG, "foo: 结束发送数据") }再看一下打印结果:`2021-08-25 14:34:45.307 15141-15141/: foo: 开始发送数据2021-08-25 14:34:48.309 15141-15141/: foo: 结束发送数据2021-08-25 14:34:48.311 15141-15141/: initData: A2021-08-25 14:34:48.312 15141-15141/: initData: B2021-08-25 14:34:48.312 15141-15141/: initData: C`这里耗时任务执行了3S多,然后数据全部返回,调用者再进行处理。使用Channel接着看一下Channel,什么是Channel的具体定义以及使用我之前也没有使用过,不过从代码注释可以知道它实际就是一个队列,而且是并发安全的,可以用来连接协程,实现不同协程的通信,还是看官方的截图:从这个图更可以看出Channel就是一个队列,不过这个队列相当于管道一样,一边生产数据,一边消费数据,最主要是它还可以在跨协程工作。那同样写个小例子://先定义一个变量,也就是这个Channel val channel = Channel<String>()//开启协程,不断地receive也就是消费管道里地数据 lifecycleScope.launch(Dispatchers.IO) { while (true){ Log.i(TAG, "initData: receive: ${channel.receive()}") } }//开启协程发送数据 lifecycleScope.launch { foo() }//模拟耗时操作 suspend fun foo(){ Log.i(TAG, "foo: 开始发送数据") delay(1000) channel.send("A") delay(1000) channel.send("B") delay(1000) channel.send("C") Log.i(TAG, "foo: 结束发送数据") }结果可想而知是什么样子的:`2021-08-25 14:58:42.236 16024-16024/: foo: 开始发送数据2021-08-25 14:58:43.240 16024-16062/: initData: receive: A2021-08-25 14:58:44.247 16024-16061/: initData: receive: B2021-08-25 14:58:45.252 16024-16024/: foo: 结束发送数据2021-08-25 14:58:45.254 16024-16062/: initData: receive: C`可以看出在第一个耗时任务结束发送时,消费者已经开始工作了,不用等待所有任务都结束。使用Flow关于Flow可以叫做流,这个和管道Channel设计的很像,也是生产者、消费者模型,一边生产数据,一边消耗数据。不过官方视频里说Flow是冷流,在有订阅对象时才开始产生数据也就是emit事件,所以在没有collect之前,Flow内部没有任何协程被激活,不会造成资源泄漏。直接看一下官方截图:示例代码://返回一个Flow实例 suspend fun foo(): Flow<String> = flow { Log.i(TAG, "foo: 开始发送数据") delay(1000) emit("A") delay(1000) emit("B") delay(1000) emit("C") Log.i(TAG, "foo: 结束发送数据") }//进行collect lifecycleScope.launch(Dispatchers.IO) { val flow = foo() flow.collect { Log.i(TAG, "initData: $it") } } //发送数据 lifecycleScope.launch { foo() }可以预见,这个结果肯定和使用Channel是一样的,打印:2021-08-25 15:15:08.507 16419-16458/: foo: 开始发送数据2021-08-25 15:15:09.515 16419-16457/: initData: A2021-08-25 15:15:10.522 16419-16458/: initData: B2021-08-25 15:15:11.529 16419-16457/: initData: C2021-08-25 15:15:11.529 16419-16457/: foo: 结束发送数据到这里会发现和使用Channel一模一样的,区别就是Flow这里先拿到了Flow引用,只有去collect的时候,上游才去emit数据。为了加深印象和区别,总结一下:Flow介绍1.Flow是序列形式的,这个是区分List的,下面这个图可以很容易的看出区别。2.Flow是冷流,什么是冷流?Flow默认是冷流,如果要使用热流,可以使用SharedFlow,关于2者的区别,可以看下面图:(1)冷流冷流可以保证不必要的内存浪费,因为只有去collect时才会触发发射端的协程代码运行,如果有2个Collector,那另一个Collector也只有在collect时才会触发发射端协程运行,且会都跑一遍,如图:看一下代码://foo是冷流Flow var foo: Flow<String>? = null//在init函数里 开启线程,去emit数据 lifecycleScope.launch { foo() } //开启2个协程 都拿到foo,然后进行collect lifecycleScope.launch(Dispatchers.IO) { foo?.collect { Log.i(TAG, "initData: A开始收集 $it") } } //这里的协程,先延迟了2s 再去进行collect lifecycleScope.launch { delay(2000) foo?.collect { Log.i(TAG, "initData: B开始收集 $it") } }foo()函数如下://这里会每隔1s发送一个数据 suspend fun foo(){ foo = flow { Log.i(TAG, "foo: 开始发送数据") delay(1000) Log.i(TAG, "foo: 开始发送A") emit("A") Log.i(TAG, "foo: 结束发送A") delay(1000) Log.i(TAG, "foo: 开始发送B") emit("B") Log.i(TAG, "foo: 结束发送B") delay(1000) Log.i(TAG, "foo: 开始发送C") emit("C") Log.i(TAG, "foo: 结束发送C") Log.i(TAG, "foo: 结束发送数据") } }然后看一下打印:2021-08-30 11:49:13.862 29955-29992/com.wayeal.yunapp I/zyh: foo: 开始发送数据 2021-08-30 11:49:14.868 29955-29992/com.wayeal.yunapp I/zyh: foo: 开始发送A 2021-08-30 11:49:14.870 29955-29992/com.wayeal.yunapp I/zyh: initData: A开始收集 A 2021-08-30 11:49:14.870 29955-29992/com.wayeal.yunapp I/zyh: foo: 结束发送A 2021-08-30 11:49:15.868 29955-29955/com.wayeal.yunapp I/zyh: foo: 开始发送数据 2021-08-30 11:49:15.874 29955-29992/com.wayeal.yunapp I/zyh: foo: 开始发送B 2021-08-30 11:49:15.875 29955-29992/com.wayeal.yunapp I/zyh: initData: A开始收集 B 2021-08-30 11:49:15.875 29955-29992/com.wayeal.yunapp I/zyh: foo: 结束发送B 2021-08-30 11:49:16.870 29955-29955/com.wayeal.yunapp I/zyh: foo: 开始发送A 2021-08-30 11:49:16.871 29955-29955/com.wayeal.yunapp I/zyh: initData: B开始收集 A 2021-08-30 11:49:16.871 29955-29955/com.wayeal.yunapp I/zyh: foo: 结束发送A 2021-08-30 11:49:16.877 29955-29992/com.wayeal.yunapp I/zyh: foo: 开始发送C 2021-08-30 11:49:16.877 29955-29992/com.wayeal.yunapp I/zyh: initData: A开始收集 C 2021-08-30 11:49:16.877 29955-29992/com.wayeal.yunapp I/zyh: foo: 结束发送C 2021-08-30 11:49:16.877 29955-29992/com.wayeal.yunapp I/zyh: foo: 结束发送数据 2021-08-30 11:49:17.873 29955-29955/com.wayeal.yunapp I/zyh: foo: 开始发送B 2021-08-30 11:49:17.873 29955-29955/com.wayeal.yunapp I/zyh: initData: B开始收集 B 2021-08-30 11:49:17.873 29955-29955/com.wayeal.yunapp I/zyh: foo: 结束发送B 2021-08-30 11:49:18.876 29955-29955/com.wayeal.yunapp I/zyh: foo: 开始发送C 2021-08-30 11:49:18.877 29955-29955/com.wayeal.yunapp I/zyh: initData: B开始收集 C 2021-08-30 11:49:18.877 29955-29955/com.wayeal.yunapp I/zyh: foo: 结束发送C 2021-08-30 11:49:18.877 29955-29955/com.wayeal.yunapp I/zyh: foo: 结束发送数据从上面打印我们不难看出:A收集者开始调用collect时,发射协程开始工作,在13.862秒开始等待2s后,B收集者开始调用collect时,发射协程又开始工作一遍,在15.868,2者相隔2s多虽然A收集者已经开始收集了,B收集者开始时,依然又跑一遍(2)热流热流是一对多的关系,当有多个collector时,这时发射端发射一个数据,每个collector都能接收到,这个很像那个LiveData观察者模型,数据能得到共享,所以也是被称为SharedFlow。同时和冷流只有在collect时才去跑发射端的协程代码不同,热流会在对象创建出时便开始执行。同样也是看一下示例代码:val _events = MutableSharedFlow<String>()//先开启协程,创建出SharedFlow lifecycleScope.launch { foo1() } //立马进行收集 lifecycleScope.launch(Dispatchers.IO) { _events.collect { Log.i(TAG, "initData: A开始收集 $it") } } //延迟2秒再进行收集 lifecycleScope.launch { delay(2000) _events.collect { Log.i(TAG, "initData: B开始收集 $it") } }//一开始就发射A,后面每延迟1s发射一次 suspend fun foo1(){ Log.i(TAG, "foo: 开始发送数据") Log.i(TAG, "foo: 开始发送A") _events.emit("A") Log.i(TAG, "foo: 结束发送A") delay(1000) Log.i(TAG, "foo: 开始发送B") _events.emit("B") Log.i(TAG, "foo: 结束发送B") delay(1000) Log.i(TAG, "foo: 开始发送C") _events.emit("C") Log.i(TAG, "foo: 结束发送C") Log.i(TAG, "foo: 结束发送数据") }打印数据:2021-08-30 14:04:53.404 8383-8383/com.wayeal.yunapp I/zyh: foo: 开始发送数据 2021-08-30 14:04:53.404 8383-8383/com.wayeal.yunapp I/zyh: foo: 开始发送A 2021-08-30 14:04:53.405 8383-8383/com.wayeal.yunapp I/zyh: foo: 结束发送A 2021-08-30 14:04:54.406 8383-8383/com.wayeal.yunapp I/zyh: foo: 开始发送B 2021-08-30 14:04:54.407 8383-8424/com.wayeal.yunapp I/zyh: initData: A开始收集 B 2021-08-30 14:04:54.407 8383-8383/com.wayeal.yunapp I/zyh: foo: 结束发送B 2021-08-30 14:04:55.415 8383-8383/com.wayeal.yunapp I/zyh: foo: 开始发送C 2021-08-30 14:04:55.421 8383-8426/com.wayeal.yunapp I/zyh: initData: A开始收集 C 2021-08-30 14:04:55.423 8383-8383/com.wayeal.yunapp I/zyh: initData: B开始收集 C 2021-08-30 14:04:55.425 8383-8383/com.wayeal.yunapp I/zyh: foo: 结束发送C 2021-08-30 14:04:55.426 8383-8383/com.wayeal.yunapp I/zyh: foo: 结束发送数据从上面打印不难看出:热流在创建时,便开始发射数据由于A收集器在发射器发射完一个数据才开始collect,所以A收集器也收集不到A2s后B收集器开始收集,这时它只能被迫接受C,因为A和B数据都错过了,也不会重新再跑一遍发射代码Flow的详细解析从前面的代码,我们肯定了解了什么是Flow,以及冷流和热流的概念,那现在我们根据来官方的源码,来介绍一下Flow是如何使用和常见API。上面图中可以大概说出Flow的特性和使用了,另外补充几点:1、改善的Flow操作符前面说了Flow就是一个数据流,在中间可以对它进行各种操作,所以Flow的API为了更好的使用链式调用,把一些API进行了改善,比如在每个数据发送时进行延迟、开始进行延迟等等,这里就使用官方的一张图来表示:2、背压在生产者、消费者模型中都避免不了背压,在RxJava中使用了很多策略来应对,不过Flow在设计之处就解决了这个问题,其一是Flow发射和收集数据都是异步的,其二就是在发射或者收集添加延迟,来达到缓解背压的情况:说道背压,这个也是和Channel有很大的区别,Channel的策略是缓冲区,但是Flow自带承压机制,因为是Cold事件源,如果没有消费者,事件源不会主动emit事件。collect方法以及flow的构造方法均为suspend,所以均可以延迟,这样如果某端没有准备好,也可以通过延迟来承压,在上图也可以看出,简而言之就是没做事情处理背压。3、buffer操作符前面的总结图里说了,Flow是顺序队列,也就是开始collect,发射A,处理A,然后发射B,处理B,这里如果收集端有耗时操作,整个耗时就非常长了,如图代码例子://这里消费者,先collect,但是我消费也需要时间,所以延迟1s lifecycleScope.launch(Dispatchers.IO) { val flow = foo() flow.collect { Log.i(TAG, "initData: 消费 $it") delay(1000) } } lifecycleScope.launch { foo() }//为了更好的看出执行顺序,每个发送都加了打印 suspend fun foo(): Flow<String> = flow { Log.i(TAG, "foo: 开始发送数据") delay(1000) Log.i(TAG, "foo: 开始发送A") emit("A") Log.i(TAG, "foo: 结束发送A") delay(1000) Log.i(TAG, "foo: 开始发送B") emit("B") Log.i(TAG, "foo: 结束发送B") delay(1000) Log.i(TAG, "foo: 开始发送C") emit("C") Log.i(TAG, "foo: 结束发送C") Log.i(TAG, "foo: 结束发送数据") }可以看出最后的执行打印:2021-08-25 15:37:44.640 16997-17035/: foo: 开始发送A 2021-08-25 15:37:44.640 16997-17035/: initData: 消费 A 2021-08-25 15:37:45.641 16997-17036/: foo: 结束发送A 2021-08-25 15:37:46.643 16997-17035/: foo: 开始发送B 2021-08-25 15:37:46.643 16997-17035/: initData: 消费 B 2021-08-25 15:37:47.643 16997-17036/: foo: 结束发送B 2021-08-25 15:37:48.645 16997-17035/: foo: 开始发送C 2021-08-25 15:37:48.645 16997-17035/: initData: 消费 C 2021-08-25 15:37:49.648 16997-17036/: foo: 结束发送C 2021-08-25 15:37:49.649 16997-17036/: foo: 结束发送数据可以看出这里的操作一共花了6s,如何进行改善呢,就是使用buffer:SharedFlow的详细解析说完了Flow是冷流,冷流固然很好,但是用的多的还是SharedFlow,也就是热流,也可以叫成共享流。比如在Android的MVVM架构中,需要使用数据驱动来完成,这时就可以替换LiveData为SharedFlow(当然是后面要说的StateFlow),对于一个flow会有多个订阅者,这时就可以进行观察,从而达到多个订阅者都可以根据数据变化而做出变化。还是简单看一下总结图:下面我们来具体分析一波。(1)、构造函数public fun <T> MutableSharedFlow( replay: Int = 0, extraBufferCapacity: Int = 0, onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND ): MutableSharedFlow<T>replay:表示当有新的订阅者collect时,发送几个已经发送过的数据给它,默认是0,即默认新订阅者不会获取到订阅之前的数据。 -extraBufferCapacity:表示减去replay,这个Flow还可以缓存多少个数据,默认是0; -onBufferOverflow:表示缓存策略,即缓冲区满了后,如何处理,默认是挂起。(2)、ShareIn函数前面说了,如果使用SharedFlow最好使用MutableSharedFlow然后使用emit来发射数据,但是经常有需求是返回一个Flow,这时就需要使用ShareIn函数把Flow转成SharedFlow,ShareIn函数是Flow的扩展函数,看一下参数:public fun <T> Flow<T>.shareIn( scope: CoroutineScope, started: SharingStarted, replay: Int = 0 ): SharedFlow<T>scope,这个很好理解,协程范围,表示共享开始时所在的协程作用域范围,之前说过SharedFlow很像LiveData,这里的收集者就是观察者,所以为了控制范围,需要传递一个观察者执行操作的协程范围。replay,这个就是当有一个新的观察者订阅时,需要重新传递给这个新的观察者的数量。zstarted,这个控制共享的开始和结束的策略。这里有3种策略,分别说一下:(3)、ShareIn函数上游flow操作符在前面我们说了一些操作符,这里就可以给应用上,比如我在上游结束、异常等情况onCompletionval flow = loginRepository.getLoginName() flow.onCompletion { cause -> if (cause == null) Log.i(TAG, "completed: ") }.shareIn(viewModelScope, SharingStarted.Eagerly) .collect { userName.value = it }retry可以对一些指定的异常进行处理,比如IO异常时进行重连等。val flow = loginRepository.getLoginName() flow.onCompletion { cause -> if (cause == null) Log.i(TAG, "completed: ") }.retry(5000) { val shallRetry = it is IOException if (shallRetry) delay(1000) shallRetry }.shareIn(viewModelScope, SharingStarted.Eagerly) .collect { userName.value = it }onStart 可以在上游数据开始之前,做一些操作。val flow = loginRepository.getLoginName() flow.onStart { emit("start") } .onCompletion { cause -> if (cause == null) Log.i(TAG, "completed: ") }.retry(5000) { val shallRetry = it is IOException if (shallRetry) delay(1000) shallRetry }.shareIn(viewModelScope, SharingStarted.Eagerly) .collect { userName.value = it }StateFlow的详细解析终于到了喜闻乐见的StateFlow了,官方就是希望用这个来替代LiveData,那么它到底是如何来替代LiveData的呢?(1)、特性StateFlow是SharedFlow的子类,根据前面说的那么它是个热流,可以被多个观察者观察,同时可以设置它消失的scope以及条件。StateFlow只更新最新的数据,也就是它是一个replay为0的SharedFlow。StateFlow里面和LiveData很像,都有个value来保存其值,也可以通过这个属性来获取或者设置它的值。(2)、使用使用MutableStateFlow就和LiveData一样,不过它需要一个默认值。也可以使用stateIn函数把一个Flow转成StateFlow,直接看这个函数:val result = userId.mapLatest { newUserId -> repository.observeItem(newUserId) }.stateIn( scope = viewModelScope, started = WhileSubscribed(5000), initialValue = Result.Loading )从这个最基本的使用,我们可以看出以下信息:它的范围是viewModelScope,当viewModelScope结束时,流会停止。当观察者都被移除后,超过5s,为了不浪费性能,流会停止。会有个默认值,也就是这个initivalValue。(3)、观察从上面来看,确实很像LiveData,不过LiveData有个很重要的功能就是具有生命周期感知性能力,在UI处于活跃状态时才会更新数据,那这个StateFlow在收集时并没有传递lifecycleOwner,那如何达到一样的效果呢?首先是观察的操作执行在协程中,这个协程的范围是lifecycleScope不错,但是是直接launch还是launchWhenStarted呢,看下面这个图:从这个图我们发现如果我们直接使用launch的话,可能会在onStop执行时这时来个数据更新,我的View根本没法更新,所以会造成错误,这里要达到和LiveData一样的效果在界面活跃时进行更新,所以这里启动协程就需要使用launchWhenStarted。这个看起来没啥问题,不过我们前面不是有个WhileSubscribed这个优化项呢,表示没有观察者时5s后停止上游数据发射,那现在这个则无法做到,这时就需要使用repeatOnLifecycle,直接看图:这个repeatOnLifecycle的作用就是当满足特定状态时启动协程,并且在生命周期退出这个状态时停止协程。那这就可以比如我APP退到后台,这时如果超过5S,则不会再订阅,协程终止,这时上游发射逻辑也会停止,提高性能。当APP再切换到前台时,会执行onStart,这时又会有观察者,这时上游逻辑又会开始,所以最佳的写法如下:onCreateView(...) { viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.lifecycle.repeatOnLifecycle(STARTED) { myViewModel.myUiState.collect { ... } } } }总结看完了关于Flow的介绍,我们会发现它不仅有类似RxJava的强大操作符,也可以和JetPack一起使用,在处理复杂数据上会比LiveData方便很多。

0
0
0
浏览量2016
德州安卓

Kotlin项目开发实践二

1.单例实现的三种方式object 先看代码:object Pro { }就这么简单实现了Pro的单例,我们可以反编译成java代码看下具体的实现原理:public final class Pro { @NotNull public static final Pro INSTANCE; private Pro() { } static { Pro var0 = new Pro(); INSTANCE = var0; } }首先Pro类的构造方法声明为private,其次可以看到这就是通过静态代码块实现的单例,利用类静态代码块只会执行一次的特性,属于线程安全且饿汉式的;java中通过Pro.INSTANCE获取这个单例,而kotlin直接通过Pro获取单例lazy 先上代码: class Pro private constructor() { companion object { val INSTANCE by lazy { Pro() } } }主要是利用了伴生对象声明的属性为静态变量,且lazy默认的实现模式是加锁线程安全的,这是个线程安全且懒汉式单例实现,关于lazy想要了解更多可以参考Kotlin开发实践之一双重检查锁 上代码:@Volatile var singleton: Pro? = null fun getInstance(): Pro { if (singleton == null) { synchronized(Pro::class.java) { if (singleton == null) { singleton = Pro() } } } return singleton!! }上面就是java双重检查锁的kotlin实现形式,其中:@Volatile保证代码指令有序性getInstance方法内外层singleton判空保证singleton已经初始化完成了,线程不要额外再去竞争锁getInstance方法内内层singleton判空保证如果之前线程已经初始化singleton完成了,后续的线程不要再重复初始化了可以看到,这是个线程安全且懒汉式方式实现的单例2.typealias给复杂类型取个别名这个typealias关键字主要是用于给类型取个别名,下面介绍下两种使用的场景:函数类型取别名 日常开发中,函数类型应该是使用很普遍的,比如//拼接Int和String类型并返回String类型 val block: ((Int, String) -> String)? = null这个函数类型(Int, String) -> String)写起来很麻烦且可读性很差,这个时候就到了typealias上传的时候了:typealias Concat = (Int, String) -> String val block: Concat? = null将(Int, String) -> String)取别名为Concat,不仅使用起来很方便,还容易看出这个函数类型的使用场景:拼接简化泛型传递 使用ViewModel时,我们可能经常会对接口的返回进行如下封装:class MainViewModel: ViewModel() { val data: MutableLiveData<Response<String>> = MutableLiveData() fun test() { data.value = Response("haha") } data class Response<T>(val data: T? = null) }使用Response对服务器返回进行封装,泛型T表示响应数据可以反序列化成的实体类。可以看到上面,每定义一个MutableLiveData都得在其泛型中声明Response<T>,由于我们这个Response是对所有接口响应的统一封装,是一个确定的类型,而Response<T>中的T才是每次创建MutableLiveData需动态化指定的类型。那Response<T>中的Response可不可以省略呢,这个时候就到了typealias上传的时候了:typealias ExternalLiveData<T> = MutableLiveData<MainViewModel.Response<T>>这样每次再创建MutableLiveData就可以这样写:val data1: ExternalLiveData<String> = MutableLiveData()

0
0
0
浏览量2016
德州安卓

Kotlin开发实践之一

1.正确使用lazy委托val vm: String by lazy { "" }这种使用默认是加锁实现的,并发环境下使用没什么问题, 不过在非并发环境中比如Android主线程中未免会造成性能开销。看下lazy的实现源码:public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> = when (mode) { LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer) LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer) LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer) }lazy懒加载支持三种模式:LazyThreadSafetyMode.SYNCHRONIZED:添加同步锁,使lazy延迟初始化线程安全LazyThreadSafetyMode.PUBLICATION:初始化的lambda表达式可以在同一时间被多次调用,但是只有第一个返回的值作为初始化的值LazyThreadSafetyMode.NONE:非加锁实现,非线程安全所以我们可以对lazy二次封装下,暴露出线程安全的lazy和不安全的lazy,根据具体的使用场景选择对应的方法调用 //线程安全 fun <T> safeLazy(initializer: () -> T): Lazy<T> = lazy(LazyThreadSafetyMode.SYNCHRONIZED, initializer) //非线程安全 fun <T> unSafeLazy(initializer: () -> T): Lazy<T> = lazy(LazyThreadSafetyMode.NONE, initializer)2.View.postDelay()优化使用日常开发中,我们一般会使用View.postDelay()获取view的宽高信息,更可以在onResume中粗略估算界面绘制耗时:mBinding.root.postDelayed({ println("size: ${mBinding.root.measuredHeight}") }, 1000) 但是这个原生的postDelayed写起来特别别扭,因为它的Runnable是放在第一个参数中的,不能完全利用kotlin编写lamdba的优势。我们可以在此基础上定义个View的postDelayed的重载扩展函数,将Runnable放到方法参数的最后一个位置: fun View.postDelayed(delayMillis: Long, runnable: Runnable) { postDelayed(runnable, delayMillis) }然后就可以这样使用:mBinding.root.postDelayed(1000L) { println("size: ${mBinding.root.measuredHeight}") }3.dp与px之间快捷转换平常我们会定义工具类方法实现dp和px之间的转换,这里我们以dp转px举例:fun dp2px(dpValue: Float): Float { return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, MainApp.mApplication.resources.displayMetrics) } //使用 val value = dp2px(10f) 这样使用起来还是不便捷,我们可以利用kotlin属性的getter进一步封装:val Float.dp: Float get() = TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, this, MainApp.mApplication.resources.displayMetrics )然后直接调用10f.dp就完成了10dp到px的转换4.arrayOf、intArrayOf的区别使用fun main() { val test1 = arrayOf(2, 4, 5) val tet2 = intArrayOf(4, 6, 7) }反编译成java代码:public static final void main() { Integer[] var10000 = new Integer[]{2, 4, 5}; int[] var2 = new int[]{4, 6, 7}; }使用arrayOf的数组元素都使用的基本类型的包装类型,使用intArrayOf的数组元素则是使用的基本数据类型,减少了包装类带来的性能开销。其他类型诸如float、byte等都和这个相同,所以大家平常编码构造基本数据类型数组时避免使用arrayOf,应选用对应的intArrayOf、byteArrayOf等等

0
0
0
浏览量2021
德州安卓

这些flow常见API的使用,你一定需要掌握!(一)

collect通知flow执行public suspend inline fun <T> Flow<T>.collect(crossinline action: suspend (value: T) -> Unit): Unit = collect(object : FlowCollector<T> { override suspend fun emit(value: T) = action(value) })flow是冷流,只有调用collect{}方法时才能触发flow代码块的执行。还有一点要注意,collect{}方法是个suspend声明的方法,需要在协程作用域的范围能调用。除此之外,collect{}方法的参数是一个被crossinline修饰的函数类型,旨在加强内联,禁止在该函数类型中直接使用return关键字(return@标签除外)。 fun main() { GlobalScope.launch { flow { emit("haha") }.collect { } } }launchIn()指定协程作用域通知flow执行public fun <T> Flow<T>.launchIn(scope: CoroutineScope): Job = scope.launch { collect() // tail-call }这个方法允许我们直接传入一个协程作用域的参数,不需要直接在外部开启一个协程执行。本质上就是使用我们传入的协程作用域手动开启一个协程代码块调用collect{}通知协程执行。这里看官方的源码有个tail-call的注释,也就是尾调用的意思,猜测这里可能官方会在这里进行了优化,减少了栈中方法调用的层级,降低栈溢出的风险。fun main() { flow { emit("haha") }.launchIn(GlobalScope) } catch{}捕捉异常public fun <T> Flow<T>.catch(action: suspend FlowCollector<T>.(cause: Throwable) -> Unit): Flow<T> = flow { val exception = catchImpl(this) if (exception != null) action(exception) }这个就是用来捕捉异常的,不过注意,只能捕捉catch()之前的异常,下面来个图阐述下:即,只能捕捉第一个红框中的异常,而不能捕捉第二个红框中的异常。merge()合流public fun <T> merge(vararg flows: Flow<T>): Flow<T> = flows.asIterable().merge()最终的实现类如下:请注意,这个合流的每个流可以理解为是并行执行的,而不是后一个流等待前一个流中的flow代码块中的逻辑执行完毕再执行,这样做的目的可以提供合流的每个流的执行效果。测试代码如下:fun main() { GlobalScope.launch { merge(flow { delay(1000) emit(4) }, flow { println("flow2") delay(2000) emit(20) }).collect { println("collect value: $it") } } }输出日志如下:map{}变换发送的数据类型public inline fun <T, R> Flow<T>.map(crossinline transform: suspend (value: T) -> R): Flow<R> = transform { value -> return@transform emit(transform(value)) }这个api没什么可将的,很多的地方比如集合、livedata中都有它的影子,它的作用就是将当前数据类型变换成另一种数据类型(可以相同)。fun main() { GlobalScope.launch { flow { emit(5) }.map { "ha".repeat(it) }.collect { println("collect value: $it") } } }总结本篇文章介绍了flow常见的api,接下来还会有一些列文章用来介绍flow的其他api,感谢阅读。

0
0
0
浏览量2017
德州安卓

写一篇好懂的常见Kotlin集合函数讲解(前)

map{}:实现集合中元素的类型转换举个例子,现在有一个Model元素的集合:List<Model>data class Model(val name: String = "", val id: Int = 0)我们现在想从这个集合中获取到每个Model元素的id属性并组成一个新的集合,通常的做法如下:fun map() { val list = mutableListOf(Model(id = 4), Model(id = 2), Model(id = 9)) val result = mutableListOf<Int>() list.forEach { result.add(it.id) } }麻烦,现在有了map就可以这么写:val result2 = list.map { it.id }当然也可以使用方法引用:val result2 = list.map(Model::id)mapTo(){}:比map多了可以指定原始集合元素类型转换后写入的目标集合之前的map调用了会返回一个新的集合类型,有时候我们想把这个新的集合添加到已有的集合中,比如:fun mapTo(source: MutableList<Int>) { val list = mutableListOf(Model(id = 4), Model(id = 2), Model(id = 9)) //将转换后的集合写入到source中 source.addAll(list.map { it.id }) }这样写起来也挺简单,但是我们还可以有更简单的写法://将转换后的集合写入到source中 list.mapTo(source) { it.id }mapIndexed{}:支持带索引的元素类型转换如果元素类型转换的时候还想知道该元素在集合中的索引,mapIndexed轻松搞定://index代表元素在集合中的索引 list.mapIndexed { index, model -> }filter{}:过滤符合指定条件的元素开发中应该会经常遇到:从某个集合中筛选处符合特定条件的元素并重新组成一个新的集合,可能大家会立马写出下面代码:fun filter() { val list = listOf(3, 4, 6, 5, 2) //从集合中筛选出不小于4的元素 val result = mutableListOf<Int>() list.forEach { if (it >= 4) { result.add(it) } } }这样写起来还是比较繁琐,直接使用filter函数实现:val result2 = list.filter { it >= 4 }该函数还有和map一样类似的函数:filterIndexed{}:带下表索引的过滤函数filterTo(){}: 类似于mapTo,可以将过滤后的元素写入到某个指定的集合中indexOfFirst{}:正向查找集合中满足指定条件的元素的索引下标,不存在返回-1val result4 = list.indexOfFirst { it.id == 5 }该函数还有下面的类似函数:indexOfLast{}: 反向查找集合中满足指定条件的元素的索引下标,不存在返回-1indexOf():正向查找指定的元素在集合中的位置,不存在返回-1lastIndexOf(): 反向查找指定的元素在集合中的位置,不存在返回-1take(n):获取集合中前n个元素当n大于等于集合的长度时,即代表获取整个集合元素;当n小于0,则会抛出异常;还有其他类似的集合函数:first()/first{}:获取集合第一个元素/符合指定条件的第一个元素last()/last{}:获取集合最后一个元素/符合指定条件的最后一个元素takeLast(n): 获取集合中最后的n个元素

0
0
0
浏览量1883
德州安卓

写一篇好懂的常见Kotlin集合函数讲解(中)

any{}: 查找集合只要存在指定条件的元素就返回true我们经常会遇见一种场景:遍历集合,只要某个元素符合指定条件就立刻返回为true:fun any(): Boolean { val list = listOf(3, 4, 6, 5, 2) for (i in list) { if (i == 5) { return true } } return false }这样写太太麻烦了,使用any改写为:return list.any { it == 5 }该函数还有其他相似函数:all{}:查找集合只有集合中所有元素都符合指定条件才返回truefold(){}:带有初始值的叠加器,如果集合为空返回的将是传入的初始值我们实现一个n!(n的阶乘):fun fold(n: Int): Int = (1..n).fold(1) { acc, next -> acc * next }第一次叠加的过程中,acc就是初始值1,next就是集合的第一个元素,计算出的acc * next就是下一次叠加的acc,下一次的next也就是集合的第二个元素,依次类推...其他相似函数:reduce:不带初始值的叠加器,其中第一次叠加的acc就是集合的第一个元素,next就是集合的第二个元素,依次类推...foldIndexed{}/reduceIndexed{}:和上面的区别就只是带了索引joinToString:将集合按照一定格式转化为字符串比如将集合转换成字符串并使用","作为集合元素间的分隔符:fun join() { val list = listOf("aa", "bf", "gd", "et") val result = list.joinToString(",") //输出:aa,bf,gd,et }还可以给集合转换后的字符串分别增加前缀和后缀:list.joinToString(",", "pre", "post") //输出:preaa,bf,gd,etpost集合转换成字符串的过程中,可以对集合的每个元素映射成的字符串内容进行处理:list.joinToString(",") { "$it haha" } //输出:aa haha,bf haha,gd haha,et hahabinarySearch:二分查找指定条件的元素相比较于find系列的操作符,查找的效率更高,时间复杂度为lognlogn(以2为底) ,如果查找不到就返回-1。举个例子,二分查找集合中是否存在某个整数(前提是要集合有序)fun binarySearch() { val list = listOf(3, 5, 7, 9, 33, 66, 88, 99) println(list.binarySearch { when { it == 66 -> 0 it < 66 -> -1 it > 66 -> 1 else -> 0 } }) } //输出:5asReversed:集合倒序val list = listOf(3, 5, 7, 9, 33, 66, 88, 99) println(list.asReversed()) //输出:[99, 88, 66, 33, 9, 7, 5, 3]elementAt(index):根据索引获取集合元素,和get[index]一样,index小于0或者越界抛出异常其他相似集合:elementAtOrElse(){}:根据索引获取元素,当传入的索引小于0或者超过集合长度时,采取传入的函数类型生成结果值elementAtOrNull():根据索引获取元素,当传入的索引小于0或者超过集合长度时,返回为nullslice(IntRange/Iterable<Int>):返回指定索引对应集合元素相比较传统的subList获取的是一段索引连续的元素元素,而slice则是能灵活指定任意数量的具体索引(可以非连续)并返回对应的元素集合:val list = listOf(3, 5, 7, 9, 33, 66, 88, 99) println(list.slice(listOf(3, 5, 6))) //输出:[9, 66, 88]

0
0
0
浏览量1838
德州安卓

你需要懂的Kotlin开发技巧之四

1.@JvmName修改方法名直接看例子: @JvmName("testCopy") fun test(name: String, age: Int) { }直接反编译成java代码看下:最终生成的方法名称就是testCopy而不是test2.@get:JvmName、@set:JvmName修改属性名@get: JvmName("getSource") @set: JvmName("setSource") var mData: String = ""直接反编译成java代码看下:对于val属性则只能使用@get: JvmName3.String判空使用isNullOrEmpty,避免TextUtils.isEmpty()我们分别对比下这两种写法:可以看到,使用isNullOrEmpty判空的String,在调用name.length不会报错,使用TextUtils.isEmpty()的则会报错,需要强制声明name!!不为空`为什么第一种不会报错呢,看下isNullOrEmpty的源码:@kotlin.internal.InlineOnly public inline fun CharSequence?.isNullOrEmpty(): Boolean { contract { returns(false) implies (this@isNullOrEmpty != null) } return this == null || this.length == 0 }最核心的就是isNullOrEmpty方法体中有个contract,这个会帮助编译器告知String是否为空,以至于当调用name.length时编译器能推断出String不是空的,就不需要程序强制声明name!!非空4.忽略大小写比较equals一般的大小写比较如下:val res = "aa".toLowerCase(Locale.ROOT) == "AA".toLowerCase(Locale.ROOT)反编译成java代码看下:可以看到,"aa".toLowerCase(Locale.ROOT)赋值给一个局部变量var6,"AA".toLowerCase(Locale.ROOT)赋值给另一个局部变量var7,然后再进行比较。这种方式的比较会额外创建两个局部String变量,所以建议使用equals替换,其中第二个参数可以指定忽略大小写比较val res = "aa".equals("AA", ignoreCase = true)反编译:可以看到,实现非常的简单,不会创建额外的局部变量5.运算符重载get、setgetclass Pro { operator fun get(content: String): String { return content.repeat(10) } }然后就可以这样使用:println(Pro()["blue"])setoperator fun set(key: String, value: String) { }set运算符重载至少需要传入两个参数,使用如下:Pro()["key"] = "value"这两个运算符使用场景非常多,比如针对于Android中SharedPreference读写封装,具体详情可以参考文章:三.委托与SharedPreference的结合还有很多其他的运算符重载函数,比如plus对应"+"、contains对应"in"等等,都是日常开发中比较常用的,大家可以根据具体场景灵活运用

0
0
0
浏览量1982
德州安卓

Kotlin invoke约定,让Kotlin代码更简洁

前言最近看到DSL这个东西,不由的觉得里面可以利用Kotlin的一些特性能简化代码,所以具体来看看它是如何实现的。正文首先一上来就说原理或许对于不熟悉Kotlin的来说会感觉有点突兀,所以我准备从头梳理一下。约定Kotlin的约定我们在平时开发中肯定用到过,不过我们没有仔细去注意这个名词而已。约定的概念就是:使用与常规方法调用语法不同的、更简洁的符号,调用着有着特殊命名的函数。这里提取2个关键点,一个是更简洁的符号调用,一个是特殊命名的函数。说白了就是让函数调用更加简洁。比如我们最熟悉的集和调用 [index] 来 替代 get(index),我们自己也来定义个类,来实现一下这个约定:data class TestBean(val name: String,val age: Int){ //定义非常简单 使用operator重载运算符get方法 operator fun get(index : Int): Any{ return when(index) { 0 -> name 1 -> age else -> name } } }然后我们在使用时://这里就可以使用 [] 来替换 get来简化调用方法了 val testBean = TestBean("zyh",20) testBean.get(0) testBean[0]invoke约定和上面的get约定一样,[] 就是调用 get 方法的更简洁的方式,这里有个invoke约定,它的作用就是让对象像函数一样调用方法,下面直接来个例子: data class TestBean(val name: String,val age: Int){ //重载定义invoke方法 operator fun invoke() : String{ return "$name - $age" } }定义完上面代码后,我们来进行使用:val testBean = TestBean("zyh",20) //正常调用 testBean.invoke() //约定后的简化调用 testBean()这里会发现testBean对象可以调用invoke方法是正常调用,但是也可以testBean()直接来调用invoke方法,这就是invoke约定的作用,让调用invoke方法更简单。invoke约定和函数式类型既然了解了invoke约定,我们来和lambda结合起来。对于lambda有点疑惑的可以查看文章:# Kotlin lambda,有你想了解的一切我们知道函数类型其实就是实现了FunctionN接口的类,然后当函数类型是函数类型时,这时传递给它一个lambda,lambda就会被编译成FunctionN的匿名内部类(当然是非内联的),然后调用lambda就变成了一次FunctionN接口的invoke调用。还是看个例子代码://定义代码 class TestInvoke { //高阶函数类型变量 private var mSingleListener: ((Int) -> Unit)? = null //设置变量 public fun setSingleListener(listener:((Int) -> Unit)?){ this.mSingleListener = listener } // fun testRun() { //调用invoke函数 mSingleListener?.invoke(100) //使用invoke约定,省去invoke if (mSingleListener != null){ mSingleListener!!(100) } } }定义完上面回调变量后,我们来使用这个回调,由于我们知道高阶函数其实是实现了FunctionN接口的类,也就是实现了://注意,这里接口的方法就是invoke public interface Function1<in P1, out R> : Function<R> { /** Invokes the function with the specified argument. */ public operator fun invoke(p1: P1): R }那我也就可以直接使用下面代码来传递参数:val function1 = object: Function1<Int,Unit> { override fun invoke(p1: Int) { Logger.d("$p1") } } testInvoke.setSingleListener(function1)这里看起来合情合理,因为在testRun函数中我们调用了invoke函数,把100当做参数,然后这个100会被回调到function1中,但是我们传递lambda时呢:val testInvoke = TestInvoke() testInvoke.setSingleListener { returnInt -> Logger.d("$returnInt") }上面代码传递lambda和传递一个类的实例效果是一样的,只不过这里只是一段代码块,没有显示的调用invoke啥的,所以这就是一个特性,当lambda被用作参数被函数调用时,也就可以看成是一次invoke的自动调用。invoke在DSL中的实践:Gradle依赖这里我们为什么要说这个invoke依赖呢,很大的原因就是它在一些DSL中有很好的用法,这里我们就来看个Gradle依赖的使用。我们很常见下面代码:dependencies { implementation 'androidx.core:core-ktx:1.6.0' implementation 'androidx.appcompat:appcompat:1.3.1' //... }这里我们都很习以为常,感觉这里很像配置项,而不像是代码,其实这个也是一段代码,只不过是这种风格。那这种风格如何实现呢,我们来简单实现一下:class DependencyHandler{ //编译库 fun compile(libString: String){ Logger.d("add $libString") } //定义invoke方法 operator fun invoke(body: DependencyHandler.() -> Unit){ body() } }上面代码写完后,我们便可以有下面3种调用方式:val dependency = DependencyHandler() //调用invoke dependency.invoke { compile("androidx.core:core-ktx:1.6.0") } //直接调用 dependency.compile("androidx.core:core-ktx:1.6.0") //带接受者lambda方式 dependency{ compile("androidx.core:core-ktx:1.6.0") }由此可见,上面代码第三种方式便是我们在Gradle配置文件中常见的一种,这里其实就2个关键点,一个是定义invoke函数,一个是定义带接受者的lambda,调用时省去this即可。总结其实关于invoke约定和带接受者lambda的写法现在越来越流行了,比如之前的anko库,现在的compose库都是这种声明式的写法,看完原理后,就会发现其实还是很方便的。后续开始研究compose的时候,再来补充一波。

0
0
0
浏览量1752
德州安卓

Kotlin泛型,有你想了解的一切

前言在Java中就有泛型的概念,但是一直没有做个统一的梳理,里面的很多知识点也补了解其细节,所以本章内容就准备好好梳理一下。为了更好的理解,我直接准备从最简单的泛型使用到泛型擦除、Kotlin中的实化类型参数、协变、逆变、点变型等等都介绍一遍。正文话不多说,直接开整。泛型基本概念我们平时总是泛型,那泛型是什么呢?泛型就是可以定义带类型参数的类型,当这种类型的实例被创建出来时,类型形参会被替换为类型实参的具体类型。所以这里重点是类型参数,关于参数我们很熟悉,比如方法的参数在定义方法时就是形参,调用时就是传递的实参给方法,那类型参数就是我们平时在类或者方法中经常看见的T,这个T就是类型形参。//这里E就是类型形参 public interface MutableList<E> : List<E>//这里的String就类型实参 val list: MutableList<String> = mutableListOf()这里就和我们平时调用方法一样需要传递参数,只不过这里传递的参数是类型而已。Kotlin要求类型实参被显示说明这里就会和Java不一样的地方了,既然我创建实例需要传参,按理说这个类型实参是必须要传递的,但是Java的泛型是1.5之后才有的,所以可以在创建泛型类实例时不进行传递实参。//这里Java代码中,tempList的类型就是List List tempList = new ArrayList();不过这种写法在Kotlin中是不允许的,因为Kotlin的创建之初就有泛型的概念,要不显示的指明泛型参数的类型,要不通过自动推导。//已经指明类型参数就是String val list: MutableList<String> = mutableListOf() //能自动推导出类型参数是String val list1 = mutableListOf("a","b")但是下面代码是无法通过IDE编译://这里无法知道类型参数,会直接报错 val errorList = listOf()对于Kotlin这样必须提供类型参数的实参,也是极大的能减少平时代码错误。声明泛型函数看一下如何声明泛型函数,在我们的集合类中,有很多泛型函数,我们来看一个。//这里的 <T> 就是定义了一个类型形参 //接收者和返回者都用了这个形参 public fun <T> List<T>.slice(indices: IntRange): List<T> { if (indices.isEmpty()) return listOf() return this.subList(indices.start, indices.endInclusive + 1).toList() }这里没啥说的,就是通过在fun关键字后和方法名前定义类型形参即可。那如何使用呢,把一个类型实参传递给这个函数,在之前的mutableList()函数其实我们就知道了,直接放在函数名后即可://类型自动推导,List的类型实参是String val list1 = mutableListOf("a","b") //显示的给slice函数传递一个类型实参 val newList = list1.slice<String>(0 .. 1)所以泛型函数可以是在接口或者类的函数,顶层函数,扩展函数,没有什么限制。声明泛型属性泛型属性就和泛型函数有点区别,它只能定义为扩展属性,不能定义为非扩展属性。//定义一个扩展属性last val <T> List<T>.last : T get() = this.last()比如上面代码给List定义了一个扩展属性last,但是你不能定义非扩展属性://类中定义这个 编译不过 val <E> e: E上面代码肯定无法编译,因为不能在一个类的属性中存储多个不同类型的值。声明泛型类声明泛型类也非常简单,和接口一样,把需要定义的类型参数通过<>放在类名后面即可,//直接在类名后面加上<>定义类型形参 public interface List<out E> : Collection<E> { //方法内部可以使用这个形参 public operator fun get(index: Int): E这里没啥可说的。类型参数约束类型参数约束可以约束泛型类或者泛型方法的类型实参的类型。这里也非常容易理解,也就是约束这个类型参数的范围,在Java中使用extends来表达,在Kotlin中直接使用冒号,<T:Number>就说明这个类型参数必须是Number或者Number的子类型。//定义类型参数约束条件,上界为Number fun <T : Number> List<T>.sum(): Number{ //... }//可以编译 val list0 = arrayListOf(1,2,3,4,5,6,7,8) list0.sum() //无法编译 val list1 = arrayListOf("a","b") list1.sum()这里也可以添加多个上界约束条件,比如同时定义类型T有2个上界A、B,那传入的类型实参必须是A、B的子类型。让类型形参非空前面说了可以添加类型参数的约束,如果不加约束,那类型T的默认上界是Any?,注意这里是可空的。//这里没有约束,T的上界默认是Any? class TestKT<T> { fun progress(t: T){ //可以为空 t?.hashCode() } }//所以调用的时候可以传入null val stringNull = TestKT<String?>() stringNull.progress(null)由于Java是没有可空类型的概念,所以这里我想让类型非空,给指定一个上界 Any 即可。//这里类型参数就不能传入可空类型实参 class TestKT<T : Any> { fun progress(t: T){ t.hashCode() } }类型擦除Java/Kotlin的泛型可以说是伪泛型,因为在运行时这个类型参数会被擦除,具体为什么要这么设计,很大原因是占用内存会更少。这里就要明白基础类型的概念,比如 List< Int > 这种类型它的基础类型就是List,当代码运行时,只知道它是List,这里有个很经典的例子,我们来看一下://定义一个保存Int类型的List val listInt = arrayListOf(1,2,3) //定义一个保存String类型的List val listString = arrayListOf("hello","java") //在运行时,他们的class是相同的,都是List if (listInt.javaClass == listString.javaClass){ Logger.d("类型相同") } //通过反射可以往保存Int类型的List中添加String listInt.javaClass.getMethod("add",Object::class.java).invoke(listInt,"aa") //打印结果发现居然还加成功了,没有报错 Logger.d("listInt size = ${listInt.size}")由上面这几行经典的代码我们能明白一个道理,就是泛型类实例在运行时是不会携带类型实参的,在运行时对于List< Int >和List< String >都是List,不知道它应该是保存何种类型的List。类型检查伴随了类型擦除这个特性,泛型类的类型就有了一些约束,比如类型检查。Kotlin中的类型检查是通过 is 函数,但是当类型参数被擦除时,你无法直接使用以下代码://定义一个ArrayList<String> val listInt = arrayListOf(1,2,3) //判断是否是ArrayList<String> 这代码无法编译 if (listInt is ArrayList<String>){ }上面代码是无法编译的,会直接报错,因为在运行时根本携带不了类型实参,这里你只能判断它是不是List,如下代码:val listInt = arrayListOf(1,2,3) //可以判断这个变量是不是一个HashSet if (listInt is HashSet<*>){ }除了 is 函数受到限制外,还有就是as函数。类型转换同样伴随着类型擦除,类型转换 as 函数也受到限制,直接看个代码://接收Collection fun printSum(c: Collection<*>){ //这里当类型转换不成功时会主动抛出异常 val intList = c as? List<Int> ?: throw IllegalArgumentException("期望是list") Logger.d("${intList.sum()}") }定义完函数,我们来进行调用:val intSet = hashSetOf(1,2,3) printSum(intSet)这个代码会直接抛出异常,提示期望是list。那我们就给他传递个list呢,但不是Int类型:val stringList = arrayListOf("a","b") printSum(stringList)这行代码执行会抛出不一样的错误:java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number其实想一下泛型的类型擦除就完全明白了,第一个调用肯定会失败,因为即使是擦除泛型,基类类型也不对。第二个调用代码能编译过,但是会有个警告:因为编译器知道这个泛型参数会被擦除,这个操作是危险操作,所以后面还是会报出类型转换错误。上面2个函数也是由编译器就直接在你编码阶段会提醒你的危险操作,防止出现由于泛型类型擦除导致的程序异常。Kotlin泛型上面介绍了泛型的基本概念,下面来介绍一些关于Kotlin泛型的知识。声明带实化类型参数的函数不同于Java,Kotlin可以实化类型参数,啥意思呢 也就是这个类型参数在运行时不会被擦除。比如下面代码是编译不过的:fun <T> isAny(value:Any): Boolean{ //由于类型擦除,在执行时这个T的值是不携带的 return value is T }但是Kotlin可以让上面的代码编译且执行成功,如下://函数声明为inline函数,且泛型参数使用reified来修饰 inline fun <reified T> isAny(value:Any): Boolean{ //能执行成功 return value is T }上面能保留类型实参的关键不仅仅是把类型实参使用reified修饰,也必须在内联函数中,因为编译器把实现内联函数的字节码插入到每一次调用发生的地方,所以每次调用带实化类型参数的函数时,编译器都知道调用中作为类型实参的确切类型,会直接把类型实参的值替换到字节码中,所以不会被擦除。关于普通的内联函数,Java是可以调用的,但是无法进行内联。对于带reified类型参数的内联函数,Java是不能调用的,因为要实现实化功能,必须要内联,且做一些处理,Java编译器是无法内联的。实化类型参数代替类引用关于实化类型参数还有一个场景就是替代类引用,当一个函数参数接接收java.lang.Class类型时,可以用实化类型参数来优化,这个是啥意思呢 举个简单的例子,比如我们都是知道startActivity需要Intent,而Intent知道跳转类的class类型:public Intent(Context packageContext, Class<?> cls) { mComponent = new ComponentName(packageContext, cls); }这里的Class就是类型的意思,既然我可以直接类型实化,那就可以改写一下://直接类型实化 inline fun <reified T: Activity> Context.StartActivity(){ //类型实化,T就可以代表具体类型 val intent = Intent(this, T::class.java) startActivity(intent) }调用也方便一点:StartActivity<AboutUsActivity>()实化类型参数的限制既然Kotlin的实化类型参数很好用,但是它也有诸多限制,我们简单总结一下,可以做的操作:用在类型检查和类型转换中 is和as使用Kotlin反射API获取相应的java.lang.Class作为调用其他函数的类型实参 不可以的操作有:不能创建指定为类型参数的类的实例调用类型参数类的伴生对象的方法在非内联函数中使用变型继续来聊泛型,这里引入了一个叫做变型的概念,什么是变型呢 这里要了解清楚,首先从字面意思来理解是变异/变化的泛型参数,用来描述拥有相同基础类型和不同类型实参的类型之间是如何关联的。这里比较绕,就比如List< T >的2个类型List< String >与List< Any >之间是如何关联的,这2个类型实参String是Any的子类型,那List< String >与List< Any >是什么关系呢,变型就是用来描述这个关系的。为什么存在变型既然知道了变型的概念,那这个在什么地方能用到这个变型呢 就是给参数传递实参的时候,可以直接看个例子://函数参数期望是List<Any>类型 fun printContent(list: List<Any>){ Logger.d(list.joinToString()) }定义完上面函数,我直接传入一个String类型集和会如何://这里能正常运行 val strings = arrayListOf("a","bc") printContent(strings)这里我们知道String是Any的子类型,所以这里期望是List< Any >,但是我传入的是List< String >没有问题也可以正常运行,那就说明这种关系一定是正常的吗 这可不一定,再看一个例子://传递进来一个可变list,然后加一个元素 fun addContent(list: MutableList<Any>){ list.add(42) }同样定义完上面函数,把string类型集和传递:会发现编译器直接报错,说期望的类型不一致,说明这时MutableList< String >和MutableList< Any >和上面的List< T >有很大区别,这就是为什么要讨论变型的原因。其实上面2段代码我们都是把一个String类型实参的列表传递给期望是Any类型实参的列表,但是当函数中需要添加或者替换列表中的值就是不安全的,当仅仅是获取列表的值就是安全的,当然上面这个结论是我们很熟悉List和MutableList接口可以得出结论,所以我们要把这个问题推广到任何泛型类,而不仅仅是List,这就是为什么需要存在变型的原因。类、类型、子类型在继续说变型前,我们先理一下上面3个东西。在我们平时使用中总是认为类就是类型,这当然是不对的。对于非泛型类来说,比如 Int 是类,但是有2个类型,分别是 Int 和 Int? 类型,其中 Int? 类型不仅仅可以保存Int类的数据还可以保存null。对于泛型类来说,一个类就对应着无数个类型了,比如List是类,它有List< Int >、List< String >等等无数个类型。子类型定义非常重要,具体定义是:在任何时候如果需要类型A的值,都能够使用类型B的值来当做类型A的值,这样类型B就是类型A的子类型。比如我定义一个Any 类型的变量a,这时用一个String 类型的值来当做a,这当然是可以的,这里就会有个问题,String是Any的子类,那类型也是这种关系吗对于非泛型类来说,子类和子类型还真是差不多的东西,但是Kotlin有可空类型的存在,比如Int类型是Int?类型的子类型,但是它俩是一个类。对于泛型类,我们前面就举过例子当我需要List< Any >的时候我们可以传递List< String >给它,所以List< String >就是List< Any >的子类型,但是当期望是MutableList< Any >的时候却不能使用MutableList< String >类型值来给它,说明这俩不是子类型关系。总结一下术语:对于List这种类来说,A是B的子类型,List< A >是List< B >的子类型,就说这个类是协变的;对于MutableList这种类来说,对于任意2种类型A和B,MutableList< A >即不是MutableList< B >的子类型也不是超类型,它就被称为在该类型参数上是不变型的。协变:保留子类型化关系理解协变这种泛型参数的变型,必须要理解上面说的子类型化的概念,这样才好理解为什么放在in和out位置。假如我有一个泛型类,就叫做List< T >,这时String类型是Any类型的子类型,那么List< String >就是List< Any >的子类型,这个类就是在T这个类型参数上是协变的,也就需要加个out来修饰T。最常见的协变泛型类就是Kotlin中的List,代码如下:public interface List<out E> : Collection<E> { override val size: Int override fun isEmpty(): Boolean }不过这个例子大家可能都看烦了,其实List也是最好的一个协变例子,我们自己来写个类试试://定义动物类 open class Animal{ fun feed(){ Logger.d("zyh animal feed") } }//定义畜群,假设一个Herd就包含10只动物 class Herd<T : Animal>{ //上界是Animal,所以底层实现也就是保存的是Animal类型 val animal = Animal() fun getVa():T{ return animal as T } //一个畜群有10个动物 val size = 10 }//给畜群投喂食物 fun feedAll(animals: Herd<Animal>){ for (i in 0 until animals.size){ //遍历取出Animal,然后投喂食物 animals.getVa().feed() } }//定义猫,继承至动物 class Cat : Animal(){ //需要清理 fun clean(){ Logger.d("zyh cat clean") } }上面函数看起来都没啥问题,我主要就是有个10只动物的畜群类,想给它们投喂食物,然后我现在想给猫群投喂食物://方法形参是猫群 fun feedCatHerd(cats: Herd<Cat>){ for (i in 0 until cats.size){ cats.getVa().clean() } //猫群属于畜群,所以我一次性投喂 feedAll(cats) }这里看起来合情合理,不过代码却无法编译:这里提示类型不匹配,所以这里就有问题了,我们再来梳理一下。首先Herd是一个类,Cat类型是Animal类型的子类型,然后Herd泛型类变型是啥呢 就是Herd< Cat >和Animal< Animal >这2个类,我们也想Herd< Cat >是Herd< Animal >的子类型,这样我就可以处理上面的问题了,所以这种变型的关系就叫做协变,只需要在Herd的泛型参数前加个out,就说明该类在该参数是协变的。//类型参数T 就是协变的 class Herd<out T : Animal>{ val animal = Animal() fun getVa():T{ return animal as T } val size = 10 }in 位置 和 out 位置上面我们熟悉了协变,就是给类型参数加上out关键字,那能不能把所有类型参数都定义为out呢 当然不可以,定义为out的类型参数在泛型类中只能被用在out位置,其实这里的out位置也就是返回值的位置,或者叫做生产者的位置,也就是生成类型为 T 的值。//比如这里的例子 interface Transformer<T>{ //方法参数的位置就是 in位置 fun transform(t: T): T //方法返回值的位置就是 out位置 }所以在类型参数T上加上out关键字有2层含义:子类型化会被保留T 只能用在out位置所以当类型参数定义为了out,这个类型参数在泛型类中就不能出现在in位置,编译器会报错。逆变:反转子类型化关系我们清楚了协变的概念,就很容易理解逆变,逆变就是协变的反子类型化。直接定义:一个在类型参数上逆变的类是这样一个泛型类(我们以Consumer< T >为例),对于这种类来说,如果B是A的子类型,那么Consumer< A >就是Consumer< B >的子类型。关于逆变,我们也举个例子://这是迭代集和的排序函数,这里要传递进来一个Comparator,它的类型是in T public fun <T> Iterable<T>.sortedWith(comparator: Comparator<in T>): List<T> { if (this is Collection) { if (size <= 1) return this.toList() @Suppress("UNCHECKED_CAST") return (toTypedArray<Any?>() as Array<T>).apply { sortWith(comparator) }.asList() } return toMutableList().apply { sortWith(comparator) } }然后我们有以下代码://泛型实参为Any val anyComparator = Comparator<Any> { o1, o2 -> o1.hashCode() - o2.hashCode() } //泛型实参为Number val numberComparator = Comparator<Number> { o1, o2 -> o1.toInt() - o2.toInt() } //泛型实参为Int val intComparator = Comparator<Int> { o1, o2 -> o1.toInt() - o2.toInt() } //创建了Number类型的集和,按照接口,它期望的参数是Comparator<Number> val numbers: List<Number> = arrayListOf(1,11,111) //可以编译 numbers.sortedWith(numberComparator) //可以编译 numbers.sortedWith(anyComparator) //不可以编译 numbers.sortedWith(intComparator)上面我们举了个例子,其中Number是Int的超类型,是Any的子类型,但是在传递参数却无法传递Comparator< Int >,其实我们可以想想这是为什么因为标记为in的逆变类型参数,是需要入参的,也就是需要使用这个类的实例,比如这里期望是Number,我调用Number中的方法,但是来了一个它的子类型Int,假如Int有个getInt的方法,这时在泛型类中类型参数还是Number,它是没有getInt方法的,所以不允许。但是来了一个它的超类型Any,Any中的操作我Number都可以操作,所以是安全的。到这里,我们把协变和逆变都说完了,其实不用死记硬背,要理解库设计的思想和原理,这个就很容易理解。点变型: 在类型出现的地方指定变型在开始我们就说了,变型是描述泛型类类型之间的关系,根据是否保留子类型化可以分为协变、逆变和不变。除了这些,我们还要掌握点变型的概念。其实上面说的协变和逆变我们都是在定义泛型类或者泛型函数时使用,这样还是不太方便,比如MutableList这个类的泛型参数是不变的,但是有一些需求我们想改变一下这个定义,直接还是看个例子。我们想编写一个复制 MutableList的函数,直接如下代码://参数都是mutableList fun <T> copyData(source: MutableList<T> , target: MutableList<T>){ for (item in source) { target.add(item) } }然后我们在使用时:val strings = arrayListOf("zyh","wy") var targets: ArrayList<String> = ArrayList() //都是String类型,当然可以 copyData(strings,targets)但是仔细一想这不太符合逻辑,我可以定义一个String类型的集和,把它复制到Any类型的集和,这个逻辑肯定是符合逻辑的:不出意外,这里肯定不行,因为MutableList< String >不是Mutable< Any >的子类型,按照之前的做法我们只需要把source的泛型类给改成协变类,那不免有些麻烦,我们换个方式,用下面代码://直接定义2个类型参数,来限制这2个类的关系 fun <T : R,R> copyData1(source: MutableList<T> , target: MutableList<R>){ for (item in source) { target.add(item) } }然后我们在使用时:val strings = arrayListOf("zyh","wy") var targets: ArrayList<Any> = ArrayList() copyData1(strings,targets)这样不会有任何问题,不过这太麻烦了,Kotlin和Java都有更优雅的处理方式,也就是点变型,啥是点变型呢 就比如这里我想在souce这个参数类型上它是协变的,这个在Java很常见,因为Java的所有泛型在定义时都是不变的,只有在使用时指定其变型,我们来看一下Java代码://这里使用<? extent T>就说明可以使用T的子类型 public static <T> void copyDataJava(ArrayList<? extends T> source , ArrayList<T> destination){ for (int i = 0; i < source.size(); i++) { destination.add(source.get(i)); } } private void testJava(){ ArrayList<String> strings = new ArrayList<>(); strings.add("zyh"); strings.add("wy"); //这里虽然类型是Object,但是不会报错 ArrayList<Object> objects = new ArrayList<>(); copyDataJava(strings,objects); }这里代码如果用Kotlin的话就更简单了://加了一个out变型修饰符 fun <T> copyData2(source: MutableList<out T> , target: MutableList<T>){ for (item in source) { target.add(item) } }直接指定source的类型类是协变的,这样就可以传递其子类型了。这样一看就能看出来点变型是非常重要了,不仅仅可以在方法参数,也可以在局部变量、返回类型等都可以指定其点变型。投影看到点变型如此方便,我就有了个疑惑,MutableList这个类在定义的时候是不变型的啊,在使用时又给它设置为了协变,那是不是MutableList< T >在T上就是协变了呢 当然不是,要是这样那岂不是乱套了。比如代码:fun <T> copyData2(source: MutableList<out T> , target: MutableList<T>){ for (item in source) { target.add(item) } }这里的source其实不是一个常规的MutableList,它是受到限制的,这个叫做类型投影,即它不是真正的类型,只是一个投影。其实投影还挺好理解的,它就是受限制的类型,不是真正的类型,因为它的有些功能是受限的,是假类型。比如上面代码,标记为了协变,那MutableList种在in位置使用的方法将无法调用,这里add方法在MutableList中是泛型参数在in的位置,所以这里直接无法使用,功能受限,也验证了为什么叫做投影。这个其实也非常好理解,一旦使用点变型让一个类型协变,将无法使用类型参数在in位置的方法,然后这个新的"假"类型就叫做投影。星号投影在理解星号投影时,还要回顾一下啥是投影,前面说了投影是受限的类型,比如在in和out都可以用的类型参数,在点变形为out时,生成的投影类型将不能使用实参在in位置的方法。星号投影也是一种投影,它的限制是相当于out,即只能使用类型参数在out位置的方法。直接理解完定义后,我们需要思考为什么要有这个星号投影,当类型参数不重要时可以使用星号投影,啥叫做不重要呢 就是当不调用带有类型参数的方法或者只读取数据而不关心其具体类型时。比如我想判断一个集和是不是ArrayList时:val strings = arrayListOf("zyh","wy") //这里只能用星号投影 if (strings is ArrayList<*>){ Logger.d("ArrayList") }因为泛型在运行时会被擦除,所以类型参数并不重要,所以这里使用星号投影。星号投影和Any?关于星号投影还容易理解出错,比如MutableList< * >和MutableList< Any? >这2个是不一样的东西,其中MutableList< Any? >这种列表可以包含任何类型,但是MutableList< * >是包含某种类型的列表,这个类型你不知道而已,只有当被赋值时才知道。直接看个例子:在被赋值前,不知道unknowns的类型,但是赋值后,可以通过get获取它的值,但是add方法无法调用,这里受到了限制,是不是有点熟悉,这个限制就是 out 点变型的协变。所以这里星号投影被转换成了 < out Any? >,只能调用其类型参数在out位置的方法,不能调用类型参数在in位置的方法。总结泛型的知识还是很多的,这一篇文章也可能说不全,下面做个简单的总结:Java/Kotlin的泛型都是伪泛型,在使用时可以用在函数、扩展属性和类上,同时泛型参数在运行时会被擦除。由于泛型参数在运行时会被擦除,所以对类型检查和类型转换函数在运行时可能会不起效果,这时IDE会给出警告。Kotlin通过内联函数和reified关键字可以实化类型参数,即在运行时还可以保留该类型参数,常常可以用于替代类引用。加强认知一点就是类、类型和子类型的关系,对于非泛型类来说,类和类型差不多;对于泛型类来说,一个类,有多类型;而任何时候要求A类型参数时可以用B类型实例代替,B就是A的子类型。变型就是主要讨论相同基础类型的不同类型的关系,比如Int是Number的子类型,那List< Int >类型是List< Number >的子类型,这就是协变; 同理还有逆变和不变。

0
0
0
浏览量1826
走你啊啊啊啊啊

android 使用 recyclerview 渲染服务端图片列表不显示?

"Recyclerview" 的 "layout" 布局文件如下: 适配器代码如下: public class ImageRecyclerViewAdapter extends RecyclerView.Adapter { private final Activity activity; private final List images; public ImageRecyclerViewAdapter(Activity activity, List images) { this.activity = activity; this.images = images; } @NonNull @NotNull @Override public ImageViewHolder onCreateViewHolder(@NonNull @NotNull ViewGroup parent, int viewType) { LayoutInflater inflater = LayoutInflater.from(parent.getContext()); // 注意第三个参数;仅返回视图,不附加视图到parent View view = inflater.inflate(R.layout.recyclerview_item_image, parent, false); return new ImageViewHolder(view); } @Override public void onBindViewHolder(@NonNull @NotNull ImageViewHolder holder, int position) { String src = this.images.get(position); ImageView imageView = holder.getImageView(); // 填充src Glide.with(this.activity.getBaseContext()) .load(src) .into(imageView); } @Override public int getItemCount() { return this.images.size(); } } 通过上述代码渲染视图图片不显示!如果给图片布局设置一个高度又可以正常显示,这是为什么?

12
1
0
浏览量270