14

Compose - List / Detail: Basics - Styling Android

 3 years ago
source link: https://blog.stylingandroid.com/compose-list-detail-basics/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
Compose / Jetpack

Compose – List / Detail: Basics

In a recent post on Styling Android we looked SlidingPanelLayout. This can simplify the implementation of a List / Detail UI. It handles the logic of whether to show a side-by-side layout or a two-page layout depending on the screen size. Currently, there is no equivalent for this in Jetpack Compose. In this article, we’ll see how Compose makes this relatively easy.

Before we dive into this it’s worth pointing out the word “currently” in the opening paragraph. This is an area of Compose that is under active development. So keep an eye out for an official implementation of this kind of functionality. Even if this is only a temporary solution there are still some interesting techniques that we’ll cover.

It’s also worth mentioning that, we’ll have a working solution by the end of this article. This may be fine for many cases. But we’ll add further functionality in the second article. This will make the behaviour even better on foldable devices.

List / Detail

The basic behaviour that we’re after is when the UI has a list of items. When the user taps an item in the list then we display the details in the detail view. On larger screens, there may be sufficient space to display both the list and detail view side-by-side. However, on smaller devices tapping an item may replace the list view with the detail view, and hitting back will return.

We can do this on older View-based UIs by having different layouts for different screen sizes. More recently, SlidingPaneLayout can handle the heavy lifting.

To do this with Compose, let’s first look at the List and Detail composables:

@Composable
fun List(list: List<String>, onSelectionChange: (String) -> Unit) {
    LazyColumn() {
        for (entry in list) {
            item() {
                    Modifier
                        .fillMaxWidth()
                        .clickable { onSelectionChange(entry) }
                        .padding(horizontal = 16.dp, vertical = 8.dp)
                    Text(text = entry)
@Composable
fun Detail(text: String) {
    Text(text = text)

I have deliberately kept these as simple as possible. This is to keep the code easy to understand.

List() uses a LazyColumn to display a list of items provided as an argument, and the selection action is handled by a listener argument. Detail() simply displays a piece of text.

Split Layout

The split layout (i.e. the side-by-side one) is the easier to implement:

@Composable
fun SplitLayout(list: List<String>) {
    var selected by rememberSaveable { mutableStateOf("") }
    Row(Modifier.fillMaxWidth()) {
        Box(modifier = Modifier.weight(1f)) {
            List(list = list) { newSelection ->
                selected = newSelection
        Box(modifier = Modifier.weight(1f)) {
            Detail(text = selected)

Here we use a mutable state to hold the currently selected text.

We display the List and Detail components side-by-side using a Row. I have used equal weights here to divide the screen in half. But it would be trivial to change that to meet differing requirements.

When the user taps on a list item, the text is displayed in the right hand pane:

Two Page Layout

Implementing the two page layout is slightly tricker because we have to consider how to handle the back behaviour. Using Navigation Compose makes this much easier.

@Composable
fun TwoPageLayout(list: List<String>) {
    val navController = rememberNavController()
    val detailRoute = NavGraph.Route.Detail
    NavHost(navController = navController, startDestination = "list") {
        composable(route = NavGraph.Route.List.route) {
            List(list = list) { selected ->
                navController.navigate(route = detailRoute.navigateRoute(selected))
        composable(route = detailRoute.route) { backStackEntry ->
            Detail(text = backStackEntry.arguments?.getString("selected") ?: "")
private object NavGraph {
    sealed class Route(val route: String) {
        object List : Route("list")
        object Detail : Route("detail/{selected}") {
            fun navigateRoute(selected: String) = "detail/$selected"

We create a NavGraph which consists of two destinations – the List and the Detail. The route for the Detail includes an argument for the selection string.

When the user taps on a list item, then it triggers navigation to the Detail view, including the selected text as an argument. We extract this value from the backStackEntry arguments and use it as the argument for the Details() composable. Navigate Compose now handles the back behaviour for us.

This gives the basic behahviour that we’re after:

To keep the code simple and easier to understand I haven’t included any navigation animations. But that is certainly something I’d look to add when using this for real.

Dynamic Layout

Now that we’ve implemented the basic behaviour patterns, we need to add the logic of when to use each. This is actually really easy in Compose:

@Composable
@Suppress("MagicNumber")
fun DynamicLayout(
    list: List<String>,
    configuration: Configuration
    if (configuration.smallestScreenWidthDp < 580) {
        TwoPageLayout(list)
    } else {
        SplitLayout(list)

A Configuration instance is passed in as an argument, and we apply some simple logic. We’ll look at where this comes from in a moment. If the smallest screen width is less than 580dp, then we emit TwoPageLayout otherwise we emit SplitLayout.

The Configuration object is not specific to Compose, it is what is used by the resource management framework to provide alternate resources. So we can leverage it in much the same way we. In this case, we’re applying the same logic as when we put a layout in res/layout/sw580.

Putting It All Together

We can now call DynamicLayout with two arguments: The list of strings to display, and the Configuration instance:

MainActivity.kt
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeListDetailTheme {
                Surface(color = MaterialTheme.colors.background) {
                    @Suppress("MagicNumber")
                    DynamicLayout(
                        (1..10).map { index -> "Item $index" },
                        LocalConfiguration.current

We obtain the current Configuration by calling LocalConfiguration.current.

The user will now see different UIs depending on the window size. This also works in multi-window – as the window size crosses the 580dp width boundary the UI switches automatically.

Conclusion

None of the individual composables here are particularly complex. That is very much by design. Keeping the composables small and focused makes them much easier to combine to create the desired UI. For example DyanmicLayout is solely about the logic for which UI to emit. TwoPageLayout and SplitLayout are solely responsible for their own specific behaviour

While this may seem like we have the behaviour that we want now, this doesn’t quite match the functionality of SlidingPaneLayout. In the next article, we’ll look at how we can get this playing even nicer with foldables.

The source code for this article is available here.

© 2021, Mark Allison. All rights reserved.

Related

Jetpack ComposeMay 14, 2021In "Compose"

Gradle: Version CatalogsMay 21, 2021In "Build"

NumberPickerJune 26, 2020In "NumberPicker"

Copyright © 2021 Styling Android. All Rights Reserved.
Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK