DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Circuit/Molecule Pattern Complete Guide — Presenter Separation/State Management

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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 }
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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")
                            }
                        }
                    )
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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 templatesGumroad

Top comments (0)