Android 技术内参

一个专注于技术分享的博客

0%

Android中的项目中的最新技术点总结

写在前面

解读一个新项目时,对一些零散的技术点做了一个简单的梳理,本文总结了项目开发中用到的一些知识点。

1. 取集合的一部分

https://www.kotlincn.net/docs/reference/collection-parts.html

Windowed

以检索给定大小的集合元素中所有可能区间。 获取它们的函数称为 windowed():它返回一个元素区间列表,比如通过给定大小的滑动窗口查看集合,则会看到该区间。 与 chunked() 不同,windowed() 返回从每个集合元素开始的元素区间(窗口)。 所有窗口都作为单个 List 的元素返回。

CourseFragment中的用法

1
2
3
4
5
6
7
8
9
10
11
12
lass Adapter(val scene: String) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var items: MutableList<MutableList<LauncherCourseInfo>> = mutableListOf()

fun setData(list: MutableList<LauncherCourseInfo>?) {
items.clear()
list?.apply {
items.addAll(
this.windowed(ITEM_COUNT, ITEM_COUNT, true)
as MutableList<MutableList<LauncherCourseInfo>>
)
}
}

2. Kotlin系列之 in 运算符

https://bingjian.blog.csdn.net/article/details/79090877

in运算符常常用来检查一个值是否在某个区间内。它还有对应的逆运算!in用来检查某个值不在某一个区间内。

1
2
3
4
5
fun recog(c: Char) = when(c){
in '0'..'9' -> "It's a digit"
in 'a'..'z', in 'A'..'Z' -> "It's a letter"
else -> "don't know..."
}

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 性注解

​ 只有在每次使用方法时都应明确检查返回值是否为 null 的情况下,才应对方法的返回值使用 @Nullable

@Nullable 注解用于指明可以为 null 的变量、参数或返回值,

​ 而 @NonNull 则用于指明不可以为 null 的变量、参数或返回值。

  • 资源注解

添加 @StringRes 注解,以检查资源参数是否包含 R.string 引用,如下所示:

1
abstract fun setTitle(@StringRes resId: Int)

其他资源类型的注解(例如 @DrawableRes@DimenRes@ColorRes@InterpolatorRes)可以使用相同的注解格式添加,并在代码检查期间运行

  • 线程注解

如果某个类中的所有方法具有相同的线程要求,您可以为该类添加一个线程注解,以验证该类中的所有方法是否从同一类型的线程调用。

线程注解的一个常见用途是验证 AsyncTask 类中的方法替换,因为此类会执行后台操作,并且仅在界面线程上发布结果。

  • 值约束注解

使用 @IntRange@FloatRange@Size 注解可以验证所传递参数的值。@IntRange@FloatRange 在应用到用户可能会弄错范围的参数时最为有用。

@IntRange 注解可以验证整型或长整型参数值是否在指定范围内。以下示例可以确保 alpha 参数包含 0 到 255 之间的整数值:

1
fun setAlpha(@IntRange(from = 0, to = 255) alpha: Int) { ... }
  • 权限注解

使用 @RequiresPermission 注解可以验证方法调用方的权限。要检查有效权限列表中是否存在某个权限,请使用 anyOf 属性。

  • 返回值注解

使用 @CheckResult 注解可验证是否实际使用了方法的结果或返回值。不应使用 @CheckResult 为每个非 void 方法添加注解,而应添加注解来阐明可能令人不解的方法的结果。

  • CallSuper 注解

    使用 @CallSuper 注解可验证替换方法是否会调用该方法的超类实现。以下示例为 onCreate() 方法添加了注解,以确保所有替换方法实现都会调用 super.onCreate()

    1
    2
    3
    @CallSuper
    override fun onCreate(savedInstanceState: Bundle?) {
    }
  • Typedef 注解(进一步了解)

    使用 @IntDef@StringDef 注解,您可以创建整数集和字符串集的枚举注解来验证其他类型的代码引用。Typedef 注解可以确保特定参数、返回值或字段引用一组特定的常量。这些注解还会启用代码补全功能,以自动提供允许的常量。

    Typedef 注解使用 @interface 来声明新的枚举注解类型。@IntDef@StringDef 注解以及 @Retention 可以对新注解添加注解,是定义枚举类型所必需的。@Retention(RetentionPolicy.SOURCE) 注解可告诉编译器不要将枚举注解数据存储在 .class 文件中。

    以下示例展示了创建某个注解的具体步骤,该注解可以确保作为方法参数传递的值引用某个已定义的常量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import android.support.annotation.IntDef
//...
// Define the list of accepted constants and declare the NavigationMode annotation
@Retention(AnnotationRetention.SOURCE)
@IntDef(NAVIGATION_MODE_STANDARD, NAVIGATION_MODE_LIST, NAVIGATION_MODE_TABS)
annotation class NavigationMode

