写在前面
解读一个新项目时,对一些零散的技术点做了一个简单的梳理,本文总结了项目开发中用到的一些知识点。
1. 取集合的一部分
https://www.kotlincn.net/docs/reference/collection-parts.html
Windowed
以检索给定大小的集合元素中所有可能区间。 获取它们的函数称为 windowed()
:它返回一个元素区间列表,比如通过给定大小的滑动窗口查看集合,则会看到该区间。 与 chunked()
不同,windowed()
返回从每个集合元素开始的元素区间(窗口)。 所有窗口都作为单个 List
的元素返回。
CourseFragment
中的用法
1 | lass Adapter(val scene: String) : RecyclerView.Adapter<RecyclerView.ViewHolder>() { |
2. Kotlin系列之 in 运算符
https://bingjian.blog.csdn.net/article/details/79090877
in
运算符常常用来检查一个值是否在某个区间内。它还有对应的逆运算!in
用来检查某个值不在某一个区间内。
1 | fun recog(c: Char) = when(c){ |
copy函数
当要复制一个对象,只改变一些属性,但其余不变,copy()就是为此而生。
copy()
函数完成的是浅拷贝
浅拷贝只复制对象应用,即指向对象的指针,而不复制对象本身,新旧对象共享同一块内存。
深拷贝会另外创建一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
https://blog.csdn.net/poorkick/article/details/119984976
Kotlin | 实现数据类(data)深拷贝
浅拷贝指的是如果要拷贝A对象,则会重新创建一个B对象,并将其内部变量全部赋值给B对象,所以我们称之为浅拷贝。
深拷贝指的是:拷贝后,如果B对象中存在引用对象,此时更改这个引用对象不会影响到原有A对象中的引用对象,因为它两所操作的内存并不是同一块内存。而浅拷贝则相反,当你操作B对象中的某个引用对象时,就会影响到A对象。对于基本类型,深拷贝与浅拷贝都是直接赋值,并没有什么区别。
————————————————
原文链接:https://blog.csdn.net/petterp/article/details/103859178
3.利用注解改进代码检查
https://developer.android.com/studio/write/annotations?hl=zh-cn
只有在每次使用方法时都应明确检查返回值是否为 null 的情况下,才应对方法的返回值使用 @Nullable
。
@Nullable
注解用于指明可以为 null 的变量、参数或返回值,
而 @NonNull
则用于指明不可以为 null 的变量、参数或返回值。
添加 @StringRes
注解,以检查资源参数是否包含 R.string
引用,如下所示:
1 | abstract fun setTitle(Int) resId: |
其他资源类型的注解(例如 @DrawableRes
、@DimenRes
、@ColorRes
和 @InterpolatorRes
)可以使用相同的注解格式添加,并在代码检查期间运行
如果某个类中的所有方法具有相同的线程要求,您可以为该类添加一个线程注解,以验证该类中的所有方法是否从同一类型的线程调用。
线程注解的一个常见用途是验证 AsyncTask 类中的方法替换,因为此类会执行后台操作,并且仅在界面线程上发布结果。
- 值约束注解
使用 @IntRange
、@FloatRange
和 @Size
注解可以验证所传递参数的值。@IntRange
和 @FloatRange
在应用到用户可能会弄错范围的参数时最为有用。
@IntRange
注解可以验证整型或长整型参数值是否在指定范围内。以下示例可以确保 alpha
参数包含 0 到 255 之间的整数值:
1 | fun setAlpha(Int) alpha: { ... } |
- 权限注解
使用 @RequiresPermission
注解可以验证方法调用方的权限。要检查有效权限列表中是否存在某个权限,请使用 anyOf
属性。
使用 @CheckResult
注解可验证是否实际使用了方法的结果或返回值。不应使用 @CheckResult
为每个非 void 方法添加注解,而应添加注解来阐明可能令人不解的方法的结果。
CallSuper 注解
使用
@CallSuper
注解可验证替换方法是否会调用该方法的超类实现。以下示例为onCreate()
方法添加了注解,以确保所有替换方法实现都会调用super.onCreate()
:1
2
3
override fun onCreate(savedInstanceState: Bundle?) {
}Typedef 注解(进一步了解)
使用
@IntDef
和@StringDef
注解,您可以创建整数集和字符串集的枚举注解来验证其他类型的代码引用。Typedef 注解可以确保特定参数、返回值或字段引用一组特定的常量。这些注解还会启用代码补全功能,以自动提供允许的常量。Typedef 注解使用
@interface
来声明新的枚举注解类型。@IntDef
和@StringDef
注解以及@Retention
可以对新注解添加注解,是定义枚举类型所必需的。@Retention(RetentionPolicy.SOURCE)
注解可告诉编译器不要将枚举注解数据存储在.class
文件中。以下示例展示了创建某个注解的具体步骤,该注解可以确保作为方法参数传递的值引用某个已定义的常量:
1 | import android.support.annotation.IntDef |
4. 后备属性
所谓后备属性,其实是对后备字段
的一个变种,它实际上也是隐含试的对属性值的初始化声明,避免了空指针。
我们根据一个官网的例子,进行说明:
示例1:
1 | private var _table: Map<String, Int>? = null |
上面的代码中我们可以看出:_table
属性是私有的,我们不能直接的访问它。故而提供了一个公有的后备属性(table
)去初始化我们的_table
属性。
通俗的讲,这和在Java中定义Bean属性的方式一样。因为访问私有的属性的getter和setter函数,会被编译器优化成直接反问其实际字段。因此不会引入函数调用开销。
https://www.cnblogs.com/Jetictors/p/9293170.html
https://developer.android.com/codelabs/basic-android-kotlin-training-viewmodel?hl=zh_cn#4
示例2:
使用后备属性,可以从 getter 返回确切对象之外的某些其他内容。
我们已经学过,Kotlin 框架会为每个属性生成 getter 和 setter。
对于 getter 和 setter 方法,您可以替换其中一个方法或同时替换两个方法,并提供您自己的自定义行为。为了实现后备属性,您需要替换 getter 方法以返回只读版本的数据。后备属性示例:
1 | // Declare private mutable variable that can only be modified |
举例而言,在您的应用中,您需要应用数据仅对 ViewModel
可见:
在 ViewModel
类之内:
_count
属性设为private
且可变。因此,只能在ViewModel
类中对其进行访问和修改。惯例是为private
属性添加下划线前缀。
在 ViewModel
类之外:
- Kotlin 中的默认可见性修饰符为
public
,因此count
是公共属性,可从界面控制器等其他类对其进行访问。由于只有get()
方法会被替换,所以此属性不可变且为只读状态。当外部类访问此属性时,它会返回_count
的值且其值无法修改。这可以防止外部类擅自对ViewModel
内的应用数据进行不安全的更改,但允许外部调用方安全地访问该应用数据的值.
1 | private var _currentScrambledWord = "test" |
5. Hilt
https://developer.android.com/codelabs/android-hilt?hl=zh-cn#0
VirxxGo中的用法:
- RepositoryModule
1 |
|
1 | interface CourseRepository { |
- 实现接口,在实现中调用网络请求接口
1 |
|
- 在viewmodel中获取数据
1 |
|
6. 使用ViewTreeLifecycleOwner获取Lifecycle
https://blog.csdn.net/vitaviva/article/details/105006686
ViewTreeLifecycleOwner是Lifecycle KTX中提供的View的一个扩展方法,可以快速地获取一个最近的Fragment或者Activity的LifecycleOwner。
7. repeatOnLifecycle
在 Android 开发中,请使用 LifecycleOwner.addRepeatingJob、suspend Lifecycle.repeatOnLifecycle 或 Flow.flowWithLifecycle 从 UI 层安全地收集数据流。
8.Android 上的 Kotlin 数据流
https://developer.android.google.cn/kotlin/flow
数据流以协程为基础构建,可提供多个值。从概念上来讲,数据流是可通过异步方式进行计算处理的一组数据序列。所发出值的类型必须相同。例如,Flow<Int>
是发出整数值的数据流。
数据流与生成一组序列值的 Iterator
非常相似,但它使用挂起函数通过异步方式生成和使用值。这就是说,例如,数据流可安全地发出网络请求以生成下一个值,而不会阻塞主线程。
创建数据流
flow
构建器函数会创建一个新数据流,您可使用emit
函数手动将新值发送到数据流中。修改数据流
存储库层将使用中间运算符
map
来转换将在View
上显示的数据:1
2
3
4
5
6val favoriteLatestNews: Flow<List<ArticleHeadline>> =
newsRemoteDataSource.latestNews
// Intermediate operation to filter the list of favorite topics
.map { news -> news.filter { userData.isFavoriteTopic(it) } }
// Intermediate operation to save the latest news in the cache
.onEach { news -> saveInCache(news) }从数据流中进行收集
使用终端运算符可触发数据流开始监听值。如需获取数据流中的所有发出值,请使用
collect
由于
collect
是挂起函数,因此需要在协程中执行。它接受 lambda 作为在每个新值上调用的参数。由于它是挂起函数,调用collect
的协程可能会挂起,直到该数据流关闭。继续之前的示例,下面将展示一个简单的
ViewModel
实现,展示其如何使用存储库层中的数据:1
2
3
4
5
6
7
8
9
10
11
12
13class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {
init {
viewModelScope.launch {
// Trigger the flow and consume its elements using collect
newsRepository.favoriteLatestNews.collect { favoriteNews ->
// Update View with the latest favorite news
}
}
}
}
数据流收集可能会由于以下原因而停止:
- 如上例所示,协程收集被取消。此操作也会让底层提供方停止活动。
- 提供方完成发出数据项。在这种情况下,数据流将关闭,调用
collect
的协程则继续执行。
除非使用其他中间运算符指定流,否则数据流始终为冷数据并延迟执行。这意味着,每次在数据流上调用终端运算符时,都会执行提供方代码。在前面的示例中,拥有多个数据流收集器会导致数据源以不同的固定时间间隔多次获取最新资讯。如需在多个使用方同时收集时优化并共享数据流,请使用 shareIn
运算符。
1 | class LatestNewsViewModel( |
提供方的数据实现可来自第三方库。这意味着它可能会引发异常情况。如需处理这些异常,请使用 catch
中间运算符。
默认情况下,flow
构建器的提供方会通过从中收集的协程的 CoroutineContext
执行,并且如前所述,它无法从不同 CoroutineContext
对值执行 emit
操作。在某些情况下,可能不需要此行为。例如,在本主题所用示例中,存储库层不应在 viewModelScope
所使用的 Dispatchers.Main
上执行操作。
如需更改数据流的 CoroutineContext
,请使用中间运算符 flowOn
。flowOn
会更改上游数据流的 CoroutineContext
,这表示会在 flowOn
之前(或之上)应用提供方以及任何中间运算符。下游数据流(晚于 flowOn
的中间运算符和使用方)不会受到影响,并会在 CoroutineContext
上执行以从数据流执行 collect
操作。如果有多个 flowOn
运算符,每个运算符都会更改当前位置的上游数据流。
1 | class NewsRepository( |
借助此代码,onEach
和 map
运算符使用 defaultDispatcher
,其中catch
运算符和使用方在 viewModelScope
所使用的 Dispatchers.Main
上执行。
随着数据源层执行 I/O 操作,您应该使用针对 I/O 操作进行优化的调度程序:
1 | class NewsRemoteDataSource( |
Jetpack 库中的数据流
许多 Jetpack 库已集成数据流,并且在 Android 第三方库中非常受欢迎。数据流非常适合实时数据更新和无限数据流。
您可以使用 Flow with Room 接收有关数据库更改的通知。在使用数据访问对象 (DAO) 时,返回 Flow
类型以获取实时更新。
1 |
|
每当 Example
数据表发生更改时,系统都会发出包含数据库新数据项的新列表。
将基于回调的 API 转换为数据流
callbackFlow
是一个数据流构建器,允许您将基于回调的 API 转换为数据流。例如,Firebase Firestore Android API 会使用回调。如需将这些 API 转换为数据流并监听 Firestore 数据库的更新,您可使用以下代码:
1 | class FirestoreUserEventsDataSource( |
与 flow
构建器不同,callbackFlow
允许通过 send
函数从不同 CoroutineContext
发出值,或者通过 offer
函数在协程外发出值。
在协程内部,callbackFlow
会使用通道,它在概念上与阻塞队列非常相似。通道都有容量配置,限定了可缓冲元素数的上限。在 callbackFlow
中所创建通道的默认容量为 64 个元素。当您尝试向完整通道添加新元素时,send
会将数据提供方挂起,直到新元素有空间为止,而 offer
不会将相关元素添加到通道中,并会立即返回 false
。
9.破解 Kotlin 协程(11) - Flow 篇
https://zhuanlan.zhihu.com/p/114295411
Flow
就是 Kotlin 协程与响应式编程模型结合的产物,你会发现它与 RxJava 非常像,二者之间也有相互转换的 API,使用起来非常方便。
- 冷数据流
一个 Flow 创建出来之后,不消费则不生产,多次消费则多次生产,生产和消费总是相对应的。
所谓冷数据流,就是只有消费时才会生产的数据流,这一点与 Channel
正对应:Channel
的发送端并不依赖于接收端。
末端操作符
前面的例子当中,我们用 collect
消费 Flow 的数据。collect
是最基本的末端操作符,功能与 RxJava 的 subscribe
类似。除了 collect
之外,还有其他常见的末端操作符,大体分为两类:
- 集合类型转换操作,包括
toList
、toSet
等。 - 聚合操作,包括将 Flow 规约到单值的
reduce
、fold
等操作,以及获得单个元素的操作包括single
、singleOrNull
、first
等。
实际上,识别是否为末端操作符,还有一个简单方法,由于 Flow 的消费端一定需要运行在协程当中,因此末端操作符都是挂起函数。
Flow 的取消
想要取消 Flow 只需要取消它所在的协程即可。
1 | job.cancelAndJoin() |
Flow 的背压
只要是响应式编程,就一定会有背压问题,我们先来看看背压究竟是什么。
背压问题在生产者的生产速率高于消费者的处理速率的情况下出现。为了保证数据不丢失,我们也会考虑添加缓存来缓解问题:
- 也可以为
buffer
指定一个容量。不过,如果我们只是单纯地添加缓存,而不是从根本上解决问题就始终会造成数据积压。 - 使用 conflate 解决背压问题
collectLatest
。顾名思义,只处理最新的数据,这看上去似乎与conflate
没有区别,其实区别大了:它并不会直接用新数据覆盖老数据,而是每一个都会被处理,只不过如果前一个还没被处理完后一个就来了的话,处理前一个数据的逻辑就会被取消。
10.StateFlow 和 SharedFlow
StateFlow
和 SharedFlow
是 Flow API,允许数据流以最优方式发出状态更新并向多个使用方发出值。
在 Android 中,StateFlow
非常适合需要让可变状态保持可观察的类。
按照 Kotlin 数据流中的示例,可以从 LatestNewsViewModel
公开 StateFlow
,以便 View
能够监听界面状态更新,并自行使屏幕状态在配置更改后继续有效。
如需将任何数据流转换为 StateFlow
,请使用 stateIn
中间运算符。
StateFlow、Flow 和 LiveData
StateFlow
和 LiveData
具有相似之处。两者都是可观察的数据容器类,并且在应用架构中使用时,两者都遵循相似模式。
但请注意,StateFlow
和 LiveData
的行为确实有所不同:
StateFlow
需要将初始状态传递给构造函数,而LiveData
不需要。当 View 进入
STOPPED
状态时,LiveData.observe()
会自动取消注册使用方,而从StateFlow
或任何其他数据流收集数据的操作并不会自动停止。如需实现相同的行为,您需要从Lifecycle.repeatOnLifecycle
块收集数据流。
不做跟风党,LiveData,StateFlow,SharedFlow 使用场景对比
11.FragmentContainerView的用法
使用navigation,navigation会自动管理fragment,您只要向navigation.xml添加fragment即可,避免使用代码容易出现的错漏;
当要传递动态参数值时,在代码调用setGraph,注意setGraph的参数是传递给navigation.xml文件中app:startDestination这个Fragment;
要切换Fragment时,使用下面的代码切换:
- Activity中切换
1 | NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.fragmentContainerView); |
- Fragment中切换
1
2avController navController = Navigation.findNavController(requireActivity(), R.id.fragmentContainerView);
navController.navigate(R.id.Fragment, null);
12.Android 上的 Kotlin 协程
背景
为什么需要协程?
java 中 回调嵌套(回调地狱)
getToken->Login->……
RxJava
compose 切换切换、flatMap、回调变成流式调用,也能解决回调问题,不过操作符使用有点困难。
我们需要进一步了解响应式的思想。
让异步执行的代码写得和同步一样, 更加符合人类的思维方式。
其实效率并无多大的提升。
协程可以使用阻塞的方式写出非阻塞式的代码,解决并发中常见的回调地狱,这是其最大的优点。
并发和并行的概念
首先从并发( Concurrency )与并行( Parallelism )说起。
并发是指在某个时间段内,多任务交替处理的能力。所谓不患寡而患不均,每个 CPU 不可能只顾着执行某个进程,让其他进程一直处于等待状态。所以, CPU 把可执行时间均匀地分成若干份,每个进程执行一段时间后,记录当前的工作状态,释放相关的执行资源并进入等待状态,让其他进程抢占 CPU 资源。
并行是指同时处理多任务的能力。目前, CPU 已经发展为多核,可以同时执行多个互不依赖的指令及执行块。
并发与并行两个概念非常容易混淆,它们的核心区别在于进程是否同时执行。
以 KTV 唱歌为例,并行指的是有多少人可以使用话筒同时唱歌,并发指的是同一个话筒被多个人轮流使用。
————————————————
https://blog.csdn.net/jun5753/article/details/122718938
协程的基本使用
kotlin中的启动模式
job.join()的使用
步骤
- 获取一个协程作用域用于创建协程
- 通过协程作用域.launch方法启动新的协程任务
- 启动时可以指定执行线程
- 内部通过withContext()方法实现切换线程
- 在onDestroy生命周期方法之中要手动取消
协程作用域
MainScope是协程默认提供的作用域,但是还有其他作用域更为方便
可使用lifecycleScope或者viewModelScope,这两种作用域会自动取消
在UI组件中使用 LifecycleOwner.lifecycleScope, 在ViewModel中使用ViewModel.viewModelScope。
Scope是什么?有什么用?
当launch
, async
或runBlocking
开启新协程的时候, 它们自动创建相应的scope. 所有的这些方法都有一个带receiver的lambda参数, 默认的receiver类型是CoroutineScope
.
Scope在实际应用中解决什么问题呢?
如果我们的应用中, 有一个对象是有自己的生命周期的, 但是这个对象又不是协程, 比如Android应用中的Activity, 其中启动了一些协程来做异步操作, 更新数据等, 当Activity被销毁的时候需要取消所有的协程, 来避免内存泄漏. 我们就可以利用CoroutineScope
来做这件事: 创建一个CoroutineScope
对象和activity的生命周期绑定, 或者让activity实现CoroutineScope
接口.
所以, scope的主要作用就是记录所有的协程, 并且可以取消它们。
CoroutineScope
CoroutineScope
会跟踪它使用 launch
或 async
创建的所有协程。您可以随时调用 scope.cancel()
以取消正在进行的工作(即正在运行的协程)。在 Android 中,某些 KTX 库为某些生命周期类提供自己的 CoroutineScope
。例如,ViewModel
有 viewModelScope
,Lifecycle
有 lifecycleScope
。不过,与调度程序不同,CoroutineScope
不运行协程。
https://www.cnblogs.com/mengdd/p/kotlin-coroutines-basics.html
小结:
问题:
在 自定义 view 中的选择作用域时 如果用
1 | findViewTreeLifecycleOwner()?.lifecycleScope?.launch { |
【官方文档】将 Kotlin 协程与生命周期感知型组件一起使用:
https://developer.android.com/topic/libraries/architecture/coroutines?hl=zh-cn
特点
协程是一种并发设计模式,您可以在 Android 平台上使用它来简化异步执行的代码。
协程是我们在 Android 上进行异步编程的推荐解决方案。值得关注的特点包括:
- 轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
- 内存泄漏更少:使用结构化并发机制在一个作用域内执行多项操作。
- 内置取消支持:取消操作会自动在运行中的整个协程层次结构内传播。
- Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发。
管理长时间运行的任务
协程在常规函数的基础上添加了两项操作,用于处理长时间运行的任务。在 invoke
(或 call
)和 return
之外,协程添加了 suspend
和 resume
:
suspend
用于暂停执行当前协程,并保存所有局部变量。resume
用于让已挂起的协程从挂起处继续执行。
如需调用 suspend
函数,只能从其他 suspend
函数进行调用,或通过使用协程构建器(例如 launch
)来启动新的协程。
使用协程确保主线程安全
Kotlin 协程使用调度程序确定哪些线程用于执行协程。要在主线程之外运行代码,可以让 Kotlin 协程在 Default 或 IO 调度程序上执行工作。在 Kotlin 中,所有协程都必须在调度程序中运行,即使它们在主线程上运行也是如此。协程可以自行挂起,而调度程序负责将其恢复。
Kotlin 提供了三个调度程序,以用于指定应在何处运行协程:
- Dispatchers.Main - 使用此调度程序可在 Android 主线程上运行协程。此调度程序只能用于与界面交互和执行快速工作。示例包括调用
suspend
函数,运行 Android 界面框架操作,以及更新LiveData
对象。 - Dispatchers.IO - 此调度程序经过了专门优化,适合在主线程之外执行磁盘或网络 I/O。示例包括使用 Room 组件、从文件中读取数据或向文件中写入数据,以及运行任何网络操作。
- Dispatchers.Default - 此调度程序经过了专门优化,适合在主线程之外执行占用大量 CPU 资源的工作。用例示例包括对列表排序和解析 JSON。
接着前面的示例来讲,您可以使用调度程序重新定义 get
函数。在 get
的主体内,调用 withContext(Dispatchers.IO)
来创建一个在 IO 线程池中运行的块。您放在该块内的任何代码都始终通过 IO
调度程序执行。由于 withContext
本身就是一个挂起函数,因此函数 get
也是一个挂起函数。
CoroutineContext
CoroutineContext
使用以下元素集定义协程的行为:
Job
:控制协程的生命周期。CoroutineDispatcher
:将工作分派到适当的线程。CoroutineName
:协程的名称,可用于调试。CoroutineExceptionHandler
:处理未捕获的异常。
启动协程
您可以通过以下两种方式来启动协程:
通常,您应使用 launch
从常规函数启动新协程,因为常规函数无法调用 await
。只有在另一个协程内时,或在挂起函数内且正在执行并行分解时,才使用 async
。
Job 作业
Job
是协程的句柄。使用 launch
或 async
创建的每个协程都会返回一个 Job
实例,该实例是相应协程的唯一标识并管理其生命周期
一个任务可以包含一系列状态: 新创建 (New)、活跃 (Active)、完成中 (Completing)、已完成 (Completed)、取消中 (Cancelling) 和已取消 (Cancelled)。虽然我们无法直接访问这些状态,但是我们可以访问 Job
的属性: isActive
、isCancelled
和 isCompleted
。
如果协程处于活跃状态,协程运行出错或者调用 job.cancel()
都会将当前任务置为取消中 (Cancelling) 状态 (isActive = false, isCancelled = true
)。当所有的子协程都完成后,协程会进入已取消 (Cancelled) 状态,此时 isCompleted = true
。
一篇文章带你了解——Kotlin协程
https://zhuanlan.zhihu.com/p/427092689
Suspend
使用 suspend
不会让 Kotlin 在后台线程上运行函数。suspend
函数在主线程上运行是一种正常的现象。在主线程上启动协程的情况也很常见。当您需要确保主线程安全时(例如,从磁盘上读取数据或向磁盘中写入数据、执行网络操作或运行占用大量 CPU 资源的操作时),应始终在 suspend
函数内使用 withContext()
。
https://developer.android.google.cn/kotlin/coroutines
CoroutineScope
CoroutineScope
会跟踪它使用 launch
或 async
创建的所有协程。您可以随时调用 scope.cancel()
以取消正在进行的工作(即正在运行的协程)。在 Android 中,某些 KTX 库为某些生命周期类提供自己的 CoroutineScope
。例如,ViewModel
有 viewModelScope
,Lifecycle
有 lifecycleScope
。不过,与调度程序不同,CoroutineScope
不运行协程。
13.在 Android 中使用协程的最佳做法
https://developer.android.google.cn/kotlin/coroutines/coroutines-best-practices
注入调度程序
在创建新协程或调用 withContext
时,请勿对 Dispatchers
进行硬编码。
1 | // DO inject Dispatchers |
挂起函数应该能够安全地从主线程调用
挂起函数应该是主线程安全的,这意味着,您可以安全地从主线程调用挂起函数。如果某个类在协程中执行长期运行的阻塞操作,那么该类负责使用 withContext
将执行操作移出主线程。这适用于应用中的所有类,无论其属于架构的哪个部分都不例外。
ViewModel 应创建协程
ViewModel
类应首选创建协程,而不是公开挂起函数来执行业务逻辑。如果只需要发出一个值,而不是使用数据流公开状态,ViewModel
中的挂起函数就会非常有用。
1 | // DO create coroutines in the ViewModel |
视图不应直接触发任何协程来执行业务逻辑,而应将这项工作委托给 ViewModel
。这样一来,业务逻辑就会变得更易于测试,因为可以对 ViewModel
对象进行单元测试,而不必使用测试视图所必需的插桩测试。
此外,如果工作是在 viewModelScope
中启动,您的协程将在配置更改后自动保留。如果您改用 lifecycleScope
创建协程,则必须手动进行处理该操作。如果协程的存在时间需要比 ViewModel
的作用域更长,请查看“在业务和数据层中创建协程”部分。
不要公开可变类型
最好向其他类公开不可变类型。这样一来,对可变类型的所有更改都会集中在一个类中,便于在出现问题时进行调试。
1 | // DO expose immutable types |
数据层和业务层应公开挂起函数和数据流
数据层和业务层中的类通常会公开函数以执行一次性调用,或接收数据随时间变化的通知。这些层中的类应该针对一次性调用公开挂起函数,并公开数据流以接收关于数据更改的通知。
1 | // Classes in the data and business layer expose |
采用该最佳做法后,调用方(通常是演示层)能够控制这些层中发生的工作的执行和生命周期,并在需要时取消相应工作。
在业务层和数据层中创建协程
对于数据层或业务层中因不同原因而需要创建协程的类,它们可以选择不同的选项。
如果仅当用户查看当前屏幕时,要在这些协程中完成的工作才具有相关性,则应遵循调用方的生命周期。在大多数情况下,调用方将是 ViewModel。在这种情况下,应使用 coroutineScope
或 supervisorScope
。
1 | class GetAllBooksAndAuthorsUseCase( |
如果只要应用处于打开状态,要完成的工作就具有相关性,并且此工作不限于特定屏幕,那么此工作的存在时间应该比调用方的生命周期更长。对于这种情况,您应使用外部 CoroutineScope
(如“不应取消的工作的协程和模式”这篇博文中所述)。
1 | class ArticlesRepository( |
externalScope
应由存在时间比当前屏幕更长的类进行创建和管理,并且可由 Application
类或作用域限定为导航图的 ViewModel
进行管理。
14. sealed中的类型
1 | sealed class HomeEvent { |
15. Kotlin中的操作符
https://blog.csdn.net/weixin_33877092/article/details/87984701
1 | // mapNotNull{...} : 同map{}相同,过滤掉转换之后为null的元素 |
映射操作符
map{...}
: 把每个元素按照特定的方法进行转换,组成一个新的集合。mapNotNull{...}
: 同map{}
函数的作用相同,只是过滤掉转换之后为null
的元素mapIndexed{index,result}
: 把每个元素按照特定的方法进行转换,只是其可以操作元素的下标(index
),组成一个新的集合。mapIndexedNotNull{index,result}
: 同mapIndexed{}
函数的作用相同,只是过滤掉转换之后为null
的元素flatMap{...}
: 根据条件合并两个集合,组成一个新的集合。groupBy{...}
: 分组。即根据条件把集合拆分为为一个Map<K,List<T>>
类型的集合。具体看实例
过滤操作符
filter{...}
: 把不满足条件的元素过滤掉filterIndexed{...}
: 和filter{}
函数作用类似,只是可以操作集合中元素的下标(index
)filterNot{...}
: 和filter{}
函数的作用相反filterNotNull()
: 过滤掉集合中为null
的元素。take(num)
: 返回集合中前num
个元素组成的集合takeWhile{...}
: 循环遍历集合,从第一个元素开始遍历集合,当第一个出现不满足条件元素的时候,退出遍历。然后把满足条件所有元素组成的集合返回。takeLast(num)
: 返回集合中后num
个元素组成的集合takeLastWhile{...}
: 循环遍历集合,从最后一个元素开始遍历集合,当第一个出现不满足条件元素的时候,退出遍历。然后把满足条件所有元素组成的集合返回。drop(num)
: 过滤集合中前num
个元素dropWhile{...}
: 相同条件下,和执行takeWhile{...}
函数后得到的结果相反dropLast(num)
: 过滤集合中后num
个元素dropLastWhile{...}
: 相同条件下,和执行takeLastWhile{...}
函数后得到的结果相反distinct()
: 去除重复元素distinctBy{...}
: 根据操作元素后的结果去除重复元素slice
: 过滤掉所有不满足执行下标的元素。
统计操作符
any()
: 判断是不是一个集合,若是,则在判断集合是否为空,若为空则返回false
,反之返回true,若不是集合,则返回hasNext
any{...}
: 判断集合中是否存在满足条件的元素。若存在则返回true
,反之返回false
all{...}
: 判断集合中的所有元素是否都满足条件。若是则返回true
,反之则返回false
none()
: 和any()
函数的作用相反none{...}
: 和all{...}
函数的作用相反max()
: 获取集合中最大的元素,若为空元素集合,则返回null
maxBy{...}
: 获取方法处理后返回结果最大值对应那个元素的初始值,如果没有则返回null
min()
: 获取集合中最小的元素,若为空元素集合,则返回null
minBy{...}
: 获取方法处理后返回结果最小值对应那个元素的初始值,如果没有则返回null
sum()
: 计算出集合元素累加的结果。sumBy{...}
: 根据元素运算操作后的结果,然后根据这个结果计算出累加的值。sumByDouble{...}
: 和sumBy{}
相似,不过sumBy{}
是操作Int
类型数据,而sumByDouble{}
操作的是Double
类型数据average()
: 获取平均数reduce{...}
: 从集合中的第一项到最后一项的累计操作。reduceIndexed{...}
: 和reduce{}
作用相同,只是其可以操作元素的下标(index
)reduceRight{...}
: 从集合中的最后一项到第一项的累计操作。reduceRightIndexed{...}
: 和reduceRight{}
作用相同,只是其可以操作元素的下标(index
)fold{...}
: 和reduce{}
类似,但是fold{}
有一个初始值foldIndexed{...}
: 和reduceIndexed{}
类似,但是foldIndexed{}
有一个初始值foldRight{...}
: 和reduceRight{}
类似,但是foldRight{}
有一个初始值foldRightIndexed{...}
: 和reduceRightIndexed{}
类似,但是foldRightIndexed{}
有一个初始值- combine 组合两个流,在经过第一次发射以后,任意方有新数据来的时候就可以发射,另一方有可能是已经发射过的数据
16.tryEmit
emit方法可以理解成先使用tryEmit进行发送,如果发送失败,则将emitter加入到队列中
17. UIState 中数据更新
1 | /** |
1 | private val _uiState = MutableStateFlow(HomeUiState()) |
VirXXgo 端的数据重新展示 类型不变,移植到新的 launcher 中。
1 | RecommendCardView |
1 | data class ImmediateUiState( |
18.问题记录
TODO:
扩展阅读:
19.操作符
distinct : 过滤掉重复的元素
distinctUntilChanged: 过滤掉连续重复的元素,不连续重复的是不过滤。
https://blog.csdn.net/qq_33210042/article/details/103351771
SharedFlow 默认无粘性的,也就是后面的观察者不能收到前面已经发射的数据。
当然也有api,支持。
1 | MutableSharedFlow |
https://blog.csdn.net/zhaoyanjun6/article/details/121911675
1 | /** |
20. ViewModel
ViewModelScope
为应用中的每个 ViewModel
定义了 ViewModelScope
。如果 ViewModel
已清除,则在此范围内启动的协程都会自动取消。如果您具有仅在 ViewModel
处于活动状态时才需要完成的工作,此时协程非常有用。例如,如果要为布局计算某些数据,则应将工作范围限定至 ViewModel
,以便在 ViewModel
清除后,系统会自动取消工作以避免消耗资源。
您可以通过 ViewModel 的 viewModelScope
属性访问 ViewModel
的 CoroutineScope
,如以下示例所示:
1 | class MyViewModel: ViewModel() { |
LifecycleScope
为每个 Lifecycle
对象定义了 LifecycleScope
。在此范围内启动的协程会在 Lifecycle
被销毁时取消。您可以通过 lifecycle.coroutineScope
或 lifecycleOwner.lifecycleScope
属性访问 Lifecycle
的 CoroutineScope
。
以下示例演示了如何使用 lifecycleOwner.lifecycleScope
异步创建预计算文本:
1 | class MyFragment: Fragment() { |
可重启生命周期感知型协程
即使 lifecycleScope
提供了适当的方法以在 Lifecycle
处于 DESTROYED
状态时自动取消长时间运行的操作,但在某些情况下,您可能需要在 Lifecycle
处于某个特定状态时开始执行代码块,并在其处于其他状态时取消。例如,您可能希望在 Lifecycle
处于 STARTED
状态时收集数据流,并在其处于 STOPPED
状态时取消收集。此方法仅在界面显示在屏幕上时才处理数据流发出操作,这样可节省资源并可能会避免发生应用崩溃问题。
对于这些情况,Lifecycle
和 LifecycleOwner
提供了挂起 repeatOnLifecycle
API 来确切实现相应操作。以下示例中的代码块会在每次关联的 Lifecycle
至少处于 STARTED
状态时运行,并且会在 Lifecycle
处于 STOPPED
状态时取消运行:
21. MVI 架构
MVI架构中,特别是谷歌推崇的开发模式,是将整个页面的状态存放于单一的类中,而且这个类必须是Kotlin的data class,因为kotlin的这个特殊的类自带了copy功能,非常方便去更新部分的属性,于是我们就有了下面的一个类:
https://juejin.cn/post/7104565566568202276
本文主要介绍了MVC
,MVP
,MVVM
与MVI
架构,目前MVVM
是官方推荐的架构,但仍然有以下几个痛点
MVVM
与MVP
的主要区别在于双向数据绑定,但由于很多人(比如我)并不喜欢使用DataBindg
,其实并没有使用MVVM
双向绑定的特性,而是单一数据源- 当页面复杂时,需要定义很多
State
,并且需要定义可变与不可变两种,状态会以双倍的速度膨胀,模板代码较多且容易遗忘 View
与ViewModel
通过ViewModel
暴露的方法交互,比较零乱难以维护
而MVI
可以比较好的解决以上痛点,它主要有以下优势:
- 强调数据单向流动,很容易对状态变化进行跟踪和回溯
- 使用
ViewState
对State
集中管理,只需要订阅一个ViewState
便可获取页面的所有状态,相对MVVM
减少了不少模板代码 ViewModel
通过ViewState
与Action
通信,通过浏览ViewState
和Aciton
定义就可以理清ViewModel
的职责,可以直接拿来作为接口文档使用。
MVI
架构为了解决MVVM
在逻辑复杂时需要写多个LiveData
(可变+不可变)的问题,使用ViewState
对State
集中管理,只需要订阅一个ViewState
便可获取页面的所有状态。
当然MVI
也有一些缺点,比如
- 所有的操作最终都会转换成
State
,所以当复杂页面的State
容易膨胀 state
是不变的,因此每当state
需要更新时都要创建新对象替代老对象,这会带来一定内存开销
软件开发中没有银弹,所有架构都不是完美的,有自己的适用场景,读者可根据自己的需求选择使用。
但通过以上的分析与介绍,我相信使用MVI
架构代替没有使用DataBinding
的MVVM
是一个比较好的选择~
这是 MVI 架构的第三篇
https://juejin.cn/post/7108498411149590558(推荐)
用 Flow 重构的数据链路上,Repository 和 ViewModel 的界限就很清晰了:
用 Flow 重构的数据链路上,Repository 和 ViewModel 的界限就很清晰了:Repository 提供原始的数据流,以供 ViewModel 用各种自己喜欢的方式进行合流及变换。
22.应用架构指南
https://developer.android.google.cn/topic/architecture#best-practices
遵循这些建议和最佳实践可以提升应用的可扩缩性、质量和稳健性,并可使应用更易于测试。不过,您应该将这些提示视为指南,并视需要进行调整来满足您的要求。
23. 模拟调试
在 Android 中模拟一个点击事件有三种方式是通过模拟 MotionEvent 来实现;一种是通过 ADB 来实现;一种是通过 Instrumentation 测试框架来实现。
模拟返回事件:
1 | Instrumentation inst = new Instrumentation(); |
24.ViewPager2的使用
https://www.jianshu.com/p/25aa5cacbfb9
25. MAD,现代安卓开发技术:Android 领域开发方式的重大变革!
https://juejin.cn/post/7056983987859750919
26.关于时间格式的处理
https://blog.csdn.net/shenyuanqing/article/details/47703951
27.编写地道的 Kotlin 代码
https://droidyue.com/blog/2019/05/19/do-and-dont-in-kotlin/
- 本文链接: https://june5753.github.io/blog/2022/07/31/Androd中的某个项目技术点总结/
- 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!