ARTICLE AD BOX
In Jetpack Compose with MVVM, I have a parent screen that contains 3 child screens. Navigation is used to move between the child screens.
Each child screen can generate a PDF in different ways:
One downloads it from the internet. Another generates it locally. Another might build it from database data.Regardless of how they are generated, all three child screens need to be able to:
Display the PDF. Send it by email. Print it.The issue is that these three actions duplicate code across all child screens. To avoid duplication, I thought about moving the logic for displaying, sending, and printing the PDF up to the parent screen. The only way I can think of doing this is:
Each child screen stores the generated PDF in its own uiState.
In the child composable, I use a LaunchedEffect to detect when the PDF changes.
When it changes, I call a function in the parent, passing the PDF upward.
Here’s a simplified snippet of how the parent receives the PDF from a child:
composable(InfractionsDestinations.Pending.name) { PendingScreen( infractions = uiState.pendingInfractions, onPDFDisplayRequested = { pdf -> vm.showPDF(pdf = pdf) } ) }The problem is that now both the parent and the child are storing the PDF in their own uiState, because it will be used for display it, send it or print it, so it needs to keep it. This breaks the principle of Single Source of Truth (SSOT).
⚡ Mini Example of the Issue
Child ViewModel:
data class PendingScreenUiState( val loading: Boolean = false, val pdf: C60PDF? = null ) class PendingScreenViewModel : ViewModel() { private val _uiState = MutableStateFlow(PendingScreenUiState()) val uiState: StateFlow<PendingScreenUiState> = _uiState fun buildPDF(context: Context, infraction: InfractionWithDetails) { viewModelScope.launch { val pdfFile = PDFUtils.generateProvisionalPDF(context, infraction) if (pdfFile.exists()) { val bulletinPDF = C60PDF(pdfFile) _uiState.update { it.copy(pdf = bulletinPDF) } } } } }Child Composable:
@Composable fun PendingScreen( infractions: List<InfractionWithDetails>, vm: PendingScreenViewModel = koinViewModel(), onPDFDisplayRequested: (C60PDF) -> Unit ) { val uiState by vm.uiState.collectAsStateWithLifecycle() LaunchedEffect(uiState.pdf) { uiState.pdf?.let { onPDFDisplayRequested(it) } } // ... rest of the UI }Is there a cleaner strategy that follows the recommended Compose + MVVM principles (like SSOT and state hoisting) to handle this situation?
Right now, I feel forced to duplicate the PDF in both child and parent states, which doesn’t feel right. Is there a recommended pattern to centralize the PDF handling in the parent while still letting each child generate it in its own ViewModel?
