<荐> RxSwift + ReactorKit 构建信息流框架

需要实现的效果(动态图)

Note: 以上即为我们需要实现的效果,可以在 RxBasicInterface 拿到基本框架的代码直接着手开发。如果你对这个使用 RxSwift 实现的基本框架感兴趣的话,可以查看文章:Rx 项目基本框架的构建 ,而当前最终代码: 信息流 Demo

在还没遇到 ReactorKit 这个框架之前,我使用 RxSwift + MVVM 去构建如图的信息流时,确实为我带来很多好处:

  • 层级更加清晰,分工和职能更加明确
  • 大幅度解耦控制器类,代码的逻辑体现更为明朗、易看
  • 响应式编程,杂散的代码块转化为了集中的单向代码流
  • 可维护性和可测试性都极大的增强
  • 不再增加一些所谓的语法糖,代码也很 ''甜''
    ....

确实是这样子,没使用 RxSwift + MVVM 时,我在代码中使用了挺多语法糖的,举个 NotificationCenter 使用语法糖的例子:

// 在需使用到 Selector 类前添加以下代码
private extension Selector {
    static let handleFunction = #selector(ExampleViewController.handleFunction)
}

// 在类中的使用就变得很简单
NotificationCenter.default.addObserver(self, selector: .handleFunction, name: .notificationName, object: nil)

其实,以上的语法糖,我在使用 RxSwift 之前,确实觉得它让代码看起来更好看了些。但在使用 RxSwift 之后,真心觉得,使用这个语法糖,就是在每个类前面加了顶绿帽子,只是增长了代码的长度而已...
而使用 RxSwift ,我们不再需要添加这个语法糖了。

但是,使用 RxSwift + MVVM ,还是一直存在几个困惑:

  • 我需要在 ViewModel、Service 类中添加多个 Observer、Observable,而有些 Observer 又得是 Observable,这样一来,我们需要额外的去注意这些属性的应用场合
  • ViewModel 作为其中主要的处理器,如果其控制器类比较复杂,那么它就需要管理很多属性,极大的增加了我们的使用负担
  • 代码复用率比较低

其实对于以上问题,我曾经想过使用面向协议编程,让一些重复使用到的代码,或者像一些列表类必须实现的方法定义成协议,让类去遵循协议,实现协议方法,进一步让该类的实现更加合理化,条理更加清晰。但是,不可避免的要定义很多协议,实现很多协议方法...
那么,有没有一个框架,可以进一步增强 RxSwfit + MVVM 的优势,削减其劣势呢?
直到遇到了 ReactorKit 这个框架,解决了我所有的困惑。

认识 ReactorKit

考察
ReactorKit 是 Jeon Suyeol 的作品,而
Jeon Suyeol 发布了很多富有创造性的框架,如 ThenURLNavigatorSwiftyImage 以及一些开源项目 RxTodoDrrrible。同时,他也是多个组织的成员( RxSwiftCommunityMoyaSwiftKorea...),所以我们完全可以放心的使用这个框架,完全不需要去担心这个框架后期维护的问题。其实这个框架的思想并不复杂,即使 Jeon Suyeol 不再维护该框架,我们也完全可以按照他的思想,写个类似的框架供自己使用。

优点

  • 分工、职能更进一步清晰、明朗
  • 更进一步的模块化和响应式,让代码更便于管理
  • 可由我们熟悉的 用户行为 折射到界面熟悉的 状态行为

使用 ReactorKit

使用介绍

ReactorKit 是一个轻量的响应式编程框架,我们把所有的视图(UIView)、界面(UIViewController)都当成 View,而 View 主要是被用户直接操作的层级,我们通过监测用户在 View 上的行为,反馈给 Reactor(响应器),经由响应器处理之后把响应状态传递给 View 层。最后, View 显示最终的传递的状态;简单来说,即 View 层只发出行为,而 Reactor 只发出状态,相互把对方所需要的东西传递给对方,构成一条响应式的序列。

响应过程

那么,我们先从响应器 Reactor 着手,先分析用户行为,再在其中将用户行为转换为可呈现在 View 上的 State。

Reactor

对于响应器来说,其主要是接收到 View 层发出的 Action,然后通过内部操作,将 Action 转换为 State。

Reactor 所有属性和方法

以上,即为该 Reactor 所有的内容,接下来我们逐步的定义、实现其全部内容。

Action: 描述用户行为

我们一般会如何操作一张列表呢?无非是:

  • 下拉 - 刷新拿到最新的数据
  • 上拉 - 加载更多的数据

那么,以上两个操作,即属于用户行为 (Action)

// 定义 Action
enum Action {
    case loadFirstPage
    case loadNextPage
}
Mutation: 用于描述状态变更

