Jetpack Compose large screen adaptation exploration

Article directory

    • 1. Convert the original size to the window Size class
    • 2. Measurement constraints
    • 3. Constraint layout
    • 4. Demo

Official documentation: https://developer.android.com/jetpack/compose/layouts/adaptive?hl=zh-cn
When you use Compose to lay out your entire app, app-level and screen-level composables take up all the space allocated to the app for rendering. At this level of app design, it may be necessary to change the overall layout of the screen to make the most of the screen space.

Key terms:

  • App-level composable: A single root composable that takes up all the space allocated to the app and contains all other composables.
  • Screen-level composable: A type of composable included in an app-level composable that takes up all the space allocated to the app. When navigating within an app, each screen-level composable typically represents a specific destination.
  • Individual composable items: All other composable items. Can be individual elements, reusable content groups, or composables hosted in screen-level composables.
    Please add image description
    Principle: Avoid basing your layout on physical hardware values.

1. Convert the original size to the window Size class

Grouping sizes into standard size buckets are breakpoints that provide the flexibility to optimize your app for the most unique situations without making it too difficult to implement. These Size classes refer to the entire window of your application, so use these classes to determine layout that affects the entire screen. You can pass these Size classes as state or perform additional logic to create derived states to pass to nested composables.

@Composable
fun Activity.rememberWindowSizeClass(): WindowSize {<!-- -->
    val configuration = LocalConfiguration.current
    val windowMetrics = remember(configuration) {<!-- -->
        //This solution relies on the window library and can also be used without compose.
        WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(this)
    }
    val windowDpSize = with(LocalDensity.current) {<!-- -->
        windowMetrics.bounds.toComposeRect().size.toDpSize()
    }

    return when {<!-- -->
        windowDpSize.width < 600.dp -> WindowSize.COMPACT
        windowDpSize.height < 840.dp -> WindowSize.MEDIUM
        else -> WindowSize.EXPANDED
    }
}

@Composable
fun MyApp(windowSizeClass: WindowSizeClass) {<!-- -->
    // Perform logic on the size class to decide whether to show
    // the top app bar.
    val showTopAppBar = windowSizeClass != WindowSizeClass.Compact

    // MyScreen knows nothing about window sizes, and performs logic
    // based on a Boolean flag.
    MyScreen(
        showTopAppBar = showTopAppBar,
        /* ... */
    )
}

2. Measurement constraints

If you can take advantage of the extra screen real estate, you can show more content to users on a large screen than on a small screen. When implementing composables with this behavior, you may want to increase efficiency by loading data based on the current screen size.
Use BoxWithConstraints as a more powerful alternative. This composable provides measurement constraints that can be used to invoke different composables depending on the available space. However, this has consequences, as BoxWithConstraints defers composition to the layout phase (when the constraints are known), causing more work to be performed during layout.

BoxWithConstraints {<!-- -->
    if (maxWidth < 400.dp) {<!-- -->
        OnPane()
    } else {<!-- -->
        TwoPane()
    }
}

//or as follows
@Composable
fun AdaptivePane(
    showOnePane: Boolean,
    /* ... */
) {<!-- -->
    if (showOnePane) {<!-- -->
        OnePane(/* ... */)
    } else {<!-- -->
        TwoPane(/* ... */)
    }
}

3. Constraint layout

ConstraintLayout is a layout that allows you to position composable items relative to other composable items on the screen. It is a practical alternative to using multiple nested Rows, Columns, Boxes, and other custom layout elements. ConstraintLayout is useful when implementing larger layouts with complex alignment requirements.

For details, see: https://developer.android.com/jetpack/compose/layouts/constraintlayout?hl=zh-cn

@Composable
fun ConstraintLayoutContent() {<!-- -->
    ConstraintLayout {<!-- -->
        // Create references for the composables to constrain
        val (button, text) = createRefs()

        Button(
            onClick = {<!-- --> /* Do something */ },
            // Assign reference "button" to the Button composable
            // and constrain it to the top of the ConstraintLayout
            modifier = Modifier.constrainAs(button) {<!-- -->
                top.linkTo(parent.top, margin = 16.dp)
            }
        ) {<!-- -->
            Text("Button")
        }

        // Assign reference "text" to the Text composable
        // and constrain it to the bottom of the Button composable
        Text("Text", Modifier.constrainAs(text) {<!-- -->
            top.linkTo(button.bottom, margin = 16.dp)
        })
    }
}

4. Demo

