Skip to main content

Counter​

The "Hello World" example of UI frameworks is the counter app. Creating the "business logic" part of such an app is incredibly simple with Kotlin Bloc:

fun bloc(context: BlocContext) = bloc<Int, Int>(context, 1) {
reduce { state + action }
}

The view part is very simple too.

Android​

class CounterActivity : AppCompatActivity() {

// create or retrieve the lifecycle aware Bloc
private val bloc by getOrCreate { bloc(it) }
setContent {
// observe the Bloc state
val state by bloc.observeState()

// updates on state / count changes
Text("Counter: $state")

// emit events / actions to update the state / count
Button(onClick = { bloc.send(1) }, content = { Text("Increment") })
Button(onClick = { bloc.send(-1) }, content = { Text("Decrement") })
}

This is very little code considering the fact that the Bloc is lifecycle aware and will survive configuration changes.

iOS​

On iOS there's a bit more boilerplate code (BlocHolder and BlocObserver are omitted here) but it's still pretty "lean":

// iOS
struct CounterView: View {
// create the lifecycle aware Bloc
private let holder = BlocHolder { CounterKt.bloc(context: $0) }

@ObservedObject
private var model: BlocObserver<KotlinInt, KotlinInt, KotlinUnit>

init() {
// observe the Bloc state
model = BlocObserver(holder.value)
}
var body: some View {
return VStack() {
// updates on state / count changes
Text("Counter \(model.value)")

// emit events / actions to update the state / count
Button(
action: { holder.value.send(value: 1) },
label: { Text("Increment") }
)
Button(
action: { holder.value.send(value: -1) },
label: { Text("Decrement") }
)

Single-Action Reducer​

Kotlin Bloc supports different MVI/MVVM "styles" and above example shows one of many ways to implement the counter app. Here are some alternative approaches to implementing the bloc:

sealed class Action
object Increment : Action()
object Decrement : Action()

fun bloc(context: BlocContext) = bloc<Int, Action>(context, 1) {
reduce<Increment> { state + 1 }
reduce<Decrement> { state - 1 }
}

Catch-all Reducer​

fun bloc(context: BlocContext) = bloc<Int, Action>(context, 1) {
reduce {
when (action) {
Increment -> state + 1
Decrement -> state - 1
}
}
}

Enums​

enum class Action { Increment, Decrement }

fun bloc(context: BlocContext) = bloc<Int, Action>(context, 1) {
reduce(Increment) { state + 1 }
reduce(Decrement) { state - 1 }
}

MVVM+ / Orbit Style​

class CounterActivityCompose : AppCompatActivity(), BlocOwner<Int, Action, Unit, Int> {

// create or retrieve the lifecycle aware Bloc
override val bloc by getOrCreate { bloc<Int, Action>(it, 1) }

private fun increment() = reduce { state + 1 }

private fun decrement() = reduce { state - 1 }
setContent {
// observe the Bloc state
val state by bloc.observeState()

// updates on state / count changes
Text("Counter: $state")

// emit events / actions to update the state / count
Button(onClick = { increment() }, content = { Text("Increment") })
Button(onClick = { decrement() }, content = { Text("Decrement") })
}
tip

With Kotlin Bloc there's no need for an Android ViewModel which only adds unnecessary boilerplate code (see BlocOwner).

Post List​

The following (artificial) example gives a more comprehensive overview of the different Kotlin Bloc functions:

  • single-action + catch-all reducer
  • single-action and catch-all side effects
  • reducer with side effects
  • thunks
  • initializer
  • Redux and MVVM+ style
sealed class Action
object Loading : Action()
data class Loaded(val posts: List<Post>) : Action()

sealed class SideEffect
data class OpenPost(val post: Post) : SideEffect()
object PostsLoaded : SideEffect()
object NOP : SideEffect()

data class Post(
val id: Int,
val title: String,
val body: String,
)

data class State(
val loading: Boolean = false,
val posts: List<Post> = emptyList(),
)

class PostViewModel : ViewModel(), BlocOwner<State, Action, SideEffect, State> {

override val bloc = bloc<State, Action, SideEffect, State>(
blocContext(), State(false)
) {
// initializer
onCreate {
Log.i("bloc", "Bloc is starting")
}

// NOP thunk
thunk {
Log.i("bloc", "current state: ${getState()}")
dispatch(action)
}

// single-action reducer
reduce<Loading> {
state.copy(loading = true)
}

// single-action reducer with side effect
reduceAnd<Loaded> {
state.copy(loading = false, posts = state.posts) and PostsLoaded
}

// catch-all reducer with side effect
reduceAnd {
when (action) {
Loading -> state.copy(loading = true).noSideEffect()
is Loaded -> state.copy(loading = false, posts = state.posts) and PostsLoaded
}
}

// single-action side effect
sideEffect<Loaded> { PostsLoaded }

// catch-all side effect
sideEffect {
when (action) {
Loading -> NOP
is Loaded -> PostsLoaded
}
}
}

// thunks for asynchronous operations, MVVM+ style
private fun load() = thunk {
dispatch(Loading)
// load the posts asynchronously
val posts: List<Post> = repository.loadPosts()
dispatch(Loaded(posts))
}

// side effects, MVVM+ style
fun onPostClicked(post: Post) = sideEffect {
OpenPost(post)
}

}