Overview
What is it?
A Kotlin multiplatform state management and navigation library for Compose, based on a fork of Voyager by Adriel Cafe, and a port of flutter_bloc by Felix Angelov.
Show me some code
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//standard setContent to define UI for an activity with Compose
setContent {
//use RootNavigator to initialize compose_bloc navigator for activity
RootNavigator(MainScreen(userName="Albert Einstein"))
}
}
}
// A Screen is what the name suggests, and is the basic UI entity to which you can
// navigate to with a Navigator. Entities associated with a Screen like a ScreenModel
// or Bloc are automatically disposed when a Screen is disposed, for example when we
// receive an "onBackPressed" event in a Screen.
// Note that a Screen must be Serializable, so that its fields, that you can think as
// it "arguments" can be saved and restored when an Activity is paused and then restarted
// or when screen is rotated.
class MainScreen(val userName:String): Screen {
@Composable
override fun Content() {
// the current navigator, in this case it is the root navigator.
// It is possible to define nested navigators
val navigator= LocalNavigator.currentOrThrow
Compose_blocTheme {
Scaffold(
topBar = { TopAppBar{ Text("Counter test app") } },
backgroundColor = MaterialTheme.colors.background,
Column {
Text("Hello $userName")
// when the user click the button we navigate to a new screen
Button(onClick={ navigator.push(CounterScreen())})
{ Text("click to open Counter Screen") }
}
}
)
}
}
}
// The idea behind the Bloc architecture, very similar to Redux, is that we
// don't modify directly the state of the application (the state associated to the Bloc)
// but instead we send events ("actions" in Redux) that are processed
// by the Bloc registered event handlers
// Here is the definition of the events CounterBloc can handle, all events
// must inherit to some base event type that the bloc is supposed to handle
interface CounterEvent
class AdditionEvent(val value:Int):CounterEvent
class SubtractionEvent(val value:Int):CounterEvent
// this is the state of the bloc. It should be an immutable object, that we don't
// update in-place. instead we create a new instance with the modified values.
// a data class is perfect for this purpose
data class CounterState(val counter:Int=0)
// here we define the Bloc itself. The bloc wrap together
// - some application state (CounterState)
// - associated event handlers to handle changes to the application state
// - a kotlin Flow with the current value of the bloc state, that we can transform to
// compose MutableState and listen to, for automatically updating the UI
// on state changes
class CounterBloc(cscope: CoroutineScope, startCounter:Int=0):
Bloc<CounterEvent, CounterState>(
// every bloc has an associated coroutineScope that is automatically cancelled
// when the bloc is disposed.
cscope, CounterState(startCounter),false)
{
// in the bloc constructor we define the bloc event handlers
init {
// each event handler define a function that is called when
// an event of the specified type is received. the function
// has two arguments
// - the received event
// - an "emit" method to call to emit the updated state according to
// the received event.
// Note that unlike Redux reducers, event handler are not necessarily pure
// function without side-effects. On the contrary there can be event handlers
// whose only purpose are their side effects, and that do not emit a new state
// at all. Use the coroutine scope associated to the bloc to run the side effects
on<AdditionEvent> { event, emit ->
val s=state
emit(s.copy(counter =s.counter+event.value ))
}
on<SubtractionEvent> { event, emit ->
val s=state
emit(s.copy(counter =s.counter-event.value ))
}
}
}
class CounterScreen: Screen {
@Composable
override fun Content() {
Column(modifier=Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally) {
// out of the BlocProvider composable subtree the bloc is not available
val bnull= rememberProvidedBloc<CounterBloc>()
Log.e(LOGTAG,"obtained bnull counter bloc: $bnull") //this must be null
// BlocProvider makes available the specified bloc (CounterBloc)
// to the associated composable subtree
BlocProvider({cscope-> CounterBloc(cscope,1)} ) {
//rememberProvidedBlocOf is similar to dependency injection:
// it retrieves the specified bloc type as defined by the closest
// enclosing BlocProvider
val b= rememberProvidedBloc<CounterBloc>()?:return@BlocProvider
// define some callbacks to wrap sending events to the bloc so
// that the actual UI does need to know anything about the bloc
val onIncrement = { b.add(AdditionEvent(1)) }
val onDecrement = { b.add(SubtractionEvent(1) }
//BlocBuilder search for the specified bloc type as defined by
// the closest enclosing blocProvider and subscribes to its states
// updates, as a Composable mutableState that when changes trigger
// recomposition
BlocBuilder(b) { counterState->
//this is the actual ui composable
CounterControls(
"Counter display updated always",
counterState.counter,
onDecrement, onIncrement)
}
}
// out of the BlocProvider composable subtree the bloc is not available
val bnull2:CounterBloc = rememberProvidedBloc()
Log.e(LOGTAG,"obtained bnull2 counter bloc: $bnull2") //this must be null
}
}
}
@Composable
fun CounterControls(
explanatoryText:String,
counterValue: Int,
onDecrement: () -> Unit,
onIncrement: () -> Unit
) {
Text(explanatoryText)
Text("Counter value: ${counterValue}")
Row {
Button(
onClick = onDecrement,
modifier = Modifier.padding(horizontal = 8.dp)
) { Text(text = "-") }
Button(
onClick = onIncrement,
modifier = Modifier.padding(horizontal = 8.dp)
) { Text(text = "+") }
}
}
Learn more about Navigator
Navigator OverviewLearn more about Bloc
Bloc and Cubit OverviewIntegration of Blocs with Navigator and Compose
Blocs and Compose OverviewLast updated