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
Updated about 6 years ago