Suas iOS & Android Unidirectional Flow Architecture

Welcome to the Suas documentation. You'll find comprehensive guides and documentation to help you start working with Suas as quickly as possible, as well as support if you get stuck. Let's jump right in!

Get Started    Guides

Counter App Example

In this example, we implement a counter application. This application consists of a label showing the counter value, an increment button that increments the count, and a decrement button that decrements it. (Check the Counter application source code on GitHub iOS / Android)

The state for this app looks like the following:

struct Counter {
  var value: Int
}
data class Counter(val value: Int = 0)
class Counter {      
  private final int count;
  
  // constructor + getter
}

We have two actions in this app; IncrementAction and DecrementAction:

// Action we will dispatch when we want to increment the value of the counter
struct IncrementAction: Action {
  let incrementValue: Int
}

// Action we will dispatch when we want to decrement the value of the counter
struct DecrementAction: Action {
  let decrementValue: Int
}
data class IncrementAction(val value: Int) : Action<Int>("increment", value)

data class DecrementAction(val value: Int) : Action<Int>("decrement", value)
private static final String INCREMENT_ACTION = "increment";
private static final String DECREMENT_ACTION = "decrement";

private Action getDecrementAction(int value) {
  return new Action<>(DECREMENT_ACTION, value);
}

private Action getIncrementAction(int value) {
  return new Action<>(INCREMENT_ACTION, value);
}

The reducer takes each of these actions and increments or decrements the count value based on the aciton internal value.

struct CounterReducer: Reducer {
  var initialState = Counter(value: 0)
  
  func reduce(state: Counter, action: Action) -> Counter? {
    // Handle each action
    if let action = action as? IncrementAction {
      var newState = state
      newState.value += action.incrementValue
      return newState
    }

    if let action = action as? DecrementAction {
      var newState = state
      newState.value -= action.decrementValue
      return newState
    }

    // Important: If action does not affec the state, return ni
    return nil
  }
}
class CounterReducer : Reducer<Counter>() {

  override fun reduce(oldState: Counter, action: Action<*>): Counter? {
    
    return when (action) {
      
      // Handle increment action
      is IncrementAction -> {
        oldState.copy(value = oldState.value + action.value)
      }
      
      // Handle decrement action
      is DecrementAction -> {
        oldState.copy(value = oldState.value - action.value)
      }
      
      // Important: If action does not affec the state, return nil
      else -> null
    }
  }
    
  // Provide a default value
  override fun getEmptyState(): Counter = Counter()
}
private static class CounterReducer extends Reducer<Counter> {

  @Nullable
  @Override
  public Counter reduce(@NonNull Counter oldState, @NonNull Action<?> action) {
    switch (action.getActionType()) {
        
      // Handle increment action
      case INCREMENT_ACTION: {
        int incrementValue = action.getData();
        return new Counter(oldState.count + incrementValue);
      }
        
      // Handle decrement action
      case DECREMENT_ACTION: {
        int decrementValue = action.getData();
        return new Counter(oldState.count - decrementValue);
      }
        
      // Important: If action does not affec the state, return nil
      default: {
        return null;
      }
    }
  }

  @NonNull
  @Override
  public Counter getEmptyState() {
    // Provide a default value
    return new Counter(0);
  }
}

There are two other things to note above:

  1. When defining the reducer, we state the initial value for it. For our CounterReducer we defined Counter(value: 0) as its initial state
  2. The reducer handles only the actions it knows about, for any other action, nil is returned. Returning nil signifies that the state was not changed.

Next, we create a store with the reducer.

let store = Suas.createStore(reducer: counterReducer, middleware: LoggerMiddleware())
val store = Suas.createStore(CounterReducer()).build()
Store store = Suas.createStore(new CounterReducer()).build();

Now that we have all the components in place, it's time to use them in the UI.

class ViewController: UIViewController {
  @IBOutlet weak var counterLabel: UILabel!

  override func viewDidLoad() {
    super.viewDidLoad()   
    // Add a listener to the store
    let subscription = store.addListener(forStateType: Counter.self) { [weak self] state in
      // Notice that we capture self weakly to prevent strong memory cycles
      self?.counterLabel.text = "\(state.value)"
    }

    // When this object is deallocated, the listener will be removed
    // Alternatively, you have to delete it by hand `subscription.removeListener()`
    subscription.linkLifeCycleTo(object: self)
  }

  @IBAction func incrementTapped(_ sender: Any) {
    // Dispatch actions to the store
    store.dispatch(action: IncrementAction(incrementValue: 1))
  }

  @IBAction func decrementTapped(_ sender: Any) {
    // Dispatch actions to the store
    store.dispatch(action: DecrementAction(decrementValue: 1))
  }
}
val subscription = store.addListener(Counter::class.java) { _, (value) ->
  println("State changed to $value")
}

// ...
store.dispatchAction(IncrementAction(10))
store.dispatchAction(IncrementAction(1))
store.dispatchAction(DecrementAction(5))

// ...
subscription.removeListener()
Subscription subscription = store.addListener(Counter.class, (oldState, newState) ->
  System.out.println("State changed to " + newState.count)
);

// ...
store.dispatchAction(getIncrementAction(10));
store.dispatchAction(getIncrementAction(1));
store.dispatchAction(getDecrementAction(5));

// ...
subscription.removeListener();

We start by adding a listener to the store, this listener will listen to the state of Counter type. (Suas store's state can have multiple state types. Read more about it in applications with multiple states).

When the counter value changes, we receive a notification with this state. The new value of the counter will be used to populate the counter label.

The subscription returned from addListener is used to link the listener lifecycle to the ViewController by calling subscription.linkLifeCycleTo(object:). When the counter view controller is deallocated, the listener is removed from the store. If we don't link the lifecycles, we have to call subscription.removeListener() in the counter view controller deinit function.

Finally, when the increment or decrement buttons are tapped we dispatch an action to the store. The action will reach the reducer which will return a new state that will eventually be used to update the counter view controller label text.

What's Next

Counter iOS application source code on GitHub
Counter command line application source code on GitHub

Other sample apps

List of sample applications
Todo App Example
Search Cities Example
Todo app with settings example

Updated 3 years ago

Counter App Example


Suggested Edits are limited on API Reference Pages

You can only suggest edits to Markdown body content, but not to the API spec.