// Declare the constants
const val NAVIGATION_MODE_STANDARD = 0
const val NAVIGATION_MODE_LIST = 1
const val NAVIGATION_MODE_TABS = 2

abstract class ActionBar {

// Decorate the target methods with the annotation
// Attach the annotation
@get:NavigationMode
@setparam:NavigationMode
abstract var navigationMode: Int

}
  • Keep 注解

    使用 @Keep 注解可以确保以下情况:如果在构建时缩减代码大小,将不会移除带有该注解的类或方法。该注解通常添加到通过反射访问的方法和类,以防止编译器将代码视为未使用。

4. 后备属性

所谓后备属性,其实是对后备字段的一个变种,它实际上也是隐含试的对属性值的初始化声明,避免了空指针。

我们根据一个官网的例子,进行说明:

示例1:

1
2
3
4
5
6
7
8
9
private var _table: Map<String, Int>? = null
public val table: Map<String, Int>
get() {
if (_table == null) {
_table = HashMap() // 初始化
}
// ?: 操作符,如果_table不为空则返回,反之则抛出AssertionError异常
return _table ?: throw AssertionError("Set to null by another thread")
}

上面的代码中我们可以看出:_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
2
3
4
5
6
7
8
9
10
// Declare private mutable variable that can only be modified
// within the class it is declared.
private var _count = 0

// Declare another public immutable field and override its getter method.
// Return the private property's value in the getter method.
// When count is accessed, the get() function is called and
// the value of _count is returned.
val count: Int
get() = _count

举例而言,在您的应用中,您需要应用数据仅对 ViewModel 可见:

ViewModel 类之内:

  • _count 属性设为 private 且可变。因此,只能在 ViewModel 类中对其进行访问和修改。惯例是为 private 属性添加下划线前缀。

ViewModel 类之外:

  • Kotlin 中的默认可见性修饰符为 public,因此 count 是公共属性,可从界面控制器等其他类对其进行访问。由于只有 get() 方法会被替换,所以此属性不可变且为只读状态。当外部类访问此属性时,它会返回 _count 的值且其值无法修改。这可以防止外部类擅自对 ViewModel 内的应用数据进行不安全的更改,但允许外部调用方安全地访问该应用数据的值.
1
2
3
private var _currentScrambledWord = "test"
val currentScrambledWord: String
get() = _currentScrambledWord

5. Hilt

https://developer.android.com/codelabs/android-hilt?hl=zh-cn#0

VirxxGo中的用法:

  1. RepositoryModule
1
2
3
4
5
6
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
abstract fun courseRepository(impl: CourseRepositoryImpl): CourseRepository
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface CourseRepository {

/**
* 开始课程
*/
suspend fun startCourse(immediateTraining: ImmediateTraining)

/**
* 上过的课
*/
suspend fun getAttendedCourse(): List<LauncherCourseInfo>

/**
* 收藏的课
*/
suspend fun getFavoriteCourse(): List<LauncherCourseInfo>
}
  1. 实现接口,在实现中调用网络请求接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Singleton
class CourseRepositoryImpl @Inject constructor() : CourseRepository {

private val dataRepository = DataRepository.getInstance()

override suspend fun getAttendedCourse(): List<LauncherCourseInfo> = suspendCancellableCoroutine {
dataRepository.getAttendedCourseInfo(
object : VirgoXXApiDisposableObserver<List<LauncherCourseInfo>>() {
override fun onError(errorCode: String?, errorMsg: String?) {
super.onError(errorCode, errorMsg)
if (it.isActive) {
it.resumeWithException(BusinessException(errorCode, errorMsg))
}
}

override fun onSuccess(result: List<LauncherCourseInfo>?) {
super.onSuccess(result)
if (it.isActive) {
if (result == null) {
it.resumeWithException(NullPointerException("result is null"))
} else {
it.resume(result)
}
}
}
}
)
}
}
  1. 在viewmodel中获取数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

@HiltViewModel
class FitnessViewModel @Inject constructor(
private val application: Application,
private val courseRepository: CourseRepository,
) : ViewModel() {

private val _uiState = MutableStateFlow(UiState())
val uiState: StateFlow<UiState> = _uiState

init {
LogUtil.d(TAG, "init")
collect()
}

private fun collect() {
// 获取上过的课
viewModelScope.launch {
try {
val attendedCourse = courseRepository.getAttendedCourse()
_uiState.update { current ->
current.copy(attendedCourse = attendedCourse)
}
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
LogUtil.w(TAG, e.stackTraceToString())
_uiState.update { current ->
current.copy(attendedCourse = null)
}
}
}
}
}

