With common xml approach we are used to using qualifiers for our layouts. But what we can do with compose layouts?
What’s about?
In this article I want to share my convenient way to handle screen configuration changes
Let’s write some code. A little bit
Whole our logic are going to fit in one file
Create file DeviceScreenConfiguration.kt, inside declare data class with the same name. Inside this class let’s declare three more enum classes: DeviceScreenOrientation and DeviceScreenSize
data class DeviceScreenConfiguration(
val size: DeviceScreenSize,
val orientation: DeviceScreenOrientation
) {
enum class DeviceScreenOrientation {
Portrait,
Landscape
}
enum class DeviceScreenSize(val screenWidthRange: IntRange) {
...
}
...
}
If everything is clear with DeviceScreenOrientation class, we need to stop on DeviceScreenSize:
enum class DeviceScreenSize(val screenWidthRange: IntRange) {
Small(0 until 600),
Medium(600 until 720),
Big(720 until Int.MAX_VALUE);
var actualWidth: Int = -1
private set
companion object {
fun getScreenSize(height: Int, width: Int): DeviceScreenSize {
val actualWidth = minOf(height, width)
return values().firstOrNull {
it.screenWidthRange.contains(actualWidth)
}?.apply DeviceScreenSize@{
this@DeviceScreenSize.actualWidth = actualWidth
} ?: throw IllegalScreenSize()
}
private const val illegalScreenSizeMessage = "Screen size can not be less than 0"
class IllegalScreenSize : Throwable(illegalScreenSizeMessage)
}
}
For our example we have three sizes: Small — for handheld, Medium — for foldables and small tablets and Big — for big tablets. As parameter we declare ranges for our enums (for handhelds from 0 to 599, etc). Variable “actualWidth” will be actualized when we find suitable type for given width in Dp with function “getScreenSize”.
We also have a few more methods, one field and one enum class Config inside DeviceScreenOrientation class:
enum class Config {
SmallPortrait,
MediumPortrait,
BigPortrait,
SmallLandscape,
MediumLandscape,
BigLandscape
}
val config: Config
get() = when (orientation) {
DeviceScreenOrientation.Landscape -> {
when (size) {
DeviceScreenSize.Big -> Config.BigLandscape
DeviceScreenSize.Medium -> Config.MediumLandscape
DeviceScreenSize.Small -> Config.SmallLandscape
}
}
DeviceScreenOrientation.Portrait -> {
when (size) {
DeviceScreenSize.Big -> Config.BigPortrait
DeviceScreenSize.Medium -> Config.MediumPortrait
DeviceScreenSize.Small -> Config.SmallPortrait
}
}
}
val isMobile: Boolean
get() = this.size == DeviceScreenSize.Small
val isTablet: Boolean
get() = !isMobile
These only for easy interaction with screen config.
Config class declares variations for orientation mixed with size.
Now let’s write composable function to handling screen changes
@Composable
fun rememberScreenConfiguration(): DeviceScreenConfiguration {
val configuration = LocalConfiguration.current
val screenWidth by remember(key1 = configuration.screenWidthDp) {
mutableStateOf(configuration.screenWidthDp)
}
val screenHeight by remember(key1 = configuration.screenHeightDp) {
mutableStateOf(configuration.screenHeightDp)
}
val screenOrientation by remember(key1 = configuration.orientation) {
val orientation = when (configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> {
DeviceScreenOrientation.Landscape
}
else -> {
DeviceScreenOrientation.Portrait
}
}
mutableStateOf(orientation)
}
return DeviceScreenConfiguration(
size = DeviceScreenSize.getScreenSize(screenHeight, screenWidth),
orientation = screenOrientation
)
}
as you can see, we use “remember” function with key to indicate what changes we want to catch and handle.
Result
Now we can use this compose function in or project to customize screens for different sizes and orientation by calling it
Easy
You can check full code on my Github repo