android-bloc
android-bloc copied to clipboard
A BLoC implementation using Kotlin Coroutines
[DEPRECATED] in favor of Kotlin State Manager
android-bloc
A BLoC implementation using Kotlin Coroutines
Architecture

Why Bloc?
Bloc makes it easy to separate presentation from business logic, making your code fast, easy to test, and reusable.
When building production quality applications, managing state becomes critical.
As developers we want to:
- know what state our application is in at any point in time.
- easily test every case to make sure our app is responding appropriately.
- record every single user interaction in our application so that we can make data-driven decisions.
- work as efficiently as possible and reuse components both within our application and across other applications.
- have many developers seamlessly working within a single code base following the same patterns and conventions.
- develop fast and reactive apps.
Bloc was designed to meet all of those needs and many more.
There are many state management solutions and deciding which one to use can be a daunting task.
Bloc was designed with three core values in mind:
- Simple
- Easy to understand & can be used by developers with varying skill levels.
- Powerful
- Help make amazing, complex applications by composing them of smaller components.
- Testable
- Easily test every aspect of an application so that we can iterate with confidence.
Bloc attempts to make state changes predictable by regulating when a state change can occur and enforcing a single way to change state throughout an entire application.
Core Concepts
There are several core concepts that are critical to understanding how to use Bloc.
In the upcoming sections, we're going to discuss each of them in detail as well as work through how they would apply to a real-world application: a counter app.
Core Concepts original content
Events
Events are the input to a Bloc. They are commonly dispatched in response to user interactions such as button presses or lifecycle events like page loads.
When designing an app we need to step back and define how users will interact with it. In the context of our counter app we will have two buttons to increment and decrement our counter.
When a user taps on one of these buttons, something needs to happen to notify the "brains" of our app so that it can respond to the user's input; this is where events come into play.
We need to be able to notify our application's "brains" of both an increment and a decrement so we need to define these events.
sealed class CounterEvent {
object Decrement : CounterEvent()
object Increment : CounterEvent()
}
In this case, we can represent the events using an sealed class.
At this point we have defined our first event! Notice that we have not used Bloc in any way so far and there is no magic happening; it's just plain Kotlin code.
States
States are the output of a Bloc and represent a part of your application's state. UI components can be notified of states and redraw portions of themselves based on the current state.
So far, we've defined the two events that our app will be responding to: CounterEvent.Increment and CounterEvent.Decrement.
Now we need to define how to represent the state of our application.
Since we're building a counter, our state is very simple: it's just an integer which represents the counter's current value.
We will see more complex examples of state later on but in this case a primitive type is perfectly suitable as the state representation.
Transitions
The change from one state to another is called a Transition. A Transition consists of the current state, the event, and the next state.
As a user interacts with our counter app they will trigger Increment and Decrement events which will update the counter's state. All of these state changes can be described as a series of Transitions.
For example, if a user opened our app and tapped the increment button once we would see the following Transition.
Transition(currentState=0, event=CounterEvent$Increment@6e93bdec, nextState=1)
Because every state change is recorded, we are able to very easily instrument our applications and track all user interactions & state changes in one place. In addition, this makes things like time-travel debugging possible.
Blocs
A Bloc (Business Logic Component) is a component which converts incoming
Eventsinto aFlowof outgoingStates. Think of a Bloc as being the "brains" described above.
Every Bloc must extend the abstract class
Blocclass and inform a coroutine scope that will be used to manager events.
import 'package:bloc/bloc.dart';
class CounterBloc(eventScope: CoroutineScope) : Bloc<CounterEvent, Int>(eventScope) {
}
In the above code snippet, we are declaring our CounterBloc as a Bloc which converts CounterEvents into ints.
Why I have to inform a coroutine scope always? Because is more flexible and make ease to test.
Every Bloc must define an initial state which is the state before any events have been recieved.
In this case, we want our counter to start at 0.
override val initialState: Int = 0
Every Bloc must implement a function called
mapEventToState. The function takes the incomingeventas an argument and emit newstateswhich is consumed by the presentation layer. We can access the current bloc state at any time using thecurrentStateproperty.
override suspend fun FlowCollector<Int>.mapEventToState(event: CounterEvent) {
val nextState = when (event) {
is CounterEvent.Decrement -> currentState - 1
is CounterEvent.Increment -> currentState + 1
}
emit(nextState)
}
At this point, we have a fully functioning CounterBloc.
import br.com.programadorthi.bloc.Bloc
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.FlowCollector
class CounterBloc(eventScope: CoroutineScope) : Bloc<CounterEvent, Int>(eventScope) {
override val initialState: Int = 0
override suspend fun FlowCollector<Int>.mapEventToState(event: CounterEvent) {
val nextState = when (event) {
is CounterEvent.Decrement -> currentState - 1
is CounterEvent.Increment -> currentState + 1
}
emit(nextState)
}
}
Blocs will ignore duplicate states. If a Bloc emit
State statewherecurrentState == state, then no transition will occur and no change will be made to theFlow<State>.
At this point, you're probably wondering "How do I notify a Bloc of an event?".
Every Bloc has a
dispatchmethod.Dispatchtakes aneventand triggersmapEventToState.Dispatchmay be called from the presentation layer or from within the Bloc and notifies the Bloc of a newevent.
We can create a simple application which counts from 0 to 3.
fun main() = runBlocking {
val scope = CoroutineScope(Dispatchers.Default)
val bloc = CounterBloc(scope)
launch {
for (value in 0..2) {
delay(1000)
bloc.dispatch(CounterEvent.Increment)
}
}
scope.launch {
bloc.state.collect { value ->
println(">>>>>> value: $value")
}
}
}
The Transitions in the above code snippet would be
Transition(currentState=0, event=CounterEvent$Increment@6e93bdec, nextState=1)
Transition(currentState=1, event=CounterEvent$Increment@6e93bdec, nextState=2)
Transition(currentState=2, event=CounterEvent$Increment@6e93bdec, nextState=3)
Unfortunately, in the current state we won't be able to see any of these transitions unless we override onTransition.
onTransitionis a method that can be overridden to handle every local BlocTransition.onTransitionis called just before a Bloc'sstatehas been updated.
Tip:
onTransitionis a great place to add bloc-specific logging/analytics.
override fun onTransition(transition: Transition<CounterEvent, Int>) {
super.onTransition(transition)
println(transition)
}
Now that we've overridden onTransition we can do whatever we'd like whenever a Transition occurs.
Just like we can handle Transitions at the bloc level, we can also handle Exceptions.
onErroris a method that can be overridden to handle every local BlocException. By default all exceptions will be ignored andBlocfunctionality will be unaffected.
Tip:
onErroris a great place to add bloc-specific error handling.
override fun onError(cause: Throwable) {
super.onError(cause)
println(cause)
// send cause to crashlytics
}
Now that we've overridden onError we can do whatever we'd like whenever an Exception is thrown.
You can check when an Event was dispatched if you override onEvent.
onEventis a method that can be overridden to handle every local BlocEvent.onEventis called just before the dispatched event to be processed;
Tip:
onEventis a great place to add bloc-specific logging/analytics.
override fun onEvent(event: CounterEvent) {
super.onEvent(event)
println(event)
// send statistics to analytics
}
Now that we've overridden onEvent we can do whatever we'd like whenever a Event occurs.
If you would like to avoid an Event to be processed you can override computeEvent.
computeEventis a method that can be overridden to avoid anEventto be processed.computeEventis called afteronEventand just before the dispatched event to be processed. Default behavior always returnstrue.
Tip:
computeEventis a great place to add a custom logic to process anEvent.
override suspend fun computeEvent(event: CounterEvent): Boolean {
// Only Decrement events will be processed
return when (event) {
is CounterEvent.Decrement -> true
// Avoiding Increment events to be processed
is CounterEvent.Increment -> false
}
}
Now that we've overridden computeEvent we can do whatever we'd like whenever a Event occurs.
There is a
computeStateversion that can be used to avoidStateto be emitted
If you would like to transform an Event in another Event you can override transformEvent.
transformEventis a method that can be overridden to transform anEventin anotherEvent.transformEventis called aftercomputeEvent. Default behavior always returns the dispatched event.
Tip:
transformEventis a great place to add a custom logic to redirect anEventto anotherEvent.
override suspend fun transformEvent(event: CounterEvent): CounterEvent {
// Making the user crazy.
// When he clicks decrement, we increment :p
// When he clicks increment, we decrement. :p
return when (event) {
is CounterEvent.Decrement -> CounterEvent.Increment
is CounterEvent.Increment -> CounterEvent.Decrement
}
}
Now that we've overridden transformEvent we can do whatever we'd like whenever a Event occurs.
There is a
transformStateversion that can be used to convert aStatein anotherState
BlocInterceptor
One added bonus of using Bloc is that we can have access to all Transitions in one place. Even though in this application we only have one Bloc, it's fairly common in larger applications to have many Blocs managing different parts of the application's state.
If we want to be able to do something in response to all Transitions we can simply create our own BlocInterceptor.
class MainBloc : BlocInterceptor {
override fun <Event, State> onTransition(transition: Transition<Event, State>) {
Logger.d(">>>>> Global onTransition: $transition")
}
}
Note: All we need to do is extend
BlocInterceptorand override theonTransitionmethod.
In order to tell Bloc to use our MainBloc, we just need to tweak our main function.
fun main() = runBlocking {
BlocInterceptor.initBlocInterceptor(MainBloc())
val scope = CoroutineScope(Dispatchers.Default)
val bloc = CounterBloc(scope)
launch {
for (value in 0..2) {
delay(1000)
bloc.dispatch(CounterEvent.Increment)
}
}
scope.launch {
bloc.state.collect { value ->
println(">>>>>> value: $value")
}
}
}
If we want to be able to do something in response to all Events dispatched, we can also override the onEvent method in our MainBloc.
class MainBloc : BlocInterceptor {
override fun <Event> onEvent(event: Event) {
Logger.i(">>>>> MainBloc onEvent: $event")
}
override fun <Event, State> onTransition(transition: Transition<Event, State>) {
Logger.d(">>>>> MainBloc onTransition: $transition")
}
}
If we want to be able to do something in response to all Exceptions thrown in a Bloc, we can also override the onError method in our MainBloc.
class MainBloc : BlocInterceptor {
override fun onError(cause: Throwable) {
Logger.e(cause, ">>>>> MainBloc onError")
}
override fun <Event> onEvent(event: Event) {
Logger.i(">>>>> MainBloc onEvent: $event")
}
override fun <Event, State> onTransition(transition: Transition<Event, State>) {
Logger.d(">>>>> MainBloc onTransition: $transition")
}
}
Note:
BlocInterceptoris a singleton which oversees all Blocs and delegates responsibilities to theBlocInterceptor.
Credits
- Bloc - a predictable state management library for Dart that was used as base to this project.
- Norris - for the project structure and inspiration using Kotlin Coroutines.
- Jetbrains - for the amazing developer experience around Kotlin and Coroutines
Author
Thiago Santos (follow me on Twitter)