BlockPage

fun BlockPage(modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(), onRefresh: suspend () -> Unit? = null, empty: @Composable () -> Unit? = null, content: BlockPageScope.() -> Unit)

A full screen composed of several blocks, scrolling as one.

val shelf = IssuesBlockSource.ofPublications(listOf(publicationId))
BlockPage(onRefresh = { myOwnSections.reload() }, empty = { NothingToShow() }) {
issues(source = shelf, title = "Latest issues", showHeaderAction = true, onEvent = ::handleIssueEvent)
articles(source = ArticlesBlockSource.ofPublications(listOf(publicationId)), title = "Articles", limit = 3, onEvent = ::handleArticleEvent)
custom("promo") { MyBanner() }
issues(source = IssuesBlockSource.ofPublications(listOf(publicationId), dates = DateRange.ofYear(2025)), title = "2025", onEvent = ::handleIssueEvent)
}

Each block keeps managing itself exactly as it does standalone — loading, pagination, targeted retry, recovery when the connection comes back, collapse when empty. On top of that the page adds what a lone block cannot provide:

  • One retry instead of many. A partial failure leaves the page visible, each failed block showing its own retry. Only when every block failed does the page replace them with a single retry that reloads all of them, and runs onRefresh for the caller's own sections it hides with them.

  • One empty state instead of a blank screen, when empty is supplied and every block settled with nothing to show.

  • Refresh across blocks, when onRefresh is supplied.

Unlike a standalone block, a page loads every block it declares as soon as it is displayed, whether or not that block is on screen — knowing that all of them failed requires all of them to have tried. Everything still idles together once the page leaves the screen, and comes back to its pages when the page does. Two blocks declaring the same content share a single request.

The block needs a androidx.lifecycle.ViewModelStoreOwner in scope. Any ComponentActivity, AppCompatActivity or fragment already is one — writing no ViewModel of your own changes nothing, since the store comes with the activity. Only a view tree that has no owner at all, such as a ComposeView in a window the host set up by hand, has none; the block then fails on its first composition with "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner", which names nothing of this SDK. Supplying one is a line: CompositionLocalProvider(LocalViewModelStoreOwner provides owner) { BlockPage(…) }.

The block then takes the retention of whichever owner it found — a NavBackStackEntry under Navigation-Compose, the activity or the fragment otherwise — and that choice is the caller's. Blocks under the same owner showing the same content share one request and its loaded pages, so an activity-wide owner has two different screens share them, and a refresh on one seen by the other; a per-destination owner does not. A transfer already under way is unaffected either way — downloads run on the SDK's own scheduler, not on the owner — but the tap that starts one is launched on the owner's scope, so an owner cleared in that instant can leave the download unstarted, with nothing reported.

Parameters

modifier

modifier applied to the page's root.

contentPadding

padding around the whole page content.

onRefresh

enables pull-to-refresh when non-null. The page refreshes every one of its blocks and runs this alongside them, for the caller's own BlockPageScope.custom sections; the indicator stays up until the last of them is done. The single retry of a page whose blocks all failed runs it too. Pass {} to enable the gesture with nothing extra to do.

empty

drawn in place of the blocks when every block the page declares has settled with nothing to show — a publication without an issue, a catalogue with nothing accessible. Caller-owned sections are replaced too, as on a failure. Null collapses the blocks as they would on their own, leaving only the BlockPageScope.custom sections; a page that is the screen usually has something to say. An unidentified session does not count: nothing on a page speaks for it, that is the host's login flow.

content

declares the blocks, in display order. Not composable, like a LazyColumn's content: resolve strings and read state before or inside it, but call no composable there. Evaluated again whenever it changes or the state it read changes, and only then — keep it free of side effects.