Giter Club home page Giter Club logo

reactorkit's Introduction

ReactorKit (Flux and Reactive Programming Architecture for Android)

flow

ref ref ref ref ref ref ref License: MIT

🏠 Port of ReactorKit to Kotlin, which corresponds to ReactorKit/1.2.1
😬 Please let me know if you need to fix this document or your license. Thank you, Suyeol Jeon (devxoul)

Getting Started

Please refer to original ReactorKit's : ReactorKit Documentation that describes the core components of apps built with ReactorKit. To get an understanding of the core principles we recommend reading the brilliant flux and reactive programming documentation.

ReactorKit is a framework for a reactive and unidirectional Kotlin application architecture. This repository introduces the basic concept of ReactorKit and describes how to build an application using ReactorKit. You may want to see the Examples section first if you'd like to see the actual code.

Table of Contents

Basic Concept

ReactorKit is a combination of Flux and Reactive Programming. The user actions and the view states are delivered to each layer via observable streams. These streams are unidirectional: the view can only emit actions and the reactor can only emit states.

flow

Design Goal

  • Testability: The first purpose of ReactorKit is to separate the business logic from a view. This can make the code testable. A reactor doesn't have any dependency to a view. Just test reactors and test view bindings. See Testing section for details.
  • Start Small: ReactorKit doesn't require the whole application to follow a single architecture. ReactorKit can be adopted partially, for one or more specific views. You don't need to rewrite everything to use ReactorKit on your existing project.
  • Less Typing: ReactorKit focuses on avoiding complicated code for a simple thing. ReactorKit requires less code compared to other architectures. Start simple and scale up.

View

A View displays data. A activity and fragment are treated as a view. The view binds user inputs to the action stream and binds the view states to each UI component. There's no business logic in a view layer. A view just defines how to map the action stream and the state stream.

To define a view, just have an existing class conform a interface named ReactorView. Then your class will have a property named reactor automatically. This property is typically set using createReactor method's parameter.

class ProfileFragment : Fragment(), ReactorView<ProfileReactor> {
  ...
  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    createReactor(CounterReactor()) // inject reactor
  }

  override fun onDestroyView() {
    super.onDestroyView()
    destroyReactor()
  }
  ...  
}

When the reactor property has changed, bind(reactor: <T : Reactor<*, *, *>>) gets called. Implement this method to define the bindings of an action stream and a state stream.

override fun bind(reactor: ProfileReactor) {
  ...
  // Action (View -> Reactor)
  refreshButton.clicks()
      .map { ProfileReactor.Action.Refresh }
      .subscribe(reactor.action)
      .addTo(disposables)

  // State (Reactor -> View)
  reactor.state.map { it.isFollowing }
      .distinctUntilChanged()
      .subscribe(followButton::setChecked)
      .addTo(disposables)
  ...
}

Reactor

A Reactor is an UI-independent layer which manages the state of a view. The foremost role of a reactor is to separate control flow from a view. Every view has its corresponding reactor and delegates all logic to its reactor. A reactor has no dependency to a view, so it can be easily tested.

Conform to the Reactor interface to define a reactor. This interface requires three types to be defined: Action, Mutation and State. It also requires a property named initialState.

class ProfileReactor
  : Reactor<ProfileReactor.Action, ProfileReactor.Mutation, ProfileReactor.State> {
  ...
  override var initialState: State = State()
    
  // represent user actions
  sealed class Action {
    data class RefreshFollowingStatus(val userId: Int) : Action()
    data class Follow(val userId: Int) : Action()
  }

  // represent state changes
  sealed class Mutation {
    data class SetFollowing(val isFollowing: Boolean) : Mutation()
  }

  // represents the current view state
  data class State(
      val isFollowing: Boolean = false
  )
  ...
}

An Action represents a user interaction and State represents a view state. Mutation is a bridge between Action and State. A reactor converts the action stream to the state stream in two steps: mutate() and reduce().

flow-reactor

mutate()

mutate() receives an Action and generates an Observable<Mutation>.

override fun mutate(val action: Action): Observable<Mutation>

Every side effect, such as an async operation or API call, is performed in this method.

override fun mutate(action: Action): Observable<Mutation> = when (action) {
  is Action.RefreshFollowingStatus(userID) -> // receive an action
    UserAPI.isFollowing(userID) // create an API stream
      .map { isFollowing -> Mutation.setFollowing(isFollowing) } // convert to Mutation stream

  is Action.Follow(userID) -> 
    UserAPI.follow().map { Mutation.setFollowing(true) }
}

reduce()

reduce() generates a new State from a previous State and a Mutation.

override fun reduce(state: State, mutation: Mutation): State

This method is a pure function. It should just return a new State synchronously. Don't perform any side effects in this function.

