Skip to content
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

Open
ZacSweers opened this issue Nov 14, 2024 · 5 comments
Open

Add a composite presenter example #1801

ZacSweers opened this issue Nov 14, 2024 · 5 comments
Labels
samples Anything relating to sample projects

Comments

@ZacSweers
Copy link
Collaborator

Possibly productionizing the inbox tutorial sample more

@ZacSweers ZacSweers added the samples Anything relating to sample projects label Nov 14, 2024
@oreillyjf
Copy link

Just curious if you know of any other samples out there right now that include example of a composite presenter?

@fethij
Copy link

fethij commented Jan 27, 2025

@oreillyjf Were you able to find any samples?

@ZacSweers
Copy link
Collaborator Author

ZacSweers commented Feb 24, 2025

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 Presenter.Factory that creates these presenters/UIs and assisted-injects them with whatever the shared state is.

@ZacSweers
Copy link
Collaborator Author

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.

@stagg
Copy link
Collaborator

stagg commented Feb 25, 2025

composite presenters are really for separation and reusability, sharing state or intercepting events between two presenters is orthogonal to that.

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
    }
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
samples Anything relating to sample projects
Projects
None yet
Development

No branches or pull requests

4 participants