6. 使用ViewTreeLifecycleOwner获取Lifecycle

https://blog.csdn.net/vitaviva/article/details/105006686

ViewTreeLifecycleOwner是Lifecycle KTX中提供的View的一个扩展方法,可以快速地获取一个最近的Fragment或者Activity的LifecycleOwner。

7. repeatOnLifecycle

使用更为安全的方式收集 Android UI 数据流

在 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
    6
    val 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
    13
    class 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {

init {
viewModelScope.launch {
newsRepository.favoriteLatestNews
// Intermediate catch operator. If an exception is thrown,
// catch and update the UI
.catch { exception -> notifyError(exception) }
.collect { favoriteNews ->
// Update View with the latest favorite news
}
}
}
}

提供方的数据实现可来自第三方库。这意味着它可能会引发异常情况。如需处理这些异常,请使用 catch 中间运算符。

  • 在不同 CoroutineContext 中执行

默认情况下,flow 构建器的提供方会通过从中收集的协程的 CoroutineContext 执行,并且如前所述,它无法从不同 CoroutineContext 对值执行 emit 操作。在某些情况下,可能不需要此行为。例如,在本主题所用示例中,存储库层不应在 viewModelScope 所使用的 Dispatchers.Main 上执行操作。

如需更改数据流的 CoroutineContext,请使用中间运算符 flowOnflowOn 会更改上游数据流的 CoroutineContext,这表示会在 flowOn 之前(或之上)应用提供方以及任何中间运算符。下游数据流(晚于 flowOn 的中间运算符和使用方)不会受到影响,并会在 CoroutineContext 上执行以从数据流执行 collect 操作。如果有多个 flowOn 运算符,每个运算符都会更改当前位置的上游数据流。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class NewsRepository(
private val newsRemoteDataSource: NewsRemoteDataSource,
private val userData: UserData,
private val defaultDispatcher: CoroutineDispatcher
) {
val favoriteLatestNews: Flow<List<ArticleHeadline>> =
newsRemoteDataSource.latestNews
.map { news -> // Executes on the default dispatcher
news.filter { userData.isFavoriteTopic(it) }
}
.onEach { news -> // Executes on the default dispatcher
saveInCache(news)
}
// flowOn affects the upstream flow ↑
.flowOn(defaultDispatcher)
// the downstream flow ↓ is not affected
.catch { exception -> // Executes in the consumer's context
emit(lastCachedNews())
}
}

借助此代码,onEachmap 运算符使用 defaultDispatcher,其中catch 运算符和使用方在 viewModelScope 所使用的 Dispatchers.Main 上执行。

随着数据源层执行 I/O 操作,您应该使用针对 I/O 操作进行优化的调度程序:

1
2
3
4
5
6
7
8
9
10
class NewsRemoteDataSource(
...,
private val ioDispatcher: CoroutineDispatcher
) {
val latestNews: Flow<List<ArticleHeadline>> = flow {
// Executes on the IO dispatcher
...
}
.flowOn(ioDispatcher)
}

Jetpack 库中的数据流

许多 Jetpack 库已集成数据流,并且在 Android 第三方库中非常受欢迎。数据流非常适合实时数据更新和无限数据流。

您可以使用 Flow with Room 接收有关数据库更改的通知。在使用数据访问对象 (DAO) 时,返回 Flow 类型以获取实时更新。

1
2
3
4
5
@Dao
abstract class ExampleDao {
@Query("SELECT * FROM Example")
abstract fun getExamples(): Flow<List<Example>>
}

每当 Example 数据表发生更改时,系统都会发出包含数据库新数据项的新列表。

将基于回调的 API 转换为数据流

callbackFlow 是一个数据流构建器,允许您将基于回调的 API 转换为数据流。例如,Firebase Firestore Android API 会使用回调。如需将这些 API 转换为数据流并监听 Firestore 数据库的更新,您可使用以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class FirestoreUserEventsDataSource(
private val firestore: FirebaseFirestore
) {
// Method to get user events from the Firestore database
fun getUserEvents(): Flow<UserEvents> = callbackFlow {

// Reference to use in Firestore
var eventsCollection: CollectionReference? = null
try {
eventsCollection = FirebaseFirestore.getInstance()
.collection("collection")
.document("app")
} catch (e: Throwable) {
// If Firebase cannot be initialized, close the stream of data
// flow consumers will stop collecting and the coroutine will resume
close(e)
}

// Registers callback to firestore, which will be called on new events
val subscription = eventsCollection?.addSnapshotListener { snapshot, _ ->
if (snapshot == null) { return@addSnapshotListener }
// Sends events to the flow! Consumers will get the new events
try {
offer(snapshot.getEvents())
} catch (e: Throwable) {
// Event couldn't be sent to the flow
}
}

// The callback inside awaitClose will be executed when the flow is
// either closed or cancelled.
// In this case, remove the callback from Firestore
awaitClose { subscription?.remove() }
}
}

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 之外,还有其他常见的末端操作符,大体分为两类:

  1. 集合类型转换操作,包括 toListtoSet 等。
  2. 聚合操作,包括将 Flow 规约到单值的 reducefold 等操作,以及获得单个元素的操作包括 singlesingleOrNullfirst 等。

