What You'll Learn
Circuit/Molecule pattern (Presenter separation, UI event-driven, state machine, improved testability) explained.
What is Circuit Pattern?
Architecture developed by Slack. Maximizes testability through clear separation of Screen/Presenter/UI.
Basic Structure
// Screen: screen definition
@Parcelize
data object HomeScreen : Screen {
data class State(
val items: List<Item> = emptyList(),
val isLoading: Boolean = false,
val eventSink: (Event) -> Unit = {}
) : CircuitUiState
sealed interface Event : CircuitUiEvent {
data object Refresh : Event
data class ItemClick(val itemId: String) : Event
data class Delete(val itemId: String) : Event
}
}
Presenter
class HomePresenter @Inject constructor(
private val repository: ItemRepository,
private val navigator: Navigator
) : Presenter<HomeScreen.State> {
@Composable
override fun present(): HomeScreen.State {
var items by remember { mutableStateOf<List<Item>>(emptyList()) }
var isLoading by remember { mutableStateOf(true) }
LaunchedEffect(Unit) {
repository.getItems().collect {
items = it
isLoading = false
}
}
return HomeScreen.State(
items = items,
isLoading = isLoading
) { event ->
when (event) {
HomeScreen.Event.Refresh -> {
isLoading = true
// Refresh logic
}
is HomeScreen.Event.ItemClick -> {
navigator.goTo(DetailScreen(event.itemId))
}
is HomeScreen.Event.Delete -> {
items = items.filter { it.id != event.itemId }
}
}
}
}
}
UI
@CircuitInject(HomeScreen::class, AppScope::class)
@Composable
fun HomeUi(state: HomeScreen.State, modifier: Modifier = Modifier) {
Scaffold(modifier) { padding ->
if (state.isLoading) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
} else {
LazyColumn(contentPadding = padding) {
items(state.items, key = { it.id }) { item ->
ListItem(
headlineContent = { Text(item.title) },
modifier = Modifier.clickable {
state.eventSink(HomeScreen.Event.ItemClick(item.id))
},
trailingContent = {
IconButton(onClick = {
state.eventSink(HomeScreen.Event.Delete(item.id))
}) {
Icon(Icons.Default.Delete, "Delete")
}
}
)
}
}
}
}
}
Molecule Pattern (Simplified)
// Molecule: Composable function generates State
@Composable
fun counterPresenter(events: Flow<CounterEvent>): CounterState {
var count by remember { mutableIntStateOf(0) }
LaunchedEffect(Unit) {
events.collect { event ->
when (event) {
CounterEvent.Increment -> count++
CounterEvent.Decrement -> count--
CounterEvent.Reset -> count = 0
}
}
}
return CounterState(count = count)
}
data class CounterState(val count: Int)
sealed interface CounterEvent {
data object Increment : CounterEvent
data object Decrement : CounterEvent
data object Reset : CounterEvent
}
Testing
@Test
fun presenterEmitsCorrectState() = runTest {
val fakeRepository = FakeItemRepository(listOf(testItem1, testItem2))
val fakeNavigator = FakeNavigator()
val presenter = HomePresenter(fakeRepository, fakeNavigator)
moleculeFlow(RecompositionMode.Immediate) { presenter.present() }.test {
val initialState = awaitItem()
assertTrue(initialState.isLoading)
val loadedState = awaitItem()
assertFalse(loadedState.isLoading)
assertEquals(2, loadedState.items.size)
loadedState.eventSink(HomeScreen.Event.Delete(testItem1.id))
val afterDelete = awaitItem()
assertEquals(1, afterDelete.items.size)
}
}
Summary
| Element | Role |
|---|---|
| Screen | Screen definition + State + Event |
| Presenter | State management logic |
| UI | Pure render function |
| eventSink | UI→Presenter event |
- Clear responsibility separation: Screen/Presenter/UI
- UI takes State only (easy test substitution)
- eventSink pattern unified event management
- Molecule makes presenter function testable
Ready-Made Android App Templates
8 production-ready Android app templates with Jetpack Compose, MVVM, Hilt, and Material 3.
Browse templates → Gumroad
Top comments (0)