override fun reduce(state: State, mutation: Mutation): State = when (mutation) {
  is Mutation.SetFollowing(isFollowing) -> state.copy(isFollowing = mutation.isFollowing) // manipulate the state, creating a new state
}

transform()

transform() transforms each stream. There are three transform() functions:

override fun transformAction(action: Observable<Action>): Observable<Action>
override fun transformMutation(mutation: Observable<Mutation>): Observable<Mutation>
override fun transformState(state: Observable<State>): Observable<State>

Implement these methods to transform and combine with other observable streams. For example, transform(mutation:) is the best place for combining a global event stream to a mutation stream. See the Global States section for details.

These methods can be also used for logging purposes:

override fun transformAction(action: Observable<Action>): Observable<Action> {
  return action.doOnNext { Log.i(TAG, "$it") } // Logging action event
}

Advanced

Global States

Unlike Redux, ReactorKit doesn't define a global app state. It means that you can use anything to manage a global state. You can use a BehaviorSubject, a PublishSubject or even a reactor. ReactorKit doesn't force to have a global state so you can use ReactorKit in a specific feature in your application.

There is no global state in the Action → Mutation → State flow. You should use transformMutation(mutation: Observable<Mutation>) to transform the global state to a mutation. Let's assume that we have a global BehaviorSubject which stores the current authenticated user. If you'd like to emit a Mutation.setUser(val user: User?) when the currentUser is changed, you can do as following:

var currentUser: BehaviorSubject<User> // global state

override fun transformMutation(mutation: Observable<Mutation>): Observable<Mutation> {
  return Observable.merge(mutation, currentUser.map(Mutation.setUser))
}

Then the mutation will be emitted each time the view sends an action to a reactor and the currentUser is changed.

View Communication

  • TBD

Testing

ReactorKit has a built-in functionality for a testing. You'll be able to easily test both a view and a reactor with a following instruction.

What to test

First of all, you have to decide what to test. There are two things to test: a view and a reactor.

  • View
    • Action: is a proper action sent to a reactor with a given user interaction?
    • State: is a view property set properly with a following state?
  • Reactor
    • State: is a state changed properly with an action?

View testing

A view can be tested with a stub reactor. A reactor has a property stub which can log actions and force change states. If a reactor's stub is enabled, both mutate() and reduce() are not executed. A stub has these properties:

var isEnabled: Bool { get set }
val state: StateRelay<Reactor.State> { get }
val action: ActionSubject<Reactor.Action> { get }
val actions: MutableList<Action> { get } // recorded actions

Here are some example test cases:

fun testAction_refresh() {
  // 1. prepare a stub reactor
  val reactor = MyReactor().apply { stub.isEnabled = true }

  // 2. prepare a view with a stub reactor
  val fragment = activityRule.activity.supportFragmentManager.findFragmentById(R.id.container) as MyFragment
  InstrumentationRegistry.getInstrumentation().runOnMainSync { fragment.reactor = reactor }

  // 3. send an user interaction programatically (with ui test framework espresso)
  onView(withId(R.id.refreshControl)).perform(click())

  // 4. assert actions
  assertEquals(reactor.stub.actions.last(), Refresh)
}

fun testState_isLoading() {
  // 1. prepare a stub reactor
  val reactor = MyReactor().apply { stub.isEnabled = true }

  // 2. prepare a view with a stub reactor
  val fragment = activityRule.activity.supportFragmentManager.findFragmentById(R.id.container) as MyFragment
  InstrumentationRegistry.getInstrumentation().runOnMainSync { fragment.reactor = reactor }

  // 3. set a stub state
  reactor.stub.state.accept(MyReactor.State(isLoading = true))

  // 4. assert view properties (with ui test framework espresso)
  onView(withId(R.id.activityIndicator)).check(matches(isDisplayed()))
}

Reactor testing

A reactor can be tested independently.

fun testIsBookmarked() {
  val reactor = MyReactor()
  reactor.initialState = MyReactor.State(value = 0)

  reactor.action.accept(ToggleBookmarked)  
  assertEquals(reactor.currentState.isBookmarked, true)

  reactor.action.accept(ToggleBookmarked)  
  assertEquals(reactor.currentState.isBookmarked, false)
}

Sometimes a state is changed more than one time for a single action. For example, a Refresh action sets state.isLoading to true at first and sets to false after the refreshing.

Examples

  • Counter : The most simple and basic example of ReactorKit
  • GitHub Search 🚧 : A simple application which provides a GitHub repository search (Under Construction)

Installation

ReactorKit officially supports JitPack only.

JitPack

Add the JitPack repository and dependency to your build file

allprojects {
  repositories {
    ...
    maven { url 'https://jitpack.io' }
  }
}
dependencies {
  implementation 'com.github.perelandrax:reactorkit:${version}'
}

License

ReactorKit is under MIT license. See the LICENSE for more info.

reactorkit's People

Contributors

devsoupe avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.