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

Learn more about Bloc

Integration of Blocs with Navigator and Compose

Last updated