Skip to content

Instantly share code, notes, and snippets.

@marenovakovic
Last active March 6, 2026 12:10
Show Gist options
  • Select an option

  • Save marenovakovic/b47db7bb153f441e2ff8a27b731d12f1 to your computer and use it in GitHub Desktop.

Select an option

Save marenovakovic/b47db7bb153f441e2ff8a27b731d12f1 to your computer and use it in GitHub Desktop.
Circuit composite Producer/Presenter
@Inject
@CircuitInject(CitiesScreen::class, AppScope::class)
class CitiesPresenter(
@Assisted private val navigator: Navigator,
private val eventTracker: EventTracker,
private val shouldShowPaywallOnCitiesScreen: ShouldShowPaywallOnCitiesScreen,
private val cityOneStateProducer: CityStateProducer,
private val cityTwoStateProducer: CityStateProducer,
) : Presenter<State> {
@Composable
override fun present(): State {
LaunchedImpressionEffect(Unit) {
if (shouldShowPaywallOnCitiesScreen())
navigator.goTo(PaywallScreen)
}
val coroutineScope = rememberCoroutineScope()
val cityOneState = cityOneStateProducer.present()
val cityTwoState = cityTwoStateProducer.present()
val eventSink: (Event) -> Unit = { event ->
when (event) {
GoBack -> navigator.pop()
ScoreCities -> coroutineScope.launch {
if (shouldShowPaywallOnCitiesScreen())
navigator.goTo(PaywallScreen)
else {
if (cityOneState.bytes == null)
cityOneState.eventSink(MissingPhoto)
if (cityTwoState.bytes == null)
cityTwoState.eventSink(MissingPhoto)
if (cityOneState.bytes != null && cityTwoState.bytes != null) {
navigator.goTo(
ScoreScreen(
cityOnePhoto = cityOneState.bytes,
cityOneMilitaryPoints = cityOneState.militaryPoints.points,
cityTwoPhoto = cityTwoState.bytes,
cityTwoMilitaryPoints = cityTwoState.militaryPoints.points,
),
)
}
}
eventTracker.track(
SCORE_CITIES_BUTTON_CLICK,
mapOf("showPaywall" to shouldShowPaywallOnCitiesScreen()),
)
}
}
}
return State(
cityOneState = cityOneState,
cityTwoState = cityTwoState,
eventSink = eventSink,
)
}
}
// Producer that has it's own State and Events. Can be a Presenter too
@Inject
class CityStateProducer(private val eventTracker: EventTracker) {
sealed interface Event : CircuitUiEvent {
data object TakePhoto : Event
data object MissingPhoto : Event
data class PhotoTaken(val bytes: ByteArray?) : Event
data class SetMilitaryPoints(val point: MilitaryPoint) : Event
}
data class CityState(
val takePhoto: Boolean = false,
val bytes: ByteArray? = null,
val militaryPoints: MilitaryPoint = Zero,
val error: Error? = null,
val eventSink: (Event) -> Unit,
) : CircuitUiState
enum class MilitaryPoint(val points: Int) {
Zero(0),
Two(2),
Five(5),
Ten(10),
}
@Immutable
sealed interface Error {
data object MissingCityPhoto : Error
}
@Composable
fun present(): CityState {
var takePhoto by rememberRetained { mutableStateOf(false) }
var photo by remember { mutableStateOf<ByteArray?>(null) }
var militaryPoints by rememberRetained { mutableStateOf(Zero) }
var error by rememberRetained { mutableStateOf<Error?>(null) }
val eventSink: (Event) -> Unit = { event ->
when (event) {
TakePhoto -> takePhoto = true
MissingPhoto -> error = MissingCityPhoto
is PhotoTaken -> Snapshot.withMutableSnapshot {
if (event.bytes != null) {
eventTracker.track(CITY_PHOTO_TAKEN)
photo = event.bytes
takePhoto = false
error = null
}
}
is SetMilitaryPoints -> militaryPoints = event.point
}
}
return CityState(
takePhoto = takePhoto,
bytes = photo,
militaryPoints = militaryPoints,
error = error,
eventSink = eventSink,
)
}
}
// UI. Just hand over the specific city state to CityCard responsible for showcasing it
@OptIn(ExperimentalMaterial3Api::class)
@CircuitInject(CitiesScreen::class, AppScope::class)
@Composable
fun CitiesUi(
state: State,
modifier: Modifier = Modifier,
) {
Scaffold(
topBar = { ... },
floatingActionButton = { ... },
content = { paddingValues ->
Column(
verticalArrangement = Arrangement.spacedBy(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
CityCard(state.cityOneState)
CityCard(state.cityTwoState)
}
},
modifier = modifier,
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment