Adding a listener with a filter

When adding a listener to the store, we can pass a filter block. This filter block receives both the old and new state and depending on their values decides whether to notify the listener or not. The function signature looks like the following.

func addListener<StateType>(
    forStateType type: StateType.Type,
       if filterBlock: FilterFunction<StateType>? = nil,
             callback: (StateType) -> ()) -> Subscription
fun <E> addListener(stateType: Class<E>, filter: Filter<E>, listener: Listener<E>): Subscription
<E> Subscription addListener(@NonNull Class<E> stateType, @NonNull Filter<E> filter, @NonNull Listener<E> listener);

When passing nil as the filter block, all state changes will end up triggering the listener notification and will execute the callback block with the new state. If we instead pass a block, it decides the behavior of the notification.

Let's consider a couple of use cases. In the counter app example our state looks like:

struct CounterState {
  var value: Int
}
data class Counter(val value: Int = 0)
class Counter {
  final int count;
  
  Counter(int count) {
  	this.count = count;
  }
}

We can register a listener to the store that gets notified only when the counter value is greater than 42. This can be achieved with the following.

store.addListener(forStateType: CounterState.self,
                  if: {old, new in return new.value > 42 },
                  callback: { _ in }
)
store.addListener(
        Counter::class.java,
        { oldState, newState -> newState.value > 42 }
) { state -> /* ... */ }
store.addListener(
        Counter.class, 
        (oldState, newState) -> newState.count > 42, 
        (state) -> {/* ... */});

Notice how we check for the value in the filter block. When the value is bigger than 42, we return true, which informs the listener notification block.

Checking for equality

One of the practical usages of the filter block is to ensure that the listener is called only when the state has changed i.e. the new state value is not equal to the old value.

store.addListener(forStateType: CounterState.self,
                  if: {old, new in return new.value == old.value },
                  callback: { _ in }
)
store.addListener(
        Counter::class.java,
        { oldState, newState -> newState.equals(oldState)}
) { state -> /* ... */ }
store.addListener(
        Counter.class,
        (oldState, newState) -> newState.equals(oldState),
        (state) -> {/* ... */});

Suas ships with helpers that generalize the above example:

iOS: EqualsFilter

To use this filter, the state must implement the SuasDynamicEquatable protocol. You don't have to implement SuasDynamicEquatable in your types, it is recommended that instead, you implement the swift Equatable protocol. By implementing Equatable your types are SuasDynamicEquatable ready.

Let's see an example with CounterState:

struct CounterState: Equatable, SuasDynamicEquatable {
  var value: Int

  static func ==(lhs: Counter, rhs: Counter) -> Bool {
    return lhs.value == rhs.value
  }
}

Notices that CounterState implements both Equatable and SuasDynamicEquatable but only needs to implement the == operator from Equatable (SuasDynamicEquatable has a protocol extension that implements the required methods for Equatable type).

Now that our state type is SuasDynamicEquatable we can use EqualsFilter with our listeners:

store.addListener(forStateType: CounterState.self,
                  if: equalsFilter,
                  callback: { [weak self] _ in }
)

Whenever the state changes, the equalsFilter is invoked with the old and new CounterState values. This filter detects that CounterState implements the SuasDynamicEquatable and checks for equality. The callback block is only executed if the old and new states are not equal.

🚧

iOS: Using Weak self in Listeners

If you are using self inside the listener notification, make sure to capture self weakly by using [weak self] inside the block. This is important to prevent strong memory cycles that lead to leaking.

Android: Filters.EQUALS

This filter uses the equals() method of the Object class, which must be over ridden by your custom class to behave as expected.

Let's see an example with Counter:

override fun equals(other: Any?): Boolean {
    if (other == null) {
        return false
    }

    if (!Counter::class.java.isAssignableFrom(other.javaClass)) {
        return false
    }

    return (other as Counter).value == value
}
@Override
public boolean equals(Object obj) {
    if (obj == null) {
        return false;
    }

    if (!Counter.class.isAssignableFrom(obj.getClass())) {
        return false;
    }

    return ((Counter) obj).count == count;
}

Now we can use Filters.EQUALS with our listeners:

store.addListener(
        Counter.class,
        Filters.EQUALS,
        (state) -> {/* ... */});
//We are currently working on this :)
//https://github.com/zendesk/Suas-Android/issues/33

Whenever the state changes, the Filters.EQUALS is invoked with the old and new Counter values. This filter checks for equality and the listener is only executed if the old and new states are not equal.

What's Next

More advanced topics
Using middlewares
Applications with multiple states
Async Actions
Filtering Listener