对于以上两个用户行为,会有哪些状态变更呢?
下拉行为:

  • 变更1:触发顶部刷新控件的状态变更
  • 变更2:触发列表数据的状态变更(拿到新一页数据/没有拿到最新数据)

上拉行为:

  • 变更1:触发底部刷新控件的状态变更
  • 变更2:触发列表数据的状态变更(增多/不变)
// 定义 Mutation
enum Mutation {
    case setLoadingFirstPage(Bool)
    case fetchedNewestDatas([ListResponseData<HomeList>])
    case setLoadingNextPage(Bool)
    case fetchedMoreDatas([ListResponseData<HomeList>], nextPage: Int)
}
State:用于记录当前状态

其用于记录当前状态:

  • 显示的数据
  • 刷新状态(索取最新数据)
  • 刷新状态(索取更多数据)

而当前的状态用于控制列表的显示状态

// 定义状态
struct State {
    var listDatas: [ListResponseData<HomeList>] = []
    var isLoadingNewest: Bool = false
    var isLoadingMore: Bool = false
    var nextPage: Int?
}
Mutate() :处理 Action

我们需要处理所有定义的 Action(这里定义了两个 Action: loadFirstPage、loadNextPage)

Mutate 数据处理过程
// 方法1:将用户行为转换为显示状态,并返回 Mutation 可观察序列
func mutate(action: Action) -> Observable<Mutation> {
    
    switch action {
    case .loadFirstPage:
        // 如果当前正在刷新最新数据,则不重复刷新
        guard !self.currentState.isLoadingNewest else { return Observable.empty() }
        return Observable.concat([
            Observable.just(Mutation.setLoadingFirstPage(true)),
            // 通过网络服务类(RequestService)索取网络数据,并处理成 Observable<Mutation> 类型
            RequestService.fetchListData(with: .home).flatMap({ (listData) -> Observable<Mutation> in
                return Observable.just(Mutation.fetchedNewestDatas([listData]))
            }),
            
            Observable.just(Mutation.setLoadingFirstPage(false)),
            
            ])
        
    case .loadNextPage:
        guard let currentPage = self.currentState.nextPage, !self.currentState.isLoadingMore else { return Observable.empty() }
        
        return Observable.concat([
            Observable.just(Mutation.setLoadingNextPage(true)),
            
            RequestService.fetchListData(with: .home, page: currentPage).flatMap({ (listData) -> Observable<Mutation> in
                return Observable.just(Mutation.fetchedMoreDatas([listData], nextPage: currentPage + 1))
            }),
            
            Observable.just(Mutation.setLoadingNextPage(false)),
            
            ])
    }
}
Reduce() :更新 State

拿到旧的状态值,根据上一个操作返回的 Mutation 处理成新的 State

// 方法2:拿到方法1中的 Mutation ,更新状态
func reduce(state: State, mutation: Mutation) -> State {
   // 拿到旧的状态值
   var newState = state
   // 拿到上一步处理好的 mutation,协助更新 State 的值(总共有四种中间状态 - Mutation)
   switch mutation {
   case .setLoadingFirstPage(let isRefreshing):
       newState.isLoadingNewest = isRefreshing
       
   case .setLoadingNextPage(let loadingMore):
       newState.isLoadingMore = loadingMore
       
   case .fetchedNewestDatas(let newestDatas):
       newState.listDatas = newestDatas
       // 这里拿第2页作为首页,故接下来应该为第3页的数据
       newState.nextPage = 3
       
   case let .fetchedMoreDatas(appendedDatas, nextPage: nextPage):
       // 拿到下一页数据之后,需要拼接到已请求到的数据之后
       newState.listDatas.append(contentsOf: appendedDatas)
       newState.nextPage = nextPage
   }

   return newState
}

OK,我们已经构建好 Reactor 类了,接下来进入主菜:构造 UIViewController。

View(UIViewController && UIView)

ReactorKit 把 UIViewController 和 UIView 都当成 View ,而它们主要是负责发出 Action,故我们需要监测 View 层发出的 Action。

控制器类的选型

对于开发控制器,一般有2种方式:

  • 使用 Storyboard 开发控制器
    这种方式下,我们需要让该控制器类继承 ReactorKit 的StoryboardView
  • 纯代码开发控制器
    这种方式下,我们需要让该控制器类继承 ReactorKit 的 View
控制器类的基本配置

配置1: 配置顶部控件

// 这是在上一篇文章中有说过的内容,感兴趣可以去查看
fileprivate func initializeTopBarControls() {
    let barStyle = NavigationBarStyle(center: (image: nil, title: "首页"))
    let navigationBar = NavigationBar(themeStyle: barStyle)
    self.view.addSubview(navigationBar)
}