The final effect is as follows: https://live.csdn.net/v/embed/341555 (The picture is compressed and slightly distorted…)

The following figure is an example. Two parts are displayed on the large screen. The left side is the picture list and the right side is the picture details page.

Next, the implementation steps of its key parts are introduced.
1. Choose layout according to screen size

//Measurement constraint layout
@Composable
fun Homepage() {<!-- -->
    BoxWithConstraints {<!-- -->
        if (maxWidth < 400.dp || maxHeight < 400.dp) {<!-- -->
            SideBarDrawer() //Hold MainNavRoute
            ResourceHolder.navigationViewModel.setTwoPane(false) //Flag bit
        } else {<!-- -->
            ResourceHolder.navigationViewModel.setTwoPane(true)
            Row {<!-- -->
                Column(modifier = Modifier.weight(1f)) {<!-- -->
                    SideBarDrawer()
                }
                Column(modifier = Modifier.weight(1f)) {<!-- -->
                    PlaceholderNavRoute()
                }
            }
        }
    }
}

2. Linkage between lists and details pages
This part is actually implemented based on routing. The two screen-level composable items left and right both hold independent NavHostController. We use Whether it is twoPane, determine which controller the current route should jump to. The two NavHostController are defined as follows. You can see that the routing of the content on the right is only a subset of the content on the left, because only some combinable items will be displayed on the right.

//Route defined by navHostController on the left
@Composable
fun MainNavRoute(curItem: String? = null) {<!-- -->
    val navHostController = rememberNavController().also {<!-- -->
        ResourceHolder.navHostController = it
    }
    val startDestination = curItem?.let {<!-- -->
        getSideBarMenu().getOrDefault(curItem, NavRoute.PICTURE_LIST_PAGE.route)
    } ?: NavRoute.PICTURE_LIST_PAGE.route

    NavHost(navHostController, startDestination = startDestination) {<!-- -->
        composable(NavRoute.PICTURE_LIST_PAGE.route) {<!-- -->
            PictureListPage(ResourceHolder.pictureViewModel)
        }

        composable(NavRoute.PICTURE_DETAIL_PAGE.route) {<!-- -->
            val position = it.arguments?.getString("position")?.toIntOrNull() ?: 0
            PictureDetailPage(ResourceHolder.pictureViewModel, position)
        }
    }
}


//Route defined by navHostController on the right
@Composable
fun PlaceholderNavRoute() {<!-- -->
    val navHostController = rememberNavController().also {<!-- -->
        ResourceHolder.navHostController2 = it
    }

    NavHost(navHostController, startDestination = NavRoute.EMPTY_PAGE.route) {<!-- -->
        composable(NavRoute.PICTURE_DETAIL_PAGE.route) {<!-- -->
            val position = it.arguments?.getString("position")?.toIntOrNull() ?: 0
            PictureDetailPage(ResourceHolder.pictureViewModel, position)
        }

        composable(NavRoute.EMPTY_PAGE.route) {<!-- -->
            EmptyPage()
        }
    }
}

So, how do we know which navHostController should perform routing jump at this time? You may need the following code, which defines when the Controller held by main or placerholder should be used.

NavigationViewModel.kt
/**
 * @param isMainRoute: false only supports opening the secondary page on the right
 */
fun setRoute(isMainRoute: Boolean = false, route: String) {<!-- -->
    safeRoute {<!-- -->
        if (twoPane.value & amp; & amp; isMainRoute.not()) {<!-- -->
            ResourceHolder.placeholderNavController?.navigate(route)
        } else {<!-- -->
            ResourceHolder.mainNavController?.navigate(route)
        }
    }
}

Through the above steps, we have basically built an application that can support screen size changes~
3. Others
If you need to sense full screen, split screen, small window, and screen rotation, we can define the following function:

@Composable
fun rememberFullScreen(
    predicate: () -> Boolean = {<!-- --> //Additional judgment conditions
        true
    }
): Boolean {<!-- -->
    val configuration = LocalConfiguration.current

    return remember(configuration) {<!-- -->
        configuration.orientation == Configuration.ORIENTATION_LANDSCAPE & amp; & amp; predicate.invoke()
    }
}

In the above solution, we defined two navHostController respectively. In fact, we can also define a global Controller and add the routing of ListWithDetail. Yes, as shown in the picture below. I highly recommend the following solution! ! ! But the cost won’t drop much.
The public account is being serialized simultaneously. If you think it is good, please follow it~