在实践中应用 RxSwift

摘要

本文上半部分将为您解释为什么在实际项目中为什么不要调用 onError 以及尽量不使用 Driver 。同时给出一种合理的解决方案,让我们仍然可以愉快的传递 Error ,并对 Value 进行处理。
下半部分将介绍用函数式来精简我们的代码。

注:本文基于 Swift 3 。

忘记 onError

onError 释放资源

可能这个标题有些吼人,不是说 Rx 中的 Error 处理是很好的方案吗?可以把 Error 归到一起处理。笔者在这里也觉得这是一个很好的方案,但有一个地方非常头疼,发射 Error 后是释放对应的事件链,也就是数据流。还是用网络请求举例,比如登录。我们打算做一个登录 -》 成功 -》保存 token -》 用 token 获取用户信息等等。
在登录的部分,点击登录,进行验证,很明显,如果密码有误,
画图,

图片表达的很清晰,对应代码的代码是:

button
    .rx_tap // 点击登录
    .flatMap(provider.login) // 登录请求
    .map(saveToken) // 保存 token
    .flatMap(provider.requestInfo) // 获取用户信息
    .subscribe(handleResult) // 处理结果

代码和流程图是一个样子的,效果还不错。
运行一下,输入正确的账号密码,登录,登录成功,获取用户信息。一切正常。
但是我们来看下面这种场景,登录失败(不论是因为网络错误,还是因为密码错误之类的原因,我们都对这些错误调用了 onError 传递错误信息),直接将 error 传递到事件结尾,即显示登录错误的信息。此时再去点击登录就不会有任何提示了。
因为上面这一条点击登录事件链都被 dispose 了。

这是一个 bug 。我们不希望在第一次点击登录失败后,再次点击登录缺没什么反应。

事实上在 Try! Swift 大会上有一场 POP 的分享,Demo 地址 RxPagination 。试着把网络关了,拉取一下数据,再打开网络,再拉取一下数据看看?此时是没有什么反应的。补一句,这个项目是值得学习一下的。

用官方的方法处理 Error ?

在讨论用官方的方式处理 Error 前,我们先来确认一件事情,处理一个登录流程,如果出现了错误是否应该继续下去,答案是显然的,不继续,停止本次事件。

官方给出了一下几种操作:

  • retry
  • catchError
  • catchErrorJustReturn
  • doOnError

很可惜,前三种方法都是处理 error ,将 error 变换成正常的值继续下去,并没有停止本次事件。而 doOnError 只是在出现 error 的时候做点什么事情,并不能对事件流有什么影响。

使用 Result

enum Result<T> {
    case value(T)
    case error(ErrorProtocol)
}

Swift 中的枚举关联值是如此的强大,这可以帮我们解决 Error 的处理,如果 case 为 error ,那就不处理,将 error 传递下去即可。

相比原有的 onError 有如下优势:

  • 不因为 error 释放资源
  • 方便对 error 传递、处理

类似这样:

provider.request(GitHubAPI.Authorize)
    .map { result -> Result<String> in
        switch result {
        case .value(let value):
            return Result.value(value["token"].stringValue)
        case .error(let error):
            return Result.error(error)
        }
    }
    .flatMap { result -> Observable<Result<JSON>> in
        switch result {
        case .value(let token):
            return provider.request(GitHubAPI.AccessToken(code: token))
        case .error(let error):
            return Observable.just(Result.error(error))
        }
    }
    .subscribeNext { json in
        // ...
    }

catch 等系列方法也可以直接在这里替代,而且更灵活了一些,可以返回任何我们想要的类型。

过多的“无用”代码

比如我们要进行多个操作,在第一个或第二个操作就可能出现 error 时,我们的代码会变得很臃肿,也就是有很多的 case .error(let error): 的代码。
这并不优雅。

摘要

在上一篇 在实践中应用 RxSwift 1 - 使用 Result 传递值中,我们解决了 error 的处理,但当我们处理一段很长的事件流时,会发现有很多不重要的代码,比如传递 Error 。本文将讨论一种优雅的方式处理该问题 - 函数式。本文结构分为两部分,第一部分讨论上一篇的 error 问题,第二部分再写一些其它的小函数,方便我们更好的写代码。

注:
本文不会为您解释过多关于函数式的内容,如果您需要了解,可以阅读 Chris 的 Functional Swift ,本书还有对应的中文版 函数式 Swift 。

enum Result<Value> {
    case value(Value)
    case error(ErrorProtocol)
}

为 Result 添加 map 和 flatMap

在上一节,我们用 Result 解决了 onError 的问题, 但缺带来了很多重复处理 Error 的代码。先来尝试下 Monad 的方案。先来写个 map

func map<T>(_ transform: (Value) throws -> T) -> Result<T> {
    switch self {
    case .value(let object):
        do {
            let nextObject = try transform(object)
            return Result<T>.value(nextObject)
        } catch {
            return Result<T>.error(error)
        }
    case .error(let error):
        return Result<T>.error(error)
    }
}

可以看到我们这个 map 的实现还是很完善的:

  • 支持对 value 的变换
  • 支持抛出 error