实际上,识别是否为末端操作符,还有一个简单方法,由于 Flow 的消费端一定需要运行在协程当中,因此末端操作符都是挂起函数。

Flow 的取消

想要取消 Flow 只需要取消它所在的协程即可。

1
job.cancelAndJoin()

Flow 的背压

只要是响应式编程,就一定会有背压问题,我们先来看看背压究竟是什么。

背压问题在生产者的生产速率高于消费者的处理速率的情况下出现。为了保证数据不丢失,我们也会考虑添加缓存来缓解问题:

  • 也可以为 buffer 指定一个容量。不过,如果我们只是单纯地添加缓存,而不是从根本上解决问题就始终会造成数据积压。
  • 使用 conflate 解决背压问题
  • collectLatest。顾名思义,只处理最新的数据,这看上去似乎与 conflate 没有区别,其实区别大了:它并不会直接用新数据覆盖老数据,而是每一个都会被处理,只不过如果前一个还没被处理完后一个就来了的话,处理前一个数据的逻辑就会被取消。

10.StateFlow 和 SharedFlow

StateFlowSharedFlowFlow API,允许数据流以最优方式发出状态更新并向多个使用方发出值。

在 Android 中,StateFlow 非常适合需要让可变状态保持可观察的类。

按照 Kotlin 数据流中的示例,可以从 LatestNewsViewModel 公开 StateFlow,以便 View 能够监听界面状态更新,并自行使屏幕状态在配置更改后继续有效。

如需将任何数据流转换为 StateFlow,请使用 stateIn 中间运算符。

StateFlow、Flow 和 LiveData

StateFlowLiveData 具有相似之处。两者都是可观察的数据容器类,并且在应用架构中使用时,两者都遵循相似模式。

但请注意,StateFlowLiveData 的行为确实有所不同:

  • 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
2
3
4
5
6
7
8
9
10
NavHostFragment navHostFragment = (NavHostFragment) getSupportFragmentManager().findFragmentById(R.id.fragmentContainerView);
NavController navController = navHostFragment.getNavController();
Bundle bundle = new Bundle();
bundle.putString(key,val);
/**
* 注意
* R.id.fragmentTrees是在navigation.xml中定义的fragment id
* bundle此时传递给R.id.fragmentTrees
*/
navController.navigate(R.id.fragmentTrees,bundle);
  • Fragment中切换
    1
    2
    avController navController = Navigation.findNavController(requireActivity(), R.id.fragmentContainerView);
    navController.navigate(R.id.Fragment, null);

12.Android 上的 Kotlin 协程

背景

为什么需要协程?

  • java 中 回调嵌套(回调地狱)

    getToken->Login->……

  • RxJava

    compose 切换切换、flatMap、回调变成流式调用,也能解决回调问题,不过操作符使用有点困难。

    我们需要进一步了解响应式的思想。

  • 让异步执行的代码写得和同步一样, 更加符合人类的思维方式。

    其实效率并无多大的提升。

    协程可以使用阻塞的方式写出非阻塞式的代码,解决并发中常见的回调地狱,这是其最大的优点。

image-20220707105729332

image-20220707110129006

并发和并行的概念
首先从并发( Concurrency )与并行( Parallelism )说起。

并发是指在某个时间段内,多任务交替处理的能力。所谓不患寡而患不均,每个 CPU 不可能只顾着执行某个进程,让其他进程一直处于等待状态。所以, CPU 把可执行时间均匀地分成若干份,每个进程执行一段时间后,记录当前的工作状态,释放相关的执行资源并进入等待状态,让其他进程抢占 CPU 资源。

并行是指同时处理多任务的能力。目前, CPU 已经发展为多核,可以同时执行多个互不依赖的指令及执行块。

并发与并行两个概念非常容易混淆,它们的核心区别在于进程是否同时执行。

以 KTV 唱歌为例,并行指的是有多少人可以使用话筒同时唱歌,并发指的是同一个话筒被多个人轮流使用。
————————————————
https://blog.csdn.net/jun5753/article/details/122718938

谈谈我对 Kotlin 中协程的理解

协程的基本使用

