-
Notifications
You must be signed in to change notification settings - Fork 83
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add a composite presenter example #1801
Comments
Just curious if you know of any other samples out there right now that include example of a composite presenter? |
@oreillyjf Were you able to find any samples? |
Here's something I whipped up in slack conversation with someone, in case it's helpful. // Combined state for the composite screen
data object UserDashboardScreen : Screen {
data class State(
val profile: ProfileScreen.State,
val settings: SettingsScreen.State,
val isRefreshing: Boolean = false,
val eventSink: (Event) -> Unit,
) : CircuitUiState
sealed interface Event {
data object RefreshDashboard : Event
}
}
// Child screens
data object ProfileScreen : Screen {
data class State(
val username: String,
val bio: String,
val isLoading: Boolean = false,
val error: String? = null,
val eventSink: (Event) -> Unit,
) : CircuitUiState
sealed interface Event {
data class UpdateBio(val newBio: String) : Event
data object RefreshProfile : Event
}
}
data object SettingsScreen : Screen {
data class State(
val isDarkMode: Boolean,
val notificationsEnabled: Boolean,
val isLoading: Boolean = false,
val error: String? = null,
val eventSink: (Event) -> Unit,
) : CircuitUiState
sealed interface Event {
data class ToggleDarkMode(val enabled: Boolean) : Event
data class ToggleNotifications(val enabled: Boolean) : Event
}
}
class ProfilePresenter(private val userId: String, private val userRepository: UserRepository) :
Presenter<ProfileScreen.State> {
@Composable
override fun present(): ProfileScreen.State {
val userFlow = remember { userRepository.userFlow(userId) }
val user by userFlow.collectAsState()
return ProfileScreen.State(username = user.username, bio = user.bio) { event ->
// handle event
}
}
}
class SettingsPresenter(private val settingsRepository: SettingsRepository) :
Presenter<SettingsScreen.State> {
@Composable
override fun present(): SettingsScreen.State {
val settingsFlow = remember { settingsRepository.settingsFlow() }
val settings by settingsFlow.collectAsState()
return SettingsScreen.State(
isDarkMode = settings.isDarkMode,
notificationsEnabled = settings.notificationsEnabled,
) { event ->
// TODO handle settings event
}
}
}
// Composite presenter that combines both child presenters
class UserDashboardPresenter(
private val userId: String,
private val userRepository: UserRepository,
private val settingsRepository: SettingsRepository,
) : Presenter<UserDashboardScreen.State> {
@Composable
override fun present(): UserDashboardScreen.State {
val profilePresenter = remember { ProfilePresenter(userId, userRepository) }
val settingsPresenter = remember { SettingsPresenter(settingsRepository) }
val profileState = profilePresenter.present()
val settingsState = settingsPresenter.present()
return UserDashboardScreen.State(profile = profileState, settings = settingsState) { event ->
when (event) {
is UserDashboardScreen.Event.RefreshDashboard -> refreshDashboard()
}
}
}
private fun refreshDashboard() {
userRepository.refreshUser(userId)
settingsRepository.refreshSettings()
}
}
@Composable
fun UserDashboard(state: UserDashboardScreen.State, modifier: Modifier = Modifier) {
PullToRefreshBox(
isRefreshing = state.isRefreshing,
onRefresh = { state.eventSink(UserDashboardScreen.Event.RefreshDashboard) },
modifier = modifier,
) {
Column {
Profile(state.profile)
Settings(state.settings)
}
}
}
@Composable
fun Profile(state: ProfileScreen.State, modifier: Modifier = Modifier) {
// ...
}
@Composable
fun Settings(state: SettingsScreen.State, modifier: Modifier = Modifier) {
// ...
} how you get those child presenters into there is sorta up to you. Can inject them, new them up yourself, pull them from a Circuit instance, etc. The ideal scenario is that shared state can be derived from a shared data layer. So if you have a refresh triggered by some bit of your UI, some other UI listening to it updates. In the example above - hitting refresh in the dashboard UI triggered an update to the underlying data layer that those child presenters would be actively observing. If you want them to share runtime state that can't fit into a data layer, the best way is to hoist that control up into a |
I think an important thing to point out is that composite presenters are really for separation and reusability, sharing state or intercepting events between two presenters is orthogonal to that. For that latter case, we often actually just spin off small, non-Presenter-implementing UseCase classes that just do one thing. |
To add, we've been calling these "state producers" to avoid any confusion that they can be used as a normal circuit presenter. Most of the time they are producing a circuit state that the parent just passes along. Example: class EmailStateProducer(
private val emailUseCase: EmailUseCase
) {
@Composable
override fun produce(emailId: String): EmailState {
val email = produceState(null) {
emailUseCase(emailId).collect { value = it }
}
return EmailState(email)
}
}
class InboxPresenter(
private val screen: InboxScreen,
private val emailStateProducer: EmailStateProducer
): Presenter<InboxState> {
@Composable
override fun present(): InboxState {
val emailState = emailStateProducer.produce(screen.emailId)
// ... Other states
return InboxState(emailState, ...) { event ->
// Inbox event sink
}
}
} |
Possibly productionizing the inbox tutorial sample more
The text was updated successfully, but these errors were encountered: