Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Getting Molecular Londroid

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest. →
Avatar for Jack Adams Jack Adams
July 24, 2025
12

Getting Molecular Londroid

Avatar for Jack Adams

Jack Adams

July 24, 2025

Transcript

  1. • Senior Android Engineer @ Trainline • Android dev for

    ~5 years • Selling train tickets for ~3.5 years • Passionate about building scalable app architectures • Outside of work I like space exploration, chess, Man City, and UFC A bit about me
  2. • Senior Android Engineer @ Trainline • Android dev for

    ~4 years • Selling train tickets for ~2.5 years • Passionate about building scalable app architectures • Outside of work I like space exploration, chess, Man City, and UFC A bit about me
  3. What am I going to talk about Molecule of course!

    • What is Molecule? • A brief intro to writing Molecule code • How Molecule fits into our traditional Android presentation architectures • What real-world problems does it solve? • Simplifying the logic expansion of the ViewModel • Combining flows with compose-style declarative code • Building reusable state management for reusable UI components
  4. Jake Wharton - Author of Molecule (and many other libraries

    you already use) “It [Molecule] allows you to use the compiler magic of Compose for the pure manipulation of data… just aggregating inputs and emitting outputs.”
  5. Molecule is a simple but powerful library allowing you to

    create StateFlow streams using Compose
  6. Molecule is a simple but powerful library allowing you to

    create StateFlow streams using Compose compose-runtime, not compose-ui
  7. More like 4 But this is a bit of a

    simplification: UI Presentation Domain (semi-optional) Data
  8. But if we don’t take care it can lead to

    a blurring of the layers Compose is amazing!
  9. @Composable fun Counter(modifier: Modifier = Modifier) { var count: Int

    by remember { mutableIntStateOf(0) } Text( text = "$count", style = MaterialTheme.typography.titleLarge, color = Color(0xFFCB2A7B), fontSize = 144.sp, modifier = modifier ) LaunchedEffect(Unit) { while (true) { delay(1000) count++ } } } Let’s look at the most simple example Imagine we want to count seconds
  10. Let’s look at the most simple example Imagine we want

    to count seconds class CounterPresenter @Inject constructor() { val counterState: StateFlow<Int> = … } { 1 // (1 second) 2 // (1 second) 3 // (1 second) 4 // (1 second) 5 // etc. }
  11. Let’s look at the most simple example Imagine we want

    to count seconds class CounterPresenter @Inject constructor() { val counterState: StateFlow<Int> = flow { var count = 0 while (true) { emit(count) delay(1000) count++ } }.stateIn( scope = viewModelScope, started = SharingStarted.Eagerly, initialValue = 0 ) } { 1 // (1 second) 2 // (1 second) 3 // (1 second) 4 // (1 second) 5 // etc. }
  12. Let’s look at the most simple example Imagine we want

    to count seconds @Composable fun counter(start: Int): Int { var count: Int by remember { mutableIntStateOf(start) } LaunchedEffect(Unit) { while (true) { delay(1000) count++ } } return count } class CounterPresenter @Inject constructor() { val counterState: StateFlow<Int> = CoroutineScope().launchMolecule(Immediate) { counter(start = 0) } } { 1 // (1 second) 2 // (1 second) 3 // (1 second) 4 // (1 second) 5 // etc. }
  13. val count by viewModel.counterState.collectAsState() CounterContent(count, viewModel::process) @Composable fun CounterText(count: Int,

    modifier: Modifier = Modifier) { Text( text = "$count", style = MaterialTheme.typography.titleLarge, color = Color(0xFFCB2A7B), fontSize = 144.sp, modifier = modifier ) } @Composable fun counter(start: Int): Int { var count: Int by remember { mutableIntStateOf(start) } LaunchedEffect(Unit) { while (true) { delay(1000) count++ } } return count } class CounterViewModel @Inject constructor() : ViewModel(){ val counterState: StateFlow<Int> = viewModelScope.launchMolecule(Immediate) { counter(start = 0) } } UI Presentation
  14. Another example val userFlow = db.userObservable() val balanceFlow = db.balanceObservable()

    @Composable fun Profile() { val user by userFlow.collectAsState(null) val balance by balanceFlow.collectAsState(0L) if (user == null) { Text("Loading…") } else { Text("${user.name} - $balance") } }
  15. Another example val userFlow = db.userObservable() val balanceFlow = db.balanceObservable()

    @Composable fun Profile() { val user by userFlow.collectAsState(null) val balance by balanceFlow.collectAsState(0L) if (user == null) { Text("Loading…") } else { Text("${user.name} - $balance") } }
  16. Another example val userFlow = db.userObservable() val balanceFlow = db.balanceObservable()

    @Composable fun Profile() { val user by userFlow.collectAsState(null) val balance by balanceFlow.collectAsState(0L) if (user == null) { Text("Loading…") } else { Text("${user.name} - $balance") } }
  17. Another example sealed interface ProfileModel { object Loading : ProfileModel

    data class Data( val name: String, val balance: Long, ) : ProfileModel } class ProfilePresenter( private val db: ExampleDB, ) { fun transform(): Flow<ProfileModel> { return combine( db.userObservable().onStart { emit(null) }, db.balanceObservable().onStart { emit(0L) }, ) { user, balance -> if (user == null) { ProfileModel.Loading } else { ProfileModel.Data(user.name, balance) } } } }
  18. Another example sealed interface ProfileModel { object Loading : ProfileModel

    data class Data( val name: String, val balance: Long, ) : ProfileModel } class ProfilePresenter( private val db: ExampleDB, ) { fun transform(): Flow<ProfileModel> { return combine( db.userObservable().onStart { emit(null) }, db.balanceObservable().onStart { emit(0L) }, ) { user, balance -> if (user == null) { ProfileModel.Loading } else { ProfileModel.Data(user.name, balance) } } } }
  19. Another example sealed interface ProfileModel { object Loading : ProfileModel

    data class Data( val name: String, val balance: Long, ) : ProfileModel } class ProfilePresenter( private val db: ExampleDB, ) { fun transform(): Flow<ProfileModel> { return combine( db.userObservable().onStart { emit(null) }, db.balanceObservable().onStart { emit(0L) }, ) { user, balance -> if (user == null) { ProfileModel.Loading } else { ProfileModel.Data(user.name, balance) } } } }
  20. Another example sealed interface ProfileModel { object Loading : ProfileModel

    data class Data( val name: String, val balance: Long, ) : ProfileModel } class ProfilePresenter( private val db: ExampleDB, ) { fun transform(): Flow<ProfileModel> { return combine( db.userObservable().onStart { emit(null) }, db.balanceObservable().onStart { emit(0L) }, ) { user, balance -> if (user == null) { ProfileModel.Loading } else { ProfileModel.Data(user.name, balance) } } } }
  21. @Composable fun profilePresenter( userFlow: Flow<User?>, balanceFlow: Flow<Float>, ): ProfileModel {

    val user by userFlow.collectAsState(null) val balance by balanceFlow.collectAsState(0L) return if (user == null) { ProfileModel.Loading } else { ProfileModel.Data(user.name, balance) } } val userFlow = db.userObservable() val balanceFlow = db.balanceObservable() val models: StateFlow<ProfileModel> = scope.launchMolecule(mode = ContextClock) { profilePresenter(userFlow, balanceFlow) } Another example
  22. @Composable fun profilePresenter( userFlow: Flow<User?>, balanceFlow: Flow<Float>, ): ProfileModel {

    val user by userFlow.collectAsState(null) val balance by balanceFlow.collectAsState(0L) return if (user == null) { ProfileModel.Loading } else { ProfileModel.Data(user.name, balance) } } val userFlow = db.userObservable() val balanceFlow = db.balanceObservable() val models: StateFlow<ProfileModel> = scope.launchMolecule(mode = ContextClock) { profilePresenter(userFlow, balanceFlow) } Another example
  23. @Composable fun profilePresenter( userFlow: Flow<User?>, balanceFlow: Flow<Float>, ): ProfileModel {

    val user by userFlow.collectAsState(null) val balance by balanceFlow.collectAsState(0L) return if (user == null) { ProfileModel.Loading } else { ProfileModel.Data(user.name, balance) } } val userFlow = db.userObservable() val balanceFlow = db.balanceObservable() val models: StateFlow<ProfileModel> = scope.launchMolecule(mode = ContextClock) { profilePresenter(userFlow, balanceFlow) } Another example
  24. @Composable fun profilePresenter( userFlow: Flow<User?>, balanceFlow: Flow<Float>, ): ProfileModel {

    val user by userFlow.collectAsState(null) val balance by balanceFlow.collectAsState(0L) return if (user == null) { ProfileModel.Loading } else { ProfileModel.Data(user.name, balance) } } val userFlow = db.userObservable() val balanceFlow = db.balanceObservable() val models: StateFlow<ProfileModel> = scope.launchMolecule(mode = ContextClock) { profilePresenter(userFlow, balanceFlow) } Another example
  25. Molecule allows us to unlock the power of compose here

    UI Presentation Domain (semi-optional) Data
  26. The CashApp/Molecule style ViewModel https://github.com/cashapp/molecule/blob/trunk/sample-viewmodel/src/main/java/com/example/molecule/viewmodel/ MoleculeViewModel.kt abstract class BaseViewModel<State, UIEvent,

    NavEffect> : ViewModel() { private val scope = CoroutineScope(viewModelScope.coroutineContext + AndroidUiDispatcher.Main) private val events = MutableSharedFlow<UIEvent>() val state: StateFlow<State> by lazy(LazyThreadSafetyMode.NONE) { scope.launchMolecule(mode = RecompositionMode.ContextClock) { state(events) } } @Composable protected abstract fun state(events: Flow<UIEvent>): State fun process(event: UIEvent) { viewModelScope.launch { events.emit(event) } } private val _navEffects = MutableSharedFlow<NavEffect>() val navEffects: SharedFlow<NavEffect> = _navEffects.asSharedFlow() protected fun navigate(effect: NavEffect) { viewModelScope.launch { _navEffects.emit(effect) } } }
  27. The CashApp/Molecule style ViewModel https://github.com/cashapp/molecule/blob/trunk/sample-viewmodel/src/main/java/com/example/molecule/viewmodel/ MoleculeViewModel.kt abstract class BaseViewModel<State, UIEvent,

    NavEffect> : ViewModel() { private val scope = CoroutineScope(viewModelScope.coroutineContext + AndroidUiDispatcher.Main) private val events = MutableSharedFlow<UIEvent>() val state: StateFlow<State> by lazy(LazyThreadSafetyMode.NONE) { scope.launchMolecule(mode = RecompositionMode.ContextClock) { state(events) } } @Composable protected abstract fun state(events: Flow<UIEvent>): State fun process(event: UIEvent) { viewModelScope.launch { events.emit(event) } } private val _navEffects = MutableSharedFlow<NavEffect>() val navEffects: SharedFlow<NavEffect> = _navEffects.asSharedFlow() protected fun navigate(effect: NavEffect) { viewModelScope.launch { _navEffects.emit(effect) } } }
  28. The CashApp/Molecule style ViewModel https://github.com/cashapp/molecule/blob/trunk/sample-viewmodel/src/main/java/com/example/molecule/viewmodel/ MoleculeViewModel.kt abstract class BaseViewModel<State, UIEvent,

    NavEffect> : ViewModel() { private val scope = CoroutineScope(viewModelScope.coroutineContext + AndroidUiDispatcher.Main) private val events = MutableSharedFlow<UIEvent>() val state: StateFlow<State> by lazy(LazyThreadSafetyMode.NONE) { scope.launchMolecule(mode = RecompositionMode.ContextClock) { state(events) } } @Composable protected abstract fun state(events: Flow<UIEvent>): State fun process(event: UIEvent) { viewModelScope.launch { events.emit(event) } } private val _navEffects = MutableSharedFlow<NavEffect>() val navEffects: SharedFlow<NavEffect> = _navEffects.asSharedFlow() protected fun navigate(effect: NavEffect) { viewModelScope.launch { _navEffects.emit(effect) } } }
  29. The CashApp/Molecule style ViewModel https://github.com/cashapp/molecule/blob/trunk/sample-viewmodel/src/main/java/com/example/molecule/viewmodel/ MoleculeViewModel.kt abstract class BaseViewModel<State, UIEvent,

    NavEffect> : ViewModel() { private val scope = CoroutineScope(viewModelScope.coroutineContext + AndroidUiDispatcher.Main) private val events = MutableSharedFlow<UIEvent>() val state: StateFlow<State> by lazy(LazyThreadSafetyMode.NONE) { scope.launchMolecule(mode = RecompositionMode.ContextClock) { state(events) } } @Composable protected abstract fun state(events: Flow<UIEvent>): State fun process(event: UIEvent) { viewModelScope.launch { events.emit(event) } } private val _navEffects = MutableSharedFlow<NavEffect>() val navEffects: SharedFlow<NavEffect> = _navEffects.asSharedFlow() protected fun navigate(effect: NavEffect) { viewModelScope.launch { _navEffects.emit(effect) } } }
  30. abstract class BaseViewModel<State, NavEffect>( initialState: State ) : ViewModel() {

    private val _state = MutableStateFlow(initialState) val state: StateFlow<State> = _state.asStateFlow() private val _navEffects = MutableSharedFlow<NavEffect>() val navEffects: SharedFlow<NavEffect> = _navEffects.asSharedFlow() protected fun setState(reducer: State.() -> State) { _state.update(reducer) } protected fun navigate(effect: NavEffect) { viewModelScope.launch { _navEffects.emit(effect) } } } The non-molecule BaseViewModel
  31. abstract class BaseViewModel<State, NavEffect>( initialState: State ) : ViewModel() {

    private val _state = MutableStateFlow(initialState) val state: StateFlow<State> = _state.asStateFlow() private val _navEffects = MutableSharedFlow<NavEffect>() val navEffects: SharedFlow<NavEffect> = _navEffects.asSharedFlow() protected fun setState(reducer: State.() -> State) { _state.update(reducer) } protected fun navigate(effect: NavEffect) { viewModelScope.launch { _navEffects.emit(effect) } } } The non-molecule BaseViewModel
  32. abstract class BaseViewModel<State, NavEffect>( initialState: State ) : ViewModel() {

    private val _state = MutableStateFlow(initialState) val state: StateFlow<State> = _state.asStateFlow() private val _navEffects = MutableSharedFlow<NavEffect>() val navEffects: SharedFlow<NavEffect> = _navEffects.asSharedFlow() protected fun setState(reducer: State.() -> State) { _state.update(reducer) } protected fun navigate(effect: NavEffect) { viewModelScope.launch { _navEffects.emit(effect) } } } The non-molecule BaseViewModel
  33. List Screen Without Molecule class PlayersListViewModel @Inject constructor( private val

    getPlayersByTitle: GetPlayersByTitleUseCase ) : BaseViewModel<List<String>, PlayerListNavigationEffects>( initialState = emptyList() ) { init { viewModelScope.launch { withContext(Dispatchers.IO) { runCatching { getPlayersByTitle(PlayerTitle.GRAND_MASTER) }.getOrDefault(emptyList()) .let { newList -> setState { newList } } } } } fun onItemClicked(index: Int) { navigate(LaunchDetails(state.value[index])) } fun onBackClicked() { navigate(NavigateBack) } }
  34. List Screen Without Molecule class PlayersListViewModel @Inject constructor( private val

    getPlayersByTitle: GetPlayersByTitleUseCase ) : BaseViewModel<List<String>, PlayerListNavigationEffects>( initialState = emptyList() ) { init { viewModelScope.launch { withContext(Dispatchers.IO) { runCatching { getPlayersByTitle(PlayerTitle.GRAND_MASTER) }.getOrDefault(emptyList()) .let { newList -> setState { newList } } } } } fun onItemClicked(index: Int) { navigate(LaunchDetails(state.value[index])) } fun onBackClicked() { navigate(NavigateBack) } }
  35. List Screen Molecule class PlayerListPresenter @Inject constructor( private val getPlayersByTitle:

    GetPlayersByTitleUseCase ) { @Composable fun present(): List<String> { var players: List<String> by remember { mutableStateOf(emptyList()) } LaunchedEffect(key1 = Unit) { try { players = getPlayersByTitle(PlayerTitle.GRAND_MASTER) } catch (e: Exception) { Log.d("error", e.message.orEmpty()) } } return players } }
  36. List Screen Molecule class PlayerListPresenter @Inject constructor( private val getPlayersByTitle:

    GetPlayersByTitleUseCase ) { @Composable fun present(): List<String> { var players: List<String> by remember { mutableStateOf(emptyList()) } LaunchedEffect(key1 = Unit) { try { players = getPlayersByTitle(PlayerTitle.GRAND_MASTER) } catch (e: Exception) { Log.d("error", e.message.orEmpty()) } } return players } }
  37. List Screen Molecule class PlayerListPresenter @Inject constructor( private val getPlayersByTitle:

    GetPlayersByTitleUseCase ) { @Composable fun present(): List<String> { var players: List<String> by remember { mutableStateOf(emptyList()) } LaunchedEffect(key1 = Unit) { try { players = getPlayersByTitle(PlayerTitle.GRAND_MASTER) } catch (e: Exception) { Log.d("error", e.message.orEmpty()) } } return players } }
  38. List Screen Molecule class PlayersListViewModel @Inject constructor( private val listPresenter:

    PlayerListPresenter ) : BaseViewModel<List<String>, PlayerListUiEvents, PlayerListNavigationEffects>() { @Composable override fun state(events: Flow<PlayerListUiEvents>): List<String> { LaunchedEffect(key1 = Unit) { events.collectLatest { event -> when (event) { OnBackClicked -> navigate(NavigateBack) is PlayerSelected -> navigate(LaunchDetails(state.value[event.index])) } } } return listPresenter.present() } }
  39. List Screen Molecule class PlayersListViewModel @Inject constructor( private val listPresenter:

    PlayerListPresenter ) : BaseViewModel<List<String>, PlayerListUiEvents, PlayerListNavigationEffects>() { @Composable override fun state(events: Flow<PlayerListUiEvents>): List<String> { LaunchedEffect(key1 = Unit) { events.collectLatest { event -> when (event) { OnBackClicked -> navigate(NavigateBack) is PlayerSelected -> navigate(LaunchDetails(state.value[event.index])) } } } return listPresenter.present() } }
  40. List Screen Molecule class PlayersListViewModel @Inject constructor( private val listPresenter:

    PlayerListPresenter ) : BaseViewModel<List<String>, PlayerListUiEvents, PlayerListNavigationEffects>() { @Composable override fun state(events: Flow<PlayerListUiEvents>): List<String> { LaunchedEffect(key1 = Unit) { events.collectLatest { event -> when (event) { OnBackClicked -> navigate(NavigateBack) is PlayerSelected -> navigate(LaunchDetails(state.value[event.index])) } } } return listPresenter.present() } }
  41. List Screen Molecule class PlayersListViewModel @Inject constructor( private val listPresenter:

    PlayerListPresenter ) : BaseViewModel<List<String>, PlayerListUiEvents, PlayerListNavigationEffects>() { @Composable override fun state(events: Flow<PlayerListUiEvents>): List<String> { LaunchedEffect(key1 = Unit) { events.collectLatest { event -> when (event) { OnBackClicked -> navigate(NavigateBack) is PlayerSelected -> navigate(LaunchDetails(state.value[event.index])) } } } return listPresenter.present() } }
  42. Details Screen Non-Molecule class PlayerDetailsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private

    val getPlayerInformation: GetPlayerInformationByUsernameUseCase, private val getPlayerStats: GetPlayerStatsByUsernameUseCase, private val timeAgoMapper: TimeAgoMapper ) : BaseViewModel<PlayerDetailsState, PlayerDetailsNavEffects>( PlayerDetailsState( PlayerInformationState.Loading, PlayerStatsState.Loading ) ) { private val details = savedStateHandle.toRoute<PlayerDetailsRoute>() init { viewModelScope.launch { combine( getPlayerInformationState(), getPlayerStatsState(), ) { playerInformationState, playerStatsState -> PlayerDetailsState( playerInformationState, playerStatsState ) }.collect { setState { it } } } } fun onBackClicked() { navigate(PlayerDetailsNavEffects.NavigateBack) } private fun getPlayerInformationState(): Flow<PlayerInformationState> = flow { ... } private fun getPlayerStatsState(): Flow<PlayerStatsState> = flow { ... } }
  43. Details Screen Non-Molecule class PlayerDetailsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private

    val getPlayerInformation: GetPlayerInformationByUsernameUseCase, private val getPlayerStats: GetPlayerStatsByUsernameUseCase, private val timeAgoMapper: TimeAgoMapper ) : BaseViewModel<PlayerDetailsState, PlayerDetailsNavEffects>( PlayerDetailsState( PlayerInformationState.Loading, PlayerStatsState.Loading ) ) { private val details = savedStateHandle.toRoute<PlayerDetailsRoute>() init { viewModelScope.launch { combine( getPlayerInformationState(), getPlayerStatsState(), ) { playerInformationState, playerStatsState -> PlayerDetailsState( playerInformationState, playerStatsState ) }.collect { setState { it } } } } fun onBackClicked() { navigate(PlayerDetailsNavEffects.NavigateBack) } private fun getPlayerInformationState(): Flow<PlayerInformationState> = flow { ... } private fun getPlayerStatsState(): Flow<PlayerStatsState> = flow { ... } }
  44. Details Screen Non-Molecule class PlayerDetailsViewModel @Inject constructor( ... ) :

    BaseViewModel<PlayerDetailsState, PlayerDetailsNavEffects>( ... ) { ... private fun getPlayerInformationState(): Flow<PlayerInformationState> = flow { emit(PlayerInformationState.Loading) getPlayerInformation(details.username).let { player -> emit( PlayerInformationState.PlayerInformation( imageUrl = player.avatarUrl ?: “https://www.shutterstock.com/image-vector/…”, name = player.realName ?: player.userName, title = player.title.mapDisplayName(), joinedChessDotCom = timeAgoMapper.map(player.joined), lastOnline = timeAgoMapper.map(player.lastOnline), ) ) } } private fun getPlayerStatsState(): Flow<PlayerStatsState> = flow { emit(PlayerStatsState.Loading) try { getPlayerStats(details.username).let { emit( PlayerStatsState.PlayerStats( blitzRating = it.blitzRating?.toString(), fideRating = it.fideRating.toString(), bulletRating = it.bulletRating?.toString(), rapidRating = it.rapidRating?.toString(), ) ) } } catch (e: CancellationException) { emit(PlayerStatsState.Hidden) throw e } catch (e: Exception) { emit(PlayerStatsState.Hidden) } } }
  45. Details Screen Non-Molecule class PlayerDetailsViewModel @Inject constructor( ... ) :

    BaseViewModel<PlayerDetailsState, PlayerDetailsNavEffects>( ... ) { ... private fun getPlayerInformationState(): Flow<PlayerInformationState> = flow { emit(PlayerInformationState.Loading) getPlayerInformation(details.username).let { player -> emit( PlayerInformationState.PlayerInformation( imageUrl = player.avatarUrl ?: “https://www.shutterstock.com/image-vector/…”, name = player.realName ?: player.userName, title = player.title.mapDisplayName(), joinedChessDotCom = timeAgoMapper.map(player.joined), lastOnline = timeAgoMapper.map(player.lastOnline), ) ) } } private fun getPlayerStatsState(): Flow<PlayerStatsState> = flow { emit(PlayerStatsState.Loading) try { getPlayerStats(details.username).let { emit( PlayerStatsState.PlayerStats( blitzRating = it.blitzRating?.toString(), fideRating = it.fideRating.toString(), bulletRating = it.bulletRating?.toString(), rapidRating = it.rapidRating?.toString(), ) ) } } catch (e: CancellationException) { emit(PlayerStatsState.Hidden) throw e } catch (e: Exception) { emit(PlayerStatsState.Hidden) } } }
  46. Details Screen Molecule class PlayerInformationPresenter @Inject constructor( private val getPlayerInformation:

    GetPlayerInformationByUsernameUseCase, private val timeAgoMapper: TimeAgoMapper ) { @Composable fun present(username: String): PlayerInformationState { var playerInformation: PlayerInformationDomain? by remember { mutableStateOf(null) } LaunchedEffect(username) { playerInformation = getPlayerInformation(username) } return playerInformation?.let { PlayerInformationState.PlayerInformation( imageUrl = it.avatarUrl ?: “https://www.shutterstock.com/image-vector/…”, name = it.realName ?: it.userName, title = it.title.mapDisplayName(), joinedChessDotCom = timeAgoMapper.map(it.joined), lastOnline = timeAgoMapper.map(it.lastOnline), location = it.location ) } ?: PlayerInformationState.Loading } }
  47. Details Screen Molecule class PlayerInformationPresenter @Inject constructor( private val getPlayerInformation:

    GetPlayerInformationByUsernameUseCase, private val timeAgoMapper: TimeAgoMapper ) { @Composable fun present(username: String): PlayerInformationState { var playerInformation: PlayerInformationDomain? by remember { mutableStateOf(null) } LaunchedEffect(username) { playerInformation = getPlayerInformation(username) } return playerInformation?.let { PlayerInformationState.PlayerInformation( imageUrl = it.avatarUrl ?: “https://www.shutterstock.com/image-vector/…”, name = it.realName ?: it.userName, title = it.title.mapDisplayName(), joinedChessDotCom = timeAgoMapper.map(it.joined), lastOnline = timeAgoMapper.map(it.lastOnline), location = it.location ) } ?: PlayerInformationState.Loading } }
  48. Details Screen Molecule class PlayerInformationPresenter @Inject constructor( private val getPlayerInformation:

    GetPlayerInformationByUsernameUseCase, private val timeAgoMapper: TimeAgoMapper ) { @Composable fun present(username: String): PlayerInformationState { var playerInformation: PlayerInformationDomain? by remember { mutableStateOf(null) } LaunchedEffect(username) { playerInformation = getPlayerInformation(username) } return playerInformation?.let { PlayerInformationState.PlayerInformation( imageUrl = it.avatarUrl ?: “https://www.shutterstock.com/image-vector/…”, name = it.realName ?: it.userName, title = it.title.mapDisplayName(), joinedChessDotCom = timeAgoMapper.map(it.joined), lastOnline = timeAgoMapper.map(it.lastOnline), location = it.location ) } ?: PlayerInformationState.Loading } }
  49. Details Screen Molecule class PlayerInformationPresenter @Inject constructor( private val getPlayerInformation:

    GetPlayerInformationByUsernameUseCase, private val timeAgoMapper: TimeAgoMapper ) { @Composable fun present(username: String): PlayerInformationState { var playerInformation: PlayerInformationDomain? by remember { mutableStateOf(null) } LaunchedEffect(username) { playerInformation = getPlayerInformation(username) } return playerInformation?.let { PlayerInformationState.PlayerInformation( imageUrl = it.avatarUrl ?: “https://www.shutterstock.com/image-vector/…”, name = it.realName ?: it.userName, title = it.title.mapDisplayName(), joinedChessDotCom = timeAgoMapper.map(it.joined), lastOnline = timeAgoMapper.map(it.lastOnline), location = it.location ) } ?: PlayerInformationState.Loading } }
  50. Details Screen Molecule class PlayerInformationPresenter @Inject constructor( private val getPlayerInformation:

    GetPlayerInformationByUsernameUseCase, private val timeAgoMapper: TimeAgoMapper ) { @Composable fun present(username: String): PlayerInformationState { var playerInformation: PlayerInformationDomain? by remember { mutableStateOf(null) } LaunchedEffect(username) { playerInformation = getPlayerInformation(username) } return playerInformation?.let { PlayerInformationState.PlayerInformation( imageUrl = it.avatarUrl ?: “https://www.shutterstock.com/image-vector/…”, name = it.realName ?: it.userName, title = it.title.mapDisplayName(), joinedChessDotCom = timeAgoMapper.map(it.joined), lastOnline = timeAgoMapper.map(it.lastOnline), location = it.location ) } ?: PlayerInformationState.Loading } }
  51. Details Screen Molecule class PlayerStatsPresenter @Inject constructor( private val getPlayerStats:

    GetPlayerStatsByUsernameUseCase, ) { @Composable fun present(username: String): PlayerStatsState { var isLoading by remember { mutableStateOf(true) } var stats: PlayerStatsDomain? by remember { mutableStateOf(null) } LaunchedEffect(username) { isLoading = true stats = try { getPlayerStats(username) } catch (e: Exception) { null } isLoading = false } return if (isLoading) PlayerStatsState.Loading else stats?.let { PlayerStatsState.PlayerStats( blitzRating = it.blitzRating?.toString(), fideRating = it.fideRating.toString(), bulletRating = it.bulletRating?.toString(), rapidRating = it.rapidRating?.toString(), puzzleRushRecord = it.puzzleRushRecord?.let { record -> "${record.score} / ${record.attempts}” } ) } ?: PlayerStatsState.Hidden } }
  52. Details Screen Molecule class PlayerStatsPresenter @Inject constructor( private val getPlayerStats:

    GetPlayerStatsByUsernameUseCase, ) { @Composable fun present(username: String): PlayerStatsState { var isLoading by remember { mutableStateOf(true) } var stats: PlayerStatsDomain? by remember { mutableStateOf(null) } LaunchedEffect(username) { isLoading = true stats = try { getPlayerStats(username) } catch (e: Exception) { null } isLoading = false } return if (isLoading) PlayerStatsState.Loading else stats?.let { PlayerStatsState.PlayerStats( blitzRating = it.blitzRating?.toString(), fideRating = it.fideRating.toString(), bulletRating = it.bulletRating?.toString(), rapidRating = it.rapidRating?.toString(), puzzleRushRecord = it.puzzleRushRecord?.let { record -> "${record.score} / ${record.attempts}” } ) } ?: PlayerStatsState.Hidden } }
  53. Details Screen Molecule class PlayerStatsPresenter @Inject constructor( private val getPlayerStats:

    GetPlayerStatsByUsernameUseCase, ) { @Composable fun present(username: String): PlayerStatsState { var isLoading by remember { mutableStateOf(true) } var stats: PlayerStatsDomain? by remember { mutableStateOf(null) } LaunchedEffect(username) { isLoading = true stats = try { getPlayerStats(username) } catch (e: Exception) { null } isLoading = false } return if (isLoading) PlayerStatsState.Loading else stats?.let { PlayerStatsState.PlayerStats( blitzRating = it.blitzRating?.toString(), fideRating = it.fideRating.toString(), bulletRating = it.bulletRating?.toString(), rapidRating = it.rapidRating?.toString(), puzzleRushRecord = it.puzzleRushRecord?.let { record -> "${record.score} / ${record.attempts}” } ) } ?: PlayerStatsState.Hidden } }
  54. Details Screen Molecule class PlayerStatsPresenter @Inject constructor( private val getPlayerStats:

    GetPlayerStatsByUsernameUseCase, ) { @Composable fun present(username: String): PlayerStatsState { var isLoading by remember { mutableStateOf(true) } var stats: PlayerStatsDomain? by remember { mutableStateOf(null) } LaunchedEffect(username) { isLoading = true stats = try { getPlayerStats(username) } catch (e: Exception) { null } isLoading = false } return if (isLoading) PlayerStatsState.Loading else stats?.let { PlayerStatsState.PlayerStats( blitzRating = it.blitzRating?.toString(), fideRating = it.fideRating.toString(), bulletRating = it.bulletRating?.toString(), rapidRating = it.rapidRating?.toString(), puzzleRushRecord = it.puzzleRushRecord?.let { record -> "${record.score} / ${record.attempts}” } ) } ?: PlayerStatsState.Hidden } }
  55. Details Screen Molecule class PlayerDetailsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private

    val playerInformationPresenter: PlayerInformationPresenter, private val playerStatsPresenter: PlayerStatsPresenter ) : BaseViewModel<PlayerDetailsState, PlayerDetailsUiEvents, PlayerDetailsNavEffects>() { private val details = savedStateHandle.toRoute<PlayerDetailsRoute>() @Composable override fun state(events: Flow<PlayerDetailsUiEvents>): PlayerDetailsState { LaunchedEffect(Unit) { events.collectLatest { when (it) { PlayerDetailsUiEvents.OnBackClicked -> navigate(PlayerDetailsNavEffects.NavigateBack) } } } return PlayerDetailsState( playerInfoState = playerInformationPresenter.present(username = details.username), playerStatsState = playerStatsPresenter.present(username = details.username) ) } }
  56. Details Screen Molecule class PlayerDetailsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private

    val playerInformationPresenter: PlayerInformationPresenter, private val playerStatsPresenter: PlayerStatsPresenter ) : BaseViewModel<PlayerDetailsState, PlayerDetailsUiEvents, PlayerDetailsNavEffects>() { private val details = savedStateHandle.toRoute<PlayerDetailsRoute>() @Composable override fun state(events: Flow<PlayerDetailsUiEvents>): PlayerDetailsState { LaunchedEffect(Unit) { events.collectLatest { when (it) { PlayerDetailsUiEvents.OnBackClicked -> navigate(PlayerDetailsNavEffects.NavigateBack) } } } return PlayerDetailsState( playerInfoState = playerInformationPresenter.present(username = details.username), playerStatsState = playerStatsPresenter.present(username = details.username) ) } }
  57. Details Screen Molecule class PlayerDetailsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private

    val playerInformationPresenter: PlayerInformationPresenter, private val playerStatsPresenter: PlayerStatsPresenter ) : BaseViewModel<PlayerDetailsState, PlayerDetailsUiEvents, PlayerDetailsNavEffects>() { private val details = savedStateHandle.toRoute<PlayerDetailsRoute>() @Composable override fun state(events: Flow<PlayerDetailsUiEvents>): PlayerDetailsState { LaunchedEffect(Unit) { events.collectLatest { when (it) { PlayerDetailsUiEvents.OnBackClicked -> navigate(PlayerDetailsNavEffects.NavigateBack) } } } return PlayerDetailsState( playerInfoState = playerInformationPresenter.present(username = details.username), playerStatsState = playerStatsPresenter.present(username = details.username) ) } }
  58. Details Screen Molecule class PlayerDetailsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private

    val playerInformationPresenter: PlayerInformationPresenter, private val playerStatsPresenter: PlayerStatsPresenter ) : BaseViewModel<PlayerDetailsState, PlayerDetailsUiEvents, PlayerDetailsNavEffects>() { private val details = savedStateHandle.toRoute<PlayerDetailsRoute>() @Composable override fun state(events: Flow<PlayerDetailsUiEvents>): PlayerDetailsState { LaunchedEffect(Unit) { events.collectLatest { when (it) { PlayerDetailsUiEvents.OnBackClicked -> navigate(PlayerDetailsNavEffects.NavigateBack) } } } return PlayerDetailsState( playerInfoState = playerInformationPresenter.present(username = details.username), playerStatsState = playerStatsPresenter.present(username = details.username) ) } }
  59. Details Screen Non-Molecule class PlayerDetailsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private

    val getPlayerInformation: GetPlayerInformationByUsernameUseCase, private val getPlayerStats: GetPlayerStatsByUsernameUseCase, private val timeAgoMapper: TimeAgoMapper, private val randomColourCodeFactory: RandomColourCodeFactory ) : BaseViewModel<PlayerDetailsState, PlayerDetailsNavEffects>( PlayerDetailsState( PlayerInformationState.Loading, PlayerStatsState.Loading ) ) { private val details = savedStateHandle.toRoute<PlayerDetailsRoute>() init { ... } fun onBackClicked() { navigate(PlayerDetailsNavEffects.NavigateBack) } fun onPlayerInformationClicked() { setState { copy( playerInfoState = (playerInfoState as PlayerInformation) .copy(backgroundColour = randomColourCodeFactory.randomColour()) ) } } fun onPlayerStatsClicked() { setState { copy( playerStatsState = (playerStatsState as PlayerStats) .copy(backgroundColour = randomColourCodeFactory.randomColour()) ) } } private fun getPlayerInformationState(): Flow<PlayerInformationState> = flow { ... } private fun getPlayerStatsState(): Flow<PlayerStatsState> = flow { ... } }
  60. Details Screen Non-Molecule class PlayerDetailsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private

    val playerInformationPresenter: PlayerInformationPresenter, private val playerStatsPresenter: PlayerStatsPresenter ) : BaseViewModel<PlayerDetailsState, PlayerDetailsUiEvents, PlayerDetailsNavEffects>() { private val details = savedStateHandle.toRoute<PlayerDetailsRoute>() @Composable override fun state(events: Flow<PlayerDetailsUiEvents>): PlayerDetailsState { LaunchedEffect(Unit) { events.collectLatest { when (it) { OnBackClicked -> navigate(PlayerDetailsNavEffects.NavigateBack) else -> Unit } } } return PlayerDetailsState( playerInfoState = playerInformationPresenter.present(events = events, username = details.username), playerStatsState = playerStatsPresenter.present(events = events, username = details.username) ) } }
  61. Details Screen Non-Molecule class PlayerDetailsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private

    val playerInformationPresenter: PlayerInformationPresenter, private val playerStatsPresenter: PlayerStatsPresenter ) : BaseViewModel<PlayerDetailsState, PlayerDetailsUiEvents, PlayerDetailsNavEffects>() { private val details = savedStateHandle.toRoute<PlayerDetailsRoute>() @Composable override fun state(events: Flow<PlayerDetailsUiEvents>): PlayerDetailsState { LaunchedEffect(Unit) { events.collectLatest { when (it) { OnBackClicked -> navigate(PlayerDetailsNavEffects.NavigateBack) else -> Unit } } } return PlayerDetailsState( playerInfoState = playerInformationPresenter.present(events = events, username = details.username), playerStatsState = playerStatsPresenter.present(events = events, username = details.username) ) } }
  62. Details Screen Molecule class PlayerStatsPresenter @Inject constructor( private val getPlayerStats:

    GetPlayerStatsByUsernameUseCase, private val randomColourCodeFactory: RandomColourCodeFactory ) { @Composable fun present(events: Flow<PlayerDetailsUiEvents>, username: String): PlayerStatsState { var isLoading by remember { mutableStateOf(true) } var stats: PlayerStatsDomain? by remember { mutableStateOf(null) } var backgroundColor by remember { mutableLongStateOf(0xFF444444) } LaunchedEffect(username) { ... } LaunchedEffect(Unit) { events.collectLatest { when(it) { PlayerDetailsUiEvents.PlayerStatsClicked -> backgroundColor = randomColourCodeFactory.randomColour() else -> Unit } } } return if (isLoading) PlayerStatsState.Loading else stats?.let { PlayerStatsState.PlayerStats( blitzRating = it.blitzRating?.toString(), fideRating = it.fideRating.toString(), bulletRating = it.bulletRating?.toString(), rapidRating = it.rapidRating?.toString(), puzzleRushRecord = it.puzzleRushRecord?.let { record -> “$ {record.score}" + “/ ${record.attempts}" }, backgroundColour = backgroundColor ) } ?: PlayerStatsState.Hidden } }
  63. Details Screen Molecule class PlayerStatsPresenter @Inject constructor( private val getPlayerStats:

    GetPlayerStatsByUsernameUseCase ) { fun present(username: String): Flow<PlayerStatsState> = flow { emit(PlayerStatsState.Loading) try { getPlayerStats(username).let { emit( PlayerStatsState.PlayerStats( blitzRating = it.blitzRating?.toString(), fideRating = it.fideRating.toString(), bulletRating = it.bulletRating?.toString(), rapidRating = it.rapidRating?.toString(), puzzleRushRecord = it.puzzleRushRecord ?.let { record -> "${record.score} / ${record.attempts}" } ) ) } } catch (e: CancellationException) { emit(PlayerStatsState.Hidden) throw e } catch (e: Exception) { Log.e("stats error", e.toString()) emit(PlayerStatsState.Hidden) } } }
  64. Details Screen Molecule class PlayerStatsPresenter @Inject constructor( private val getPlayerStats:

    GetPlayerStatsByUsernameUseCase ) { fun present(username: String): Flow<PlayerStatsState> = flow { emit(PlayerStatsState.Loading) try { getPlayerStats(username).let { emit( PlayerStatsState.PlayerStats( blitzRating = it.blitzRating?.toString(), fideRating = it.fideRating.toString(), bulletRating = it.bulletRating?.toString(), rapidRating = it.rapidRating?.toString(), puzzleRushRecord = it.puzzleRushRecord ?.let { record -> "${record.score} / ${record.attempts}" } ) ) } } catch (e: CancellationException) { emit(PlayerStatsState.Hidden) throw e } catch (e: Exception) { Log.e("stats error", e.toString()) emit(PlayerStatsState.Hidden) } } }
  65. Details Screen Molecule class PlayerStatsPresenter @Inject constructor( private val getPlayerStats:

    GetPlayerStatsByUsernameUseCase ) { fun present(username: String): Flow<PlayerStatsState> = flow { emit(PlayerStatsState.Loading) try { getPlayerStats(username).let { emit( PlayerStatsState.PlayerStats( blitzRating = it.blitzRating?.toString(), fideRating = it.fideRating.toString(), bulletRating = it.bulletRating?.toString(), rapidRating = it.rapidRating?.toString(), puzzleRushRecord = it.puzzleRushRecord ?.let { record -> "${record.score} / ${record.attempts}" } ) ) } } catch (e: CancellationException) { emit(PlayerStatsState.Hidden) throw e } catch (e: Exception) { Log.e("stats error", e.toString()) emit(PlayerStatsState.Hidden) } } }
  66. Details Screen Molecule class PlayerDetailsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private

    val playerInformationPresenter: PlayerInfoPresenter, private val playerStatsPresenter: PlayerStatsPresenter, ) : BaseViewModel<PlayerDetailsState, PlayerDetailsNavEffects>( PlayerDetailsState( PlayerInformationState.Loading, PlayerStatsState.Loading ) ) { private val details = savedStateHandle.toRoute<PlayerDetailsRoute>() init { viewModelScope.launch { combine( playerInformationPresenter.present(details.username), playerStatsPresenter.present(details.username), ) { playerInformationState, playerStatsState -> PlayerDetailsState( playerInformationState, playerStatsState ) }.collect { setState { it } } } } fun onBackClicked() { navigate(PlayerDetailsNavEffects.NavigateBack) } }
  67. Details Screen Molecule class PlayerDetailsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private

    val playerInformationPresenter: PlayerInfoPresenter, private val playerStatsPresenter: PlayerStatsPresenter, ) : BaseViewModel<PlayerDetailsState, PlayerDetailsNavEffects>( PlayerDetailsState( PlayerInformationState.Loading, PlayerStatsState.Loading ) ) { private val details = savedStateHandle.toRoute<PlayerDetailsRoute>() init { viewModelScope.launch { combine( playerInformationPresenter.present(details.username), playerStatsPresenter.present(details.username), ) { playerInformationState, playerStatsState -> PlayerDetailsState( playerInformationState, playerStatsState ) }.collect { setState { it } } } } fun onBackClicked() { navigate(PlayerDetailsNavEffects.NavigateBack) } }
  68. class PlayerDetailsViewModel @Inject constructor( … ) : BaseViewModel<PlayerDetailsState, PlayerDetailsNavEffects>( …

    ) { init { viewModelScope.launch { combine( playerInformationPresenter.present(details.username), playerStatsPresenter.present(details.username), ) { playerInformationState, playerStatsState -> PlayerDetailsState( playerInformationState, playerStatsState ) }.collect { setState { it } } } } } But Combine doesn’t scale so well
  69. class PlayerDetailsViewModel @Inject constructor( … ) : BaseViewModel<PlayerDetailsState, PlayerDetailsNavEffects>( …

    ) { init { viewModelScope.launch { combine( playerInformationPresenter.present(details.username), playerStatsPresenter.present(details.username), userProfileFlow(), counterFlow(), alphabetFlow() ) { playerInformationState, playerStatsState, profile, count, char -> PlayerDetailsState( playerInformationState, playerStatsState, profile, count, char ) }.collect { setState { it } } } } } But Combine doesn’t scale so well
  70. class PlayerDetailsViewModel @Inject constructor( … ) : BaseViewModel<PlayerDetailsState, PlayerDetailsNavEffects>( …

    ) ) { init { viewModelScope.launch { combine( playerInformationPresenter.present(details.username), playerStatsPresenter.present(details.username), userProfileFlow(), counterFlow(), alphabetFlow(), reverseAlphabetFlow() ) { params -> PlayerDetailsState( params[0] as PlayerInformationState, params[1] as PlayerStatsState, params[2] as UserStatus, params[3] as Int, params[4] as Char, params[5] as Char ) }.collect { setState { it } } } } } But Combine doesn’t scale so well
  71. fetchDataFlow() .filter { it.isNotEmpty() } .map { it.toUpperCase() } .flatMapConcat

    { downloadImages(it) } .buffer(10) .onEach { processImage(it) } .catch { handleError(it) } .collect { updateUi(it) } And if our logic becomes complex we can find ourselves chaining like it’s Rx
  72. class PlayerDetailsViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val playerInformationPresenter: PlayerInformationPresenter,

    private val playerStatsPresenter: PlayerStatsPresenter, private val playerMatchesPresenter: PlayerMatchesPresenter ) : BaseViewModel<PlayerDetailsState, PlayerDetailsUiEvents, PlayerDetailsNavEffects>() { private val details = savedStateHandle.toRoute<PlayerDetailsRoute>() @Composable override fun state(events: Flow<PlayerDetailsUiEvents>): PlayerDetailsState { var matchesSearchTerm: String by remember { mutableStateOf("") } LaunchedEffect(Unit) { events.collectLatest { when (it) { PlayerDetailsUiEvents.OnBackClicked -> navigate(PlayerDetailsNavEffects.NavigateBack) is PlayerDetailsUiEvents.MatchesSearchTermChanged -> matchesSearchTerm = it.searchTerm } } } return PlayerDetailsState( playerInfoState = playerInformationPresenter.present(username = details.username), playerStatsState = playerStatsPresenter.present(username = details.username), playerMatchesState = playerMatchesPresenter.present( username = details.username, searchTerm = matchesSearchTerm ) ) } } And using Molecule we can leverage other features of Compose
  73. class Screen1ViewModel @Inject constructor( private val getPlayerInformation: GetPlayerInformationByUsernameUseCase, private val

    playerComponentStateMapper: PlayerComponentStateMapper, private val randomPlayerFactory: RandomPlayerFactory ) : BaseViewModel<Screen1State, Screen1NavEffects>( Screen1State(playerComponentState = PlayerComponentState.Loading) ) { init { loadRandomPlayer() } fun onBackClicked() { navigate(NavigateBack) } fun onContinueClicked() { navigate(LaunchScreen2) } fun onRandomiseClicked() { loadRandomPlayer() } private fun loadRandomPlayer() { viewModelScope.launch { try { setState { copy(playerComponentState = PlayerComponentState.Loading) } val domain = getPlayerInformation(randomPlayerFactory.getRandomPlayer()) setState { copy(playerComponentState = playerComponentStateMapper.map(domain)) } } catch (e: CancellationException) { throw e } catch (e: Exception) { setState { copy( playerComponentState = PlayerComponentState.Error( message = when { e is HttpException -> e.mapDisplayErrorMessage() else -> "Internal error, please report a bug" } ) ) } } } } }
  74. class Screen2ViewModel @Inject constructor( private val getPlayerInformation: GetPlayerInformationByUsernameUseCase, private val

    playerComponentStateMapper: PlayerComponentStateMapper, private val randomPlayerFactory: RandomPlayerFactory ) : BaseViewModel<Screen2State, Screen2NavEffects>( Screen2State(playerComponentState = PlayerComponentState.Loading) ) { init { loadRandomPlayer() } fun onBackClicked() { navigate(NavigateBack) } fun onContinueClicked() { navigate(LaunchScreen3) } fun onRandomiseClicked() { loadRandomPlayer() } private fun loadRandomPlayer() { viewModelScope.launch { try { setState { copy(playerComponentState = PlayerComponentState.Loading) } val domain = getPlayerInformation(randomPlayerFactory.getRandomPlayer()) setState { copy(playerComponentState = playerComponentStateMapper.map(domain)) } } catch (e: CancellationException) { throw e } catch (e: Exception) { setState { copy( playerComponentState = PlayerComponentState.Error( message = when { e is HttpException -> e.mapDisplayErrorMessage() else -> "Internal error, please report a bug" } ) ) } } } } }
  75. class Screen3ViewModel @Inject constructor( private val getPlayerInformation: GetPlayerInformationByUsernameUseCase, private val

    playerComponentStateMapper: PlayerComponentStateMapper, private val randomPlayerFactory: RandomPlayerFactory ) : BaseViewModel<Screen3State, Screen3NavEffects>( Screen3State(playerComponentState = PlayerComponentState.Loading) ) { init { loadRandomPlayer() } fun onBackClicked() { navigate(Screen3NavEffects.NavigateBack) } fun onRandomiseClicked() { loadRandomPlayer() } private fun loadRandomPlayer() { viewModelScope.launch { try { setState { copy(playerComponentState = PlayerComponentState.Loading) } val domain = getPlayerInformation(randomPlayerFactory.getRandomPlayer()) setState { copy(playerComponentState = playerComponentStateMapper.map(domain)) } } catch (e: CancellationException) { throw e } catch (e: Exception) { setState { copy( playerComponentState = PlayerComponentState.Error( message = when { e is HttpException -> e.mapDisplayErrorMessage() else -> "Internal error, please report a bug" } ) ) } } } } }
  76. class PlayerComponentPresenter @Inject constructor( private val getPlayerInformation: GetPlayerInformationByUsernameUseCase, private val

    playerComponentStateMapper: PlayerComponentStateMapper, private val randomPlayerFactory: RandomPlayerFactory ) { @Composable fun present(events: Flow<PlayerComponentUiEvents>): PlayerComponentState { var isLoading: Boolean by remember { mutableStateOf(true) } var playerName: String by remember { mutableStateOf(randomPlayerFactory.getRandomPlayer()) } var playerInformationDomain: PlayerInformationDomain? by remember { mutableStateOf(null) } LaunchedEffect(playerName) { isLoading = true playerInformationDomain = getPlayerInformation(playerName) isLoading = false } LaunchedEffect(Unit) { events.collectLatest { when (it) { PlayerComponentUiEvents.RandomiseClicked -> playerName = randomPlayerFactory.getRandomPlayer() } } } if (isLoading) return PlayerComponentState.Loading return playerInformationDomain?.let { playerComponentStateMapper.map(it) } ?: PlayerComponentState.Error("error loading player") } }
  77. class PlayerComponentPresenter @Inject constructor( private val getPlayerInformation: GetPlayerInformationByUsernameUseCase, private val

    playerComponentStateMapper: PlayerComponentStateMapper, private val randomPlayerFactory: RandomPlayerFactory ) { @Composable fun present(events: Flow<PlayerComponentUiEvents>): PlayerComponentState { var isLoading: Boolean by remember { mutableStateOf(true) } var playerName: String by remember { mutableStateOf(randomPlayerFactory.getRandomPlayer()) } var playerInformationDomain: PlayerInformationDomain? by remember { mutableStateOf(null) } LaunchedEffect(playerName) { isLoading = true playerInformationDomain = getPlayerInformation(playerName) isLoading = false } LaunchedEffect(Unit) { events.collectLatest { when (it) { PlayerComponentUiEvents.RandomiseClicked -> playerName = randomPlayerFactory.getRandomPlayer() } } } if (isLoading) return PlayerComponentState.Loading return playerInformationDomain?.let { playerComponentStateMapper.map(it) } ?: PlayerComponentState.Error("error loading player") } }
  78. class PlayerComponentPresenter @Inject constructor( private val getPlayerInformation: GetPlayerInformationByUsernameUseCase, private val

    playerComponentStateMapper: PlayerComponentStateMapper, private val randomPlayerFactory: RandomPlayerFactory ) { @Composable fun present(events: Flow<PlayerComponentUiEvents>): PlayerComponentState { var isLoading: Boolean by remember { mutableStateOf(true) } var playerName: String by remember { mutableStateOf(randomPlayerFactory.getRandomPlayer()) } var playerInformationDomain: PlayerInformationDomain? by remember { mutableStateOf(null) } LaunchedEffect(playerName) { isLoading = true playerInformationDomain = getPlayerInformation(playerName) isLoading = false } LaunchedEffect(Unit) { events.collectLatest { when (it) { PlayerComponentUiEvents.RandomiseClicked -> playerName = randomPlayerFactory.getRandomPlayer() } } } if (isLoading) return PlayerComponentState.Loading return playerInformationDomain?.let { playerComponentStateMapper.map(it) } ?: PlayerComponentState.Error("error loading player") } }
  79. class PlayerComponentPresenter @Inject constructor( private val getPlayerInformation: GetPlayerInformationByUsernameUseCase, private val

    playerComponentStateMapper: PlayerComponentStateMapper, private val randomPlayerFactory: RandomPlayerFactory ) { @Composable fun present(events: Flow<PlayerComponentUiEvents>): PlayerComponentState { var isLoading: Boolean by remember { mutableStateOf(true) } var playerName: String by remember { mutableStateOf(randomPlayerFactory.getRandomPlayer()) } var playerInformationDomain: PlayerInformationDomain? by remember { mutableStateOf(null) } LaunchedEffect(playerName) { isLoading = true playerInformationDomain = getPlayerInformation(playerName) isLoading = false } LaunchedEffect(Unit) { events.collectLatest { when (it) { PlayerComponentUiEvents.RandomiseClicked -> playerName = randomPlayerFactory.getRandomPlayer() } } } if (isLoading) return PlayerComponentState.Loading return playerInformationDomain?.let { playerComponentStateMapper.map(it) } ?: PlayerComponentState.Error("error loading player") } }
  80. class PlayerComponentPresenter @Inject constructor( private val getPlayerInformation: GetPlayerInformationByUsernameUseCase, private val

    playerComponentStateMapper: PlayerComponentStateMapper, private val randomPlayerFactory: RandomPlayerFactory ) { @Composable fun present(events: Flow<PlayerComponentUiEvents>): PlayerComponentState { var isLoading: Boolean by remember { mutableStateOf(true) } var playerName: String by remember { mutableStateOf(randomPlayerFactory.getRandomPlayer()) } var playerInformationDomain: PlayerInformationDomain? by remember { mutableStateOf(null) } LaunchedEffect(playerName) { isLoading = true playerInformationDomain = getPlayerInformation(playerName) isLoading = false } LaunchedEffect(Unit) { events.collectLatest { when (it) { PlayerComponentUiEvents.RandomiseClicked -> playerName = randomPlayerFactory.getRandomPlayer() } } } if (isLoading) return PlayerComponentState.Loading return playerInformationDomain?.let { playerComponentStateMapper.map(it) } ?: PlayerComponentState.Error("error loading player") } }
  81. class Screen1ViewModel @Inject constructor( private val playerComponentPresenter: PlayerComponentPresenter ) :

    BaseViewModel<Screen1State, Screen1UiEvent, Screen1NavEffects>() { @Composable override fun state(events: Flow<Screen1UiEvent>): Screen1State { LaunchedEffect(Unit) { events.collectLatest { when (it) { Screen1UiEvent.BackClicked -> navigate(NavigateBack) Screen1UiEvent.Randomise -> Unit Screen1UiEvent.ContinueClicked -> navigate(LaunchScreen2) } } } return Screen1State( playerComponentState = playerComponentPresenter.present( events = events.filter { it is Screen1UiEvent.Randomise } .map { PlayerComponentUiEvents.RandomiseClicked } ) ) } }
  82. class Screen2ViewModel @Inject constructor( private val playerComponentPresenter: PlayerComponentPresenter ) :

    BaseViewModel<Screen2State, Screen2UiEvent, Screen2NavEffects>() { @Composable override fun state(events: Flow<Screen2UiEvent>): Screen2State { LaunchedEffect(Unit) { events.collectLatest { when (it) { Screen2UiEvent.BackClicked -> navigate(NavigateBack) Screen2UiEvent.Randomise -> Unit Screen2UiEvent.ContinueClicked -> navigate(LaunchScreen3) } } } return Screen2State( playerComponentState = playerComponentPresenter.present( events = events.filter { it is Screen2UiEvent.Randomise } .map { PlayerComponentUiEvents.RandomiseClicked } ) ) } }
  83. class Screen3ViewModel @Inject constructor( private val playerComponentPresenter: PlayerComponentPresenter ) :

    BaseViewModel<Screen3State, Screen3UiEvent, Screen3NavEffects>( ) { @Composable override fun state(events: Flow<Screen3UiEvent>): Screen3State { LaunchedEffect(Unit) { events.collectLatest { when (it) { Screen3UiEvent.BackClicked -> navigate(Screen3NavEffects.NavigateBack) else -> Unit } } } return Screen3State( playerComponentState = playerComponentPresenter.present( events = events.filter { it is Screen3UiEvent.Randomise } .map { PlayerComponentUiEvents.RandomiseClicked } ) ) } }
  84. Testing We can test our logic using Turbine (or any

    other fl ow testing methodology) @Test fun `GIVEN randomise event is received THEN new player data is loaded`() = runTest { val input = flowOf(RandomiseClicked, RandomiseClicked) ... moleculeFlow(RecompositionMode.Immediate) { sut.present(events = input) }.test { assertEquals(PlayerComponentState.Loading, awaitItem()) assertEquals(fakePlayer1InfoState, awaitItem()) assertEquals(PlayerComponentState.Loading, awaitItem()) assertEquals(fakePlayer2InfoState, awaitItem()) assertEquals(PlayerComponentState.Loading, awaitItem()) assertEquals(fakePlayer3InfoState, awaitItem()) } }
  85. Normal flow val counterState: StateFlow<Int> = viewModelScope.launchMolecule(Immediate) { counter(start =

    0) } val counterState: Flow<Int> = moleculeFlow(Immediate) { counter(start = 0) }
  86. To recap • Molecule is a library for using Compose

    for non-ui purposes • It allows us to write compose for our presentation logic • It’s fun! • Using the Molecule ViewModel we can create VMs that scale much better than more traditional methods • It lets us combine multiple streams of data in a readable and imperative style • It’s really great for decoupling the logic for a reusable ui component from the ViewModel of each screen