步骤

  1. 获取一个协程作用域用于创建协程
  2. 通过协程作用域.launch方法启动新的协程任务
    1. 启动时可以指定执行线程
    2. 内部通过withContext()方法实现切换线程
  3. 在onDestroy生命周期方法之中要手动取消

协程作用域

  • MainScope是协程默认提供的作用域,但是还有其他作用域更为方便

  • 可使用lifecycleScope或者viewModelScope,这两种作用域会自动取消

  • 在UI组件中使用 LifecycleOwner.lifecycleScope, 在ViewModel中使用ViewModel.viewModelScope。

Scope是什么?有什么用?

launch, asyncrunBlocking开启新协程的时候, 它们自动创建相应的scope. 所有的这些方法都有一个带receiver的lambda参数, 默认的receiver类型是CoroutineScope.

Scope在实际应用中解决什么问题呢?

如果我们的应用中, 有一个对象是有自己的生命周期的, 但是这个对象又不是协程, 比如Android应用中的Activity, 其中启动了一些协程来做异步操作, 更新数据等, 当Activity被销毁的时候需要取消所有的协程, 来避免内存泄漏. 我们就可以利用CoroutineScope来做这件事: 创建一个CoroutineScope对象和activity的生命周期绑定, 或者让activity实现CoroutineScope接口.

所以, scope的主要作用就是记录所有的协程, 并且可以取消它们。

CoroutineScope

CoroutineScope 会跟踪它使用 launchasync 创建的所有协程。您可以随时调用 scope.cancel() 以取消正在进行的工作(即正在运行的协程)。在 Android 中,某些 KTX 库为某些生命周期类提供自己的 CoroutineScope。例如,ViewModelviewModelScopeLifecyclelifecycleScope。不过,与调度程序不同,CoroutineScope 不运行协程。

https://www.cnblogs.com/mengdd/p/kotlin-coroutines-basics.html

小结:

在 View 上使用挂起函数

问题:

在 自定义 view 中的选择作用域时 如果用

1
2
3
findViewTreeLifecycleOwner()?.lifecycleScope?.launch {

}

【官方文档】将 Kotlin 协程与生命周期感知型组件一起使用:

https://developer.android.com/topic/libraries/architecture/coroutines?hl=zh-cn

一文带你理解Kotlin协程本质核心

特点

协程是一种并发设计模式,您可以在 Android 平台上使用它来简化异步执行的代码。

协程是我们在 Android 上进行异步编程的推荐解决方案。值得关注的特点包括:

  • 轻量:您可以在单个线程上运行多个协程,因为协程支持挂起,不会使正在运行协程的线程阻塞。挂起比阻塞节省内存,且支持多个并行操作。
  • 内存泄漏更少:使用结构化并发机制在一个作用域内执行多项操作。
  • 内置取消支持取消操作会自动在运行中的整个协程层次结构内传播。
  • Jetpack 集成:许多 Jetpack 库都包含提供全面协程支持的扩展。某些库还提供自己的协程作用域,可供您用于结构化并发。

管理长时间运行的任务

协程在常规函数的基础上添加了两项操作,用于处理长时间运行的任务。在 invoke(或 call)和 return 之外,协程添加了 suspendresume

  • 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 使用以下元素集定义协程的行为:

启动协程

您可以通过以下两种方式来启动协程:

  • launch 可启动新协程而不将结果返回给调用方。任何被视为“一劳永逸”的工作都可以使用 launch 来启动。
  • async会启动一个新的协程,并允许您使用一个名为 await 的挂起函数返回结果。

通常,您应使用 launch 从常规函数启动新协程,因为常规函数无法调用 await。只有在另一个协程内时,或在挂起函数内且正在执行并行分解时,才使用 async

Job 作业

Job 是协程的句柄。使用 launchasync 创建的每个协程都会返回一个 Job 实例,该实例是相应协程的唯一标识并管理其生命周期

一个任务可以包含一系列状态: 新创建 (New)、活跃 (Active)、完成中 (Completing)、已完成 (Completed)、取消中 (Cancelling) 和已取消 (Cancelled)。虽然我们无法直接访问这些状态,但是我们可以访问 Job 的属性: isActiveisCancelledisCompleted
如果协程处于活跃状态,协程运行出错或者调用 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 会跟踪它使用 launchasync 创建的所有协程。您可以随时调用 scope.cancel() 以取消正在进行的工作(即正在运行的协程)。在 Android 中,某些 KTX 库为某些生命周期类提供自己的 CoroutineScope。例如,ViewModelviewModelScopeLifecyclelifecycleScope。不过,与调度程序不同,CoroutineScope 不运行协程。

13.在 Android 中使用协程的最佳做法

https://developer.android.google.cn/kotlin/coroutines/coroutines-best-practices

注入调度程序