配置2:列表 CollectionView 的基本配置

// 因为我们是使用 Storyboard 进行配置的,所以这里需要配置的属性就很少
fileprivate func configure(for currentCollectionView: UICollectionView) {
    currentCollectionView.registerForCell(HomeListCell.self)
}

配置3:定义列表的数据源属性

let dataSource = RxCollectionViewSectionedReloadDataSource<ListResponseData<HomeList>>()
在控制器类中使用 ReactorKit

第1步:引进该框架

import ReactorKit

第2步:指定 Reactor 的类型

// 这里是首页模块,故其类型为 HomePageReactor
typealias Reactor = HomePageReactor

第3步:实现协议属性

var disposeBag = DisposeBag()

第4步:注入 Reactor

if let homeViewController = homeNav.viewControllers.first as? HomeViewController {
    // 必须先注入 Reactor 类,注入之后, ReactorKit 自动回调用第5步的绑定方法
    homeViewController.reactor = HomePageReactor()
}

第5步:实现协议方法

func bind(reactor: Reactor) {
  // 待会会在这里搞事情,请期待...
}
处理绑定事件

在处理绑定事件前,我们先设置 UICollectionView 的代理和数据源方法

// DataSource && Delegate
 collectionView.rx.setDelegate(self).disposed(by: disposeBag)

 self.dataSource.configureCell = { _, collectionView, indexPath, element in
    let cell = collectionView.dequeueCell(HomeListCell.self, indexPath: indexPath)
    // 设置 Cell 的响应器(Cell 也是 View 层,其中的处理与当前控制器类是一样的,这里不再赘余)
    cell.reactor = HomeListCellReactor(data: element)
    cell.feedNumber = indexPath.item + 1
    return cell
 }

在以上第5步的绑定方法中,我们需要去监测 View 层的 Action,只有我们定义的ObserVable Sequence 中有 Action 发出,我们的 Reactor 就拿到该 Action 进行相应的处理

// Action(View -> Reactor)
  // 处理加载第一页数据的 Action
 collectionView.rx.contentOffset
    .filter { [weak self] offset in
        guard let strongSelf = self else { return false }
        guard strongSelf.collectionView.height > 0 else { return false }
        return ((offset.y < Constant.refreshTriggerValue || strongSelf.collectionView.contentSize.height == 0) ? true : false)
    }
    .map { _ in Reactor.Action.loadFirstPage }
    .bind(to: reactor.action)
    .disposed(by: self.disposeBag)

  //  处理加载更多的 Action
 // Note: 这里我们可以进行数据的预加载,通过控制 UICollectionView 的 OffSet,
  // 当其快滑到当前列表的底部时,我们先进行数据的加载,没必要等到列表完全加载完才去加载数据
 collectionView.rx.contentOffset
    .filter { [weak self] offset in
        guard let strongSelf = self else { return false }
        guard strongSelf.collectionView.contentSize.height > 0 else { return false }
        return (offset.y + strongSelf.collectionView.height + 50 > strongSelf.collectionView.contentSize.height ? true : false)
    }
    .map { _ in Reactor.Action.loadNextPage }
    .bind(to: reactor.action)
    .disposed(by: self.disposeBag)

而当 Reactor 处理好 Action 之后,会有新的 state,这个时候,我们就需要去监听新的 state,将其绑定到 UICollectionView 的数据源上,而当数据源发生变化时,RxSwift 又会去处理 dataSource 的
configureCell 方法,实现数据的良性传输。

// State(Reactor -> View)
 reactor.state.asObservable()
    .map { $0.listDatas }
    .bind(to: self.collectionView.rx.items(dataSource: self.dataSource))
    .disposed(by: self.disposeBag)

到这里,我们就已成功的使用 RxSwift + ReactorKit 构建了一个信息流框架。
如果感兴趣的话,无论你们的项目现在或将来会不会使用到这个技术点,都不妨亲手试试,一定会有不少收获的!
Demohttps://github.com/iJudson/RxSwift-ReactorKit
欢迎 stars
Thanks:多谢观看,欢迎收藏文章,欢迎关注、交流...

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,498评论 25 707
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,599评论 18 139
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,868评论 6 13
  • “我终于拿到第一笔提成了!” 表妹兴奋地欢呼到。这是她12月份新找到的一家公司,应聘时说她作为实习生是没有提出可拿...
    我是吴掌柜阅读 261评论 0 0
  • 讲师:Allan Adams 授课语言:英文 类型:演讲 TED全网首播 科技 课程简介:2015年9月14日是一...
    TED精选集阅读 333评论 0 0