当RxCache遇上Kotlin协程Flow,会擦出怎样的火花

什么?RxCache是什么?先整一段代码

RequestApi.api(ApiClient.create(Api.class).getBanner())
    .cacheKey("banner")
    .cacheStrategy(CacheStrategy.CACHE_AND_REMOTE)
    .cacheable(data -> data.hasData())
    .buildCacheWithCacheResult(new CacheType>>() {})
    .subscribe(new CacheSubscriber>>() {
        @Override
        public void onResponse(boolean isFromCache, ApiResponse> result) {
            ((TextView) findViewById(R.id.textview)).setText(new Gson().toJson(result.data));
            Toast.makeText(MainActivity.this, "来自" + (isFromCache ? "缓存" : "网络"), Toast.LENGTH_SHORT).show();
        }
    });

上面这段代码干了两件事情:

  1. 从缓存取数据
  2. 加载网络更新缓存

这就是RxCache,基于RxJava+DiskLruCache实现的磁盘缓存库,支持根据策略自动处理网络数据缓存。

代码传送门:

  • RxJavaCache(该库基于https://github.com/HolenZhou/rxcache修改)
  • 基于Kotlin协程Flow的RxCache版本

随着kotlin的崛起,协程、Flow的出现,我思考着使用Flow重写一下RxCache。这次重写的过程,对Flow有了更深的理解,和大家一起分享。

这里简单说下使用方式。

API

初始化

使用前必须先进行初始化操作。

RxCache.initialize(context)

也可以设置更多参数

/**
 * 初始化
 *
 * @param cacheDir       缓存目录
 * @param cacheVersion   缓存版本
 * @param maxCacheSize   缓存最大size
 * @param cacheConverter 缓存Converter
 */
fun initialize(
    cacheDir: File,
    cacheConverter: GsonCacheConverter = GsonCacheConverter(Gson()),
    cacheVersion: Int = 1,
    maxCacheSize: Long = MAX_CACHE_SIZE

)

写入数据

// 同步写入数据
RxCache.apply {
    put("url", "111")
    put("data", BannerBean().apply {
        desc = "flutter"
        title = "flutter 中文社区"
    })
}

// 异步写入数据
lifecycleScope.launch {
    RxCache.rxPut("ulr2","222").collect()
}

读取数据

// 同步读取数据
RxCache.get("url", String::class.java)

// 异步读取数据
lifecycleScope.launch {
    RxCache.rxGet("data", BannerBean::class.java).collect {
        ToastUtil.toast("rxGet data = ${it?.title}")
    }
}

移除某缓存

RxCache.remove("url");

// 异步
lifecycleScope.launch {
    RxCache.rxRemove("url").collect()
}

清除全部缓存

// 同步
RxCache.clear()

// 异步
lifecycleScope.launch {
    RxCache.rxClear().collect()
    
    RxCache().clearAsync()
}

缓存策略

定义了IStrategy接口,框架内部提供了6中缓存策略,支持自定义。

缓存策略 说明
NO_CACHE 不使用RxCache进行缓存
ONLY_REMOTE 只请求网络,但数据依然会被缓存
ONLY_CACHE 只加载缓存,如离线模式
FIRST_REMOTE 优先请求网络,网络数据无效后,再加载缓存
(如果缓存也没有,则会响应网络的response or error)
FIRST_CACHE 优先加载缓存,缓存没有再去请求网络
CACHE_AND_REMOTE 先加载缓存(成功才会回调缓存response),不管缓存什么结果都会再请求网络。
如果缓存成功,网络请求数据无效,则网络不回调。
如果缓存成功,网络也成功,且网络和缓存数据相同则只有缓存回调,网络不再二次回调,否则会二次回调

网络请求

  • 生成请求的flow
  • 设置缓存策略
  • 设置cacheKey
  • 设置cacheable,用于判断数据是否有效,有效才进行缓存
  • buildCacheWithCacheResult构建
  • flowOn(Dispatchers.IO)指定运行在线程中
  • catch异常
  • collect获取数据
lifecycleScope.launch {
    RequestApi(
        flow {
            emit(ApiClient.create(Api::class.java).getBanner())
        }
    )  // 创建flow
        .cacheStrategy(CacheStrategy.CACHE_AND_REMOTE) // 配置缓存策略
        .cacheKey("banner") // 设置缓存key
        .cacheable(object : ICacheable>> { // 判断数据是否有效,有效才缓存
            override fun cacheable(data: ApiResponse>?): Boolean {
                return data?.errorCode == 0 && data.data != null
            }
        })
//                .buildCache(object : CacheType>>() {})
        .buildCacheWithCacheResult(object : CacheType>>() {})//构建
        .flowOn(Dispatchers.IO) 
        .catch { // 捕获异常
            it.printStackTrace()
            ToastUtil.toast(it.message)
            binding.textview.text = null
        }
        .collect {
            ToastUtil.toast("数据是否来自缓存:${it.isFromCache}")
            binding.textview.text = Gson().toJson(it.data?.data)
        }
}

Flow

下面我们通过RxJava与Flow对比来认识Flow的操作符

对比 RxJava Flow
数据源 Observable Flow
发射数据 onNext emit
改变数据发射的线程 subscribeOn flowOn
改变消费数据的线程 observeOn 协程launch的时候指定context
捕获异常 onError catch或者try-cathch块
完成 onComplete onCompletion
map map map
flatMap flatMap flatMapConcat
compose compose let(transformer)
转换 transformer transformer
去重 distinct distinctUntilChanged
合并 concatWith onCompletion { emitAll(other) }
onErrorResumeNext onErrorResumeNext catch { emitAll(fallback) }
onErrorReturn onErrorReturn catch { emit(fallback) }
压缩 zip zip

创建flow

创建flow有多种方式

  • flowOf
flowOf(1)
flowOf(1, 2, 3)
  • asFlow()
方法.asFlow()
(1..3).asFlow()
  • flow{ emit(value) }
flow { 
    emit(1)
}

切换线程

RxJava中可以使用subscribeOn来切换发射线程,使用observeOn来指定消费线程。而Flow只能通过flowOn来切换发射线程,不能切换消费线程。collect执行的线程取决于协程launch时指定的上下文。

异常

RxJava中使用onError来捕获异常。Flow中使用catch{}或者try-cathch()语句来捕获异常。推荐使用catch{}。

lifecycleScope.launch {
    flow {
        emit("111")
        throw NullPointerException()
        emit("222") // 这个不会发射
    }.catch {
        it.printStackTrace()
        emit("333")
    }.flowOn(Dispatchers.Main)
    .collect {
        println("collect >> $it")
    }
}
// 输出结果:
collect >> 111
collect >> 333

catch可以调用多次,作用范围是调用catch之前的代码。catch内部可以调用emit(value)或者emitAll(flow)再次发射数据。

onCompletion

不管onCompletion之前是否发生了异常,都会回调该方法。只有onCompletion之前发生了异常且没有被catch,参数cause才不会空,参数是throwable。

lifecycleScope.launch {
    flow {
        emit("111")
        throw NullPointerException()
        emit("222") // 这个不会发射
    }.catch {
        it.printStackTrace()
        emit("333")
    }.onCompletion { throwable ->
        if (throwable != null) { // 发生了异常
            emit("444")
        } else {
            emit("555")
        }
    }.flowOn(Dispatchers.Main)
        .collect {
            println("collect >> $it")
        }
}
// 输出结果:
collect >> 111
collect >> 333
collect >> 555

虽然发生了异常,但由于onCompletion被catch住了,所以到onCompletion时是不存在异常的,所以throwable==null。如果前面没有catch,那么throwable就是上面的NullPointerException。

CacheAndRemoteStrategy

下面我们通过分析缓存策略CacheAndRemoteStrategy的实现过程,来简单分析下Flow的操作符。

策略:先加载缓存(成功才会回调缓存response),不管缓存什么结果都会再请求网络。

如果缓存成功,网络请求数据无效,则网络不回调

如果缓存成功,网络也成功,且网络和缓存数据相同则只有缓存回调,网络不再二次回调,否则会二次回调

  • 首先我们是要发射两个数据源,一个是cache一个是net。在RxJava中,我们可以使用concatWith来组合两个Observable。

Flow也有这个操作符,不过我们看下实现。

@Deprecated(
    level = DeprecationLevel.ERROR,
    message = "Flow analogue of 'concatWith' is 'onCompletion'. Use 'onCompletion { emit(value) }'",
    replaceWith = ReplaceWith("onCompletion { emit(value) }")
)
public fun  Flow.concatWith(value: T): Flow = noImpl()

这个扩展方法是不能调用的,让我们使用onCompletion { emit(value) }实现。

还有subscribe、compose、onErrorXxx等RxJava的常用操作符。看到这里,我有点怀疑,官方是故意这么设计的。参考RxJava的常用操作符,让我们可以快速熟悉使用。

不得不说,Flow的操作符的实现更简单,没有各种复杂的操作符,而是通过简单的集中操作符组合来实现功能,更方便理解和使用。

  • loadCache返回cache的Flow。onCompletion在cache执行完成之后,通过emitAll发射网络数据。

前面说过,如果loadCache发生了异常(无缓存时,内部会抛出NoCacheException),onCompletion中可以拿到这个异常。

我们看下网络Flow都干了什么。

  • netSource通过flatMapConcat来判断网络数据是否有效,有效则直接发射netResult、如果无效,则判断当前缓存是否有效。

    if (cacheEx != null) 意味着loadCache发生了异常,没有缓存,那么我们发射netResult。这么做的目的是能响应错误数据,如响应不同的错误码,或者toast 错误信息。

    如果有缓存,则发射一个空的Flow,跳过netFlow。

再往下看,一个catch。

  • 这一步的目的是,如果netSource发生了异常,如网络相关异常、数据解析异常等,那么上面的flatMapConcat里的代码就不会被执行了。

    在catch中,我们判断是否有缓存,如果没有缓存,则抛出netSource发生的异常。

继续往下看,distinctUntilChanged的作用是去重

  • 判断两个数据源response是否一致,一致则不再触发collect二次回调。

下面还有个catch

  • 这一步的目的是捕获住无缓存的异常,如果不是NoCacheException则抛出。

至此,我们就实现了策略CacheAndRemoteStrategy。这算是一个比较复杂的Flow的场景了。

  • 组合Flow
  • Flow的数据转换flatMapConcat
  • 发射数据flowOf
  • 发射emptyFlow
  • 异常处理,多次catch的使用
  • 异常中发射数据emitAll
  • 去重distinctUntilChanged
override fun  execute(
        cache: RxCache,
        cacheKey: String,
        netSource: Flow>,
        type: Type
    ): Flow> {
        return loadCache(cache, cacheKey, type)
            .onCompletion { cacheEx ->
                // 判断是否发生异常
                emitAll(
                    netSource.flatMapConcat { netResult ->
                        // 如果网络数据有效则正常处理
                        if (netResult.cacheable) {
                            flowOf(netResult)
                        } else {
                            // 如果网络数据是无效的,缓存也是无效的,则抛出网络的结果。如果有缓存,则网络结果不再分发
                            if (cacheEx != null) { // 没有缓存
                                flowOf(netResult)
                            } else {
                                emptyFlow()
                            }
                        }
                    }.catch { netEx ->
                        // 网络请求发生了异常,根据是否有缓存判断如何分发
                        if (cacheEx != null) { // 没有缓存,则分发网络结果
                            throw netEx
                        } else {  // 有缓存则不发射网络结果
                            emitAll(emptyFlow())
                        }
                    }
                )
            }
            .distinctUntilChanged { old, new ->
                // 如果网络数据和缓存数据一致,则只发射一次
                if (old.data == null || !new.cacheable) { // 网络无数据或没有缓存
                    false
                } else {
                    isDataSame(old.data, new.data)
                }
            }
            .catch { // 捕获NoCacheException
                if (it !is NoCacheException) {
                    throw it
                }
            }
    }

通过这次重写,不得不说,Flow是真的爽。

现在Kotlin越来越流行了,协程也逐渐兴起,新知识更新很快,一不留神,就落后了。我个人觉得协程和Flow都很简单,只要对比这RxJava,然后多加练习,很快就能上手。

你可能感兴趣的:(当RxCache遇上Kotlin协程Flow,会擦出怎样的火花)