在创建新协程或调用 withContext 时,请勿对 Dispatchers 进行硬编码。

1
2
3
4
5
6
7
8
9
10
11
12
// DO inject Dispatchers
class NewsRepository(
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
suspend fun loadNews() = withContext(defaultDispatcher) { /* ... */ }
}

// DO NOT hardcode Dispatchers
class NewsRepository {
// DO NOT use Dispatchers.Default directly, inject it instead
suspend fun loadNews() = withContext(Dispatchers.Default) { /* ... */ }
}

挂起函数应该能够安全地从主线程调用

挂起函数应该是主线程安全的,这意味着,您可以安全地从主线程调用挂起函数。如果某个类在协程中执行长期运行的阻塞操作,那么该类负责使用 withContext 将执行操作移出主线程。这适用于应用中的所有类,无论其属于架构的哪个部分都不例外。

ViewModel 应创建协程

ViewModel 类应首选创建协程,而不是公开挂起函数来执行业务逻辑。如果只需要发出一个值,而不是使用数据流公开状态,ViewModel 中的挂起函数就会非常有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// DO create coroutines in the ViewModel
class LatestNewsViewModel(
private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase
) : ViewModel() {

private val _uiState = MutableStateFlow<LatestNewsUiState>(LatestNewsUiState.Loading)
val uiState: StateFlow<LatestNewsUiState> = _uiState

fun loadNews() {
viewModelScope.launch {
val latestNewsWithAuthors = getLatestNewsWithAuthors()
_uiState.value = LatestNewsUiState.Success(latestNewsWithAuthors)
}
}
}

// Prefer observable state rather than suspend functions from the ViewModel
class LatestNewsViewModel(
private val getLatestNewsWithAuthors: GetLatestNewsWithAuthorsUseCase
) : ViewModel() {
// DO NOT do this. News would probably need to be refreshed as well.
// Instead of exposing a single value with a suspend function, news should
// be exposed using a stream of data as in the code snippet above.
suspend fun loadNews() = getLatestNewsWithAuthors()
}

视图不应直接触发任何协程来执行业务逻辑,而应将这项工作委托给 ViewModel。这样一来,业务逻辑就会变得更易于测试,因为可以对 ViewModel 对象进行单元测试,而不必使用测试视图所必需的插桩测试。

此外,如果工作是在 viewModelScope 中启动,您的协程将在配置更改后自动保留。如果您改用 lifecycleScope 创建协程,则必须手动进行处理该操作。如果协程的存在时间需要比 ViewModel 的作用域更长,请查看“在业务和数据层中创建协程”部分

不要公开可变类型

最好向其他类公开不可变类型。这样一来,对可变类型的所有更改都会集中在一个类中,便于在出现问题时进行调试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// DO expose immutable types
class LatestNewsViewModel : ViewModel() {

private val _uiState = MutableStateFlow(LatestNewsUiState.Loading)
val uiState: StateFlow<LatestNewsUiState> = _uiState

/* ... */
}

class LatestNewsViewModel : ViewModel() {

// DO NOT expose mutable types
val uiState = MutableStateFlow(LatestNewsUiState.Loading)

/* ... */
}

数据层和业务层应公开挂起函数和数据流

数据层和业务层中的类通常会公开函数以执行一次性调用,或接收数据随时间变化的通知。这些层中的类应该针对一次性调用公开挂起函数,并公开数据流以接收关于数据更改的通知

1
2
3
4
5
6
7
// Classes in the data and business layer expose
// either suspend functions or Flows
class ExampleRepository {
suspend fun makeNetworkRequest() { /* ... */ }

fun getExamples(): Flow<Example> { /* ... */ }
}

采用该最佳做法后,调用方(通常是演示层)能够控制这些层中发生的工作的执行和生命周期,并在需要时取消相应工作。

在业务层和数据层中创建协程

对于数据层或业务层中因不同原因而需要创建协程的类,它们可以选择不同的选项。

如果仅当用户查看当前屏幕时,要在这些协程中完成的工作才具有相关性,则应遵循调用方的生命周期。在大多数情况下,调用方将是 ViewModel。在这种情况下,应使用 coroutineScopesupervisorScope

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class GetAllBooksAndAuthorsUseCase(
private val booksRepository: BooksRepository,
private val authorsRepository: AuthorsRepository,
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
suspend fun getBookAndAuthors(): BookAndAuthors {
// In parallel, fetch books and authors and return when both requests
// complete and the data is ready
return coroutineScope {
val books = async(defaultDispatcher) {
booksRepository.getAllBooks()
}
val authors = async(defaultDispatcher) {
authorsRepository.getAllAuthors()
}
BookAndAuthors(books.await(), authors.await())
}
}
}