笔者认为这基本满足了我们的需求,传递 Error ,对 value 进行变换,抛出错误。现在我们可以把上一篇的代码改成下面这个样子:

provider
    .request(GitHubAPI.Authorize)
    .map { result in
        result.map { json in
            return json["token"].stringValue
        }
    }
    .flatMap { result -> Observable<Result<JSON>> in
        switch result {
        case .value(let token):
            return provider.request(GitHubAPI.AccessToken(code: token))
        case .error(let error):
            return Observable.just(Result.error(error))
        }
    }
    .subscribeNext { json in
        // ...
    }

易读性仍然不够,我们继续。

在 Rx 中,mapflatMap 是最常用的,我们添加一些小工具。

mapValue

func mapValue<T, K>(_ transform: (T) throws -> K) -> (Result<T>) -> Result<K> {
    return { result in
        result.map(transform)
    }
}

于是我们对 Resultmap 操作可以变成这个样子:

    .map(mapValue { json in
        return json["token"].stringValue
    }

优雅了很多,不需要再处理 error 问题了。

flatMapRequest

类似的,我们还可以对网络请求的 flatMap 下手。

func flatMapRequest<T>(_ transform: (T) -> Target) -> (Result<T>) -> Observable<Result<JSON>> {
    return { result in
        let api = result.map(transform)
        switch api {
        case .value(let value):
            return provider.request(value)
        case .error(let error):
            return Observable.just(Result.error(error))
        }
    }
}

完整的调用就变成了这个样子:

provider
    .request(GitHubAPI.Authorize)
    .map(mapValue { json in
        return json["token"].stringValue
        })
    .flatMap(flatMapRequest { token in
        return GitHubAPI.AccessToken(code: token)
    })
    .subscribeNext { result in
        // ...
    }

注:
这里的 flatMapRequest 的 flatMap 并非真正的 flatMap ,笔者只是方便对应 Rx 中的 flatMap 操作。以此表示这个方法是用在 flatMap 上的。

其他小工具

类似上面的方式,我们还可以写一些常用的方法:

func toTrue<T>() -> T -> Bool {
    return { _ in
        return true
    }
}

再比如调用 rx_sendMessage 时,我们可能不需要参数:

func toVoid<T>() -> T -> Void {
    return { _ in }
}

只需要 Resultvalue 情况?同时获取 value

func filterValue<T>() -> Result<T> -> Observable<T> {
    return { result in
        switch result {
        case .value(let object):
            return Observable.just(object)
        case error(let error):
            return Observable.empty()
        }
    }
}

再比如只处理成功的情况:

func success<T>(_ action: (T) -> Void) -> Result<T> -> Void {
    return { result in
        result.success(action)
    }
}

甚至是带有默认错误处理方法的函数,当然这里笔者就不再赘述,有兴趣可以自行试试看~

可以看到,在实现每一个操作符(比如 map)中传入的闭包,尝试这样函数式的代码,会减少写很多重复代码,重要的是,代码变得更加清晰易读了。此外,您还可以这样组织代码:

class FlatMap<T> {

    private init() { }

    static func request(api: (T) -> Target) -> (T) -> Observable<Result<JSON>> {
        return { object in
            return provider.request(api(object))
        }
    }

    static func request(api: (T) -> Target) -> (Result<T>) -> Observable<Result<JSON>> {
        return { result in
            switch result {
            case .value(let object):
                return request(api: api)(object)
            case let .error(error):
                return Observable.just(Result.error(error))
            }
        }
    }
    /// 过滤出 Result 中的 value
    static var filterValue: (Result<T>) -> Observable<T> {
        return { result in
            switch result {
            case .value(let object):
                return Observable.just(object)
            case .error:
                return Observable.empty()
            }
        }
    }
    /// 过滤出 Result 中的 error
    static var filterError: (Result<T>) -> Observable<ErrorProtocol> {
        return { result in
            switch result {
            case .value:
                return Observable.empty()
            case let .error(error):
                return Observable.just(error)
            }
        }
    }
}

这里我表示所有的方法都是用给 Rx 中 flatMap 操作的。

关于为什么 FlatMap 中会有 filter ,您可以参考这篇文章 用更 Swifty 的代码遍历数据

这里还有一篇美团的 FRP 实践 iOS开发下的函数响应式编程 ,不论您是用 RAC 还是 Rx ,都值得看一看。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • 转载自:https://xiaobailong24.me/2017/03/18/Android-RxJava2.x...
    Young1657阅读 2,011评论 1 9
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,596评论 18 139
  • 首页 资讯 文章 资源 小组 相亲 登录 注册 首页 最新文章 IT 职场 前端 后端 移动端 数据库 运维 其他...
    Helen_Cat阅读 3,843评论 1 10
  • error code(错误代码)=0是操作成功完成。error code(错误代码)=1是功能错误。error c...
    Heikki_阅读 3,355评论 1 9
  • 概述 RxSwift顾名思义是Swift的一种框架,您或许曾经听说过「响应式编程」(Reactive Progra...
    Mr大喵喵阅读 1,850评论 3 4