如果只要应用处于打开状态,要完成的工作就具有相关性,并且此工作不限于特定屏幕,那么此工作的存在时间应该比调用方的生命周期更长。对于这种情况,您应使用外部 CoroutineScope(如“不应取消的工作的协程和模式”这篇博文中所述)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class ArticlesRepository(
private val articlesDataSource: ArticlesDataSource,
private val externalScope: CoroutineScope,
private val defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
// As we want to complete bookmarking the article even if the user moves
// away from the screen, the work is done creating a new coroutine
// from an external scope
suspend fun bookmarkArticle(article: Article) {
externalScope.launch(defaultDispatcher) {
articlesDataSource.bookmarkArticle(article)
}
.join() // Wait for the coroutine to complete
}
}

externalScope 应由存在时间比当前屏幕更长的类进行创建和管理,并且可由 Application 类或作用域限定为导航图的 ViewModel 进行管理。

14. sealed中的类型

1
2
3
4
sealed class HomeEvent {
object Refresh : HomeEvent()
object TipsShown : HomeEvent()
}

15. Kotlin中的操作符

https://blog.csdn.net/weixin_33877092/article/details/87984701

1
2
3
4
5
6
7
8
9
// mapNotNull{...} : 同map{}相同,过滤掉转换之后为null的元素
private fun mapNotNull() {
val mList = arrayListOf(null, 0, 1, 2, 3, 4, 5, 6, null)
val mapNotNullList = mList.mapNotNull {
it?.let { it * 2 }
}
println(mapNotNullList)
}
//[0, 2, 4, 6, 8, 10, 12]

映射操作符

  • 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
2
3
4
5
6
7
8
9
10
11
12
13
/**
Updates the MutableStateFlow.value atomically using the specified function of its value.
function may be evaluated multiple times, if value is being concurrently updated.
*/
public inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
while (true) {
val prevValue = value
val nextValue = function(prevValue)
if (compareAndSet(prevValue, nextValue)) {
return
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private val _uiState = MutableStateFlow(HomeUiState())
val uiState: StateFlow<HomeUiState> = _uiState
// 可以实现局部字段更新
_uiState.update { current ->
current.copy(words = currentWords)
}

*/
data class HomeUiState(
val words: List<String>? = null,
val time: String? = null,
val date: String? = null,
val weather: WeatherUiState? = null,
val name: String? = null,
val list: List<Any>? = null,
val tips: TipsUiState? = null,
) {

fun getImmediateUiState(index: Int): ImmediateUiState? {
return list?.getOrNull(index) as? ImmediateUiState
}
}

VirXXgo 端的数据重新展示 类型不变,移植到新的 launcher 中。

1
RecommendCardView
1
2
3
4
5
6
7
8
9
10
11
12
data class ImmediateUiState(
val immediateTraining: ImmediateTraining,
val onStart: () -> Unit,
)

val list = userRepository.getImmediateTraining()
.map { immediateTraining ->
ImmediateUiState(immediateTraining) {
// onStart block 回调
startCourse(immediateTraining)
}
}

18.问题记录

TODO:

扩展阅读:

hilt、kotlin学习记录

19.操作符

distinct : 过滤掉重复的元素

distinctUntilChanged: 过滤掉连续重复的元素,不连续重复的是不过滤。

https://blog.csdn.net/qq_33210042/article/details/103351771

SharedFlow 默认无粘性的,也就是后面的观察者不能收到前面已经发射的数据。

当然也有api,支持。

1
2
3
4
5
MutableSharedFlow
replay 代表重放的数据个数
replay 为0 代表不重放,也就是没有粘性
replay 为1 代表重放最新的一个数据,后来的接收器能接受1个最新数据。
replay 为2 代表重放最新的两个数据,后来的接收器能接受2个最新数据。

https://blog.csdn.net/zhaoyanjun6/article/details/121911675

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Returns a [Flow] whose values are generated with [transform] function by combining
* the most recently emitted values by each flow.
*
* It can be demonstrated with the following example:
* ```
* val flow = flowOf(1, 2).onEach { delay(10) }
* val flow2 = flowOf("a", "b", "c").onEach { delay(15) }
* combine(flow, flow2) { i, s -> i.toString() + s }.collect {
* println(it) // Will print "1a 2a 2b 2c"
* }
* ```
*
* This function is a shorthand for `combineTransform(flow, flow2) { a, b -> emit(transform(a, b)) }
*/
public fun <T1, T2, R> combine(flow: Flow<T1>, flow2: Flow<T2>, transform: suspend (a: T1, b: T2) -> R): Flow<R> =
flow.combine(flow2, transform)

20. ViewModel

ViewModelScope

为应用中的每个 ViewModel 定义了 ViewModelScope。如果 ViewModel 已清除,则在此范围内启动的协程都会自动取消。如果您具有仅在 ViewModel 处于活动状态时才需要完成的工作,此时协程非常有用。例如,如果要为布局计算某些数据,则应将工作范围限定至 ViewModel,以便在 ViewModel 清除后,系统会自动取消工作以避免消耗资源。

您可以通过 ViewModel 的 viewModelScope 属性访问 ViewModelCoroutineScope,如以下示例所示:

1
2
3
4
5
6
7
class MyViewModel: ViewModel() {
init {
viewModelScope.launch {
// Coroutine that will be canceled when the ViewModel is cleared.
}
}
}

LifecycleScope

为每个 Lifecycle 对象定义了 LifecycleScope。在此范围内启动的协程会在 Lifecycle 被销毁时取消。您可以通过 lifecycle.coroutineScopelifecycleOwner.lifecycleScope 属性访问 LifecycleCoroutineScope

以下示例演示了如何使用 lifecycleOwner.lifecycleScope 异步创建预计算文本:

1
2
3
4
5
6
7
8
9
10
11
12
class MyFragment: Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewLifecycleOwner.lifecycleScope.launch {
val params = TextViewCompat.getTextMetricsParams(textView)
val precomputedText = withContext(Dispatchers.Default) {
PrecomputedTextCompat.create(longTextContent, params)
}
TextViewCompat.setPrecomputedText(textView, precomputedText)
}
}
}

可重启生命周期感知型协程

即使 lifecycleScope 提供了适当的方法以在 Lifecycle 处于 DESTROYED 状态时自动取消长时间运行的操作,但在某些情况下,您可能需要在 Lifecycle 处于某个特定状态时开始执行代码块,并在其处于其他状态时取消。例如,您可能希望在 Lifecycle 处于 STARTED 状态时收集数据流,并在其处于 STOPPED 状态时取消收集。此方法仅在界面显示在屏幕上时才处理数据流发出操作,这样可节省资源并可能会避免发生应用崩溃问题。

对于这些情况,LifecycleLifecycleOwner 提供了挂起 repeatOnLifecycle API 来确切实现相应操作。以下示例中的代码块会在每次关联的 Lifecycle 至少处于 STARTED 状态时运行,并且会在 Lifecycle 处于 STOPPED 状态时取消运行:

21. MVI 架构

MVI架构中,特别是谷歌推崇的开发模式,是将整个页面的状态存放于单一的类中,而且这个类必须是Kotlin的data class,因为kotlin的这个特殊的类自带了copy功能,非常方便去更新部分的属性,于是我们就有了下面的一个类:

https://juejin.cn/post/7104565566568202276

Google 推荐使用 MVI 架构?卷起来了

本文主要介绍了MVC,MVP,MVVMMVI架构,目前MVVM是官方推荐的架构,但仍然有以下几个痛点

  1. MVVMMVP的主要区别在于双向数据绑定,但由于很多人(比如我)并不喜欢使用DataBindg,其实并没有使用MVVM双向绑定的特性,而是单一数据源
  2. 当页面复杂时,需要定义很多State,并且需要定义可变与不可变两种,状态会以双倍的速度膨胀,模板代码较多且容易遗忘
  3. ViewViewModel通过ViewModel暴露的方法交互,比较零乱难以维护

MVI可以比较好的解决以上痛点,它主要有以下优势:

  1. 强调数据单向流动,很容易对状态变化进行跟踪和回溯
  2. 使用ViewStateState集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态,相对 MVVM 减少了不少模板代码
  3. ViewModel通过ViewStateAction通信,通过浏览ViewStateAciton 定义就可以理清 ViewModel 的职责,可以直接拿来作为接口文档使用。

MVI架构为了解决MVVM在逻辑复杂时需要写多个LiveData(可变+不可变)的问题,使用ViewStateState集中管理,只需要订阅一个 ViewState 便可获取页面的所有状态。

当然MVI也有一些缺点,比如

  1. 所有的操作最终都会转换成State,所以当复杂页面的State容易膨胀
  2. state是不变的,因此每当state需要更新时都要创建新对象替代老对象,这会带来一定内存开销

软件开发中没有银弹,所有架构都不是完美的,有自己的适用场景,读者可根据自己的需求选择使用。
但通过以上的分析与介绍,我相信使用MVI架构代替没有使用DataBindingMVVM是一个比较好的选择~

这是 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 测试框架来实现。

Android模拟点击的四种方式

模拟返回事件:

1
2
Instrumentation inst = new Instrumentation();
inst.sendKeyDownUpSync(KeyEvent.KEYCODE_BACK);

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/

-------------本文结束---感谢您的阅读-------------