feat: add WindowSizeClass system for responsive layout (Closes #1) #40

Merged
shahondin1624 merged 1 commits from feature/issue-1-window-size-classification-system-1773402694 into main 2026-03-13 12:54:19 +01:00
3 changed files with 95 additions and 0 deletions

View File

@@ -9,6 +9,7 @@ import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.runtime.rememberCoroutineScope
@@ -23,6 +24,8 @@ import org.shahondin1624.lib.components.charactermodel.attributespage.Attributes
import org.shahondin1624.model.EXAMPLE_CHARACTER
import org.shahondin1624.theme.AppTheme
import org.shahondin1624.theme.LocalThemeIsDark
import org.shahondin1624.theme.LocalWindowSizeClass
import org.shahondin1624.theme.WindowSizeClass
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@@ -30,6 +33,20 @@ import org.shahondin1624.theme.LocalThemeIsDark
fun App(
onThemeChanged: @Composable (isDark: Boolean) -> Unit = {}
) = AppTheme(onThemeChanged) {
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
val windowSizeClass = WindowSizeClass.fromWidth(maxWidth)
CompositionLocalProvider(LocalWindowSizeClass provides windowSizeClass) {
AppContent(onThemeChanged = {})
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun AppContent(
onThemeChanged: @Composable (isDark: Boolean) -> Unit = {}
) {
val character = EXAMPLE_CHARACTER
var isDark by LocalThemeIsDark.current
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)

View File

@@ -0,0 +1,36 @@
package org.shahondin1624.theme
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
/**
* Classification of window widths following Material Design 3 breakpoints.
*
* - [Compact] : width < 600 dp (phones in portrait)
* - [Medium] : 600 dp <= width <= 840 dp (tablets, foldables)
* - [Expanded] : width > 840 dp (desktops, landscape tablets)
*/
enum class WindowSizeClass {
Compact,
Medium,
Expanded;
companion object {
/**
* Returns the [WindowSizeClass] for the given [width].
*/
fun fromWidth(width: Dp): WindowSizeClass = when {
width < 600.dp -> Compact
width <= 840.dp -> Medium
else -> Expanded
}
}
}
/**
* CompositionLocal providing the current [WindowSizeClass].
* Set at the app root via [BoxWithConstraints] so every composable
* in the tree can read it without parameter drilling.
*/
val LocalWindowSizeClass = compositionLocalOf { WindowSizeClass.Expanded }

View File

@@ -0,0 +1,42 @@
package org.shahondin1624
import androidx.compose.ui.unit.dp
import org.shahondin1624.theme.WindowSizeClass
import kotlin.test.Test
import kotlin.test.assertEquals
class WindowSizeClassTest {
@Test
fun compactForWidthBelow600() {
assertEquals(WindowSizeClass.Compact, WindowSizeClass.fromWidth(0.dp))
assertEquals(WindowSizeClass.Compact, WindowSizeClass.fromWidth(320.dp))
assertEquals(WindowSizeClass.Compact, WindowSizeClass.fromWidth(599.dp))
}
@Test
fun mediumForWidth600To840() {
assertEquals(WindowSizeClass.Medium, WindowSizeClass.fromWidth(600.dp))
assertEquals(WindowSizeClass.Medium, WindowSizeClass.fromWidth(720.dp))
assertEquals(WindowSizeClass.Medium, WindowSizeClass.fromWidth(840.dp))
}
@Test
fun expandedForWidthAbove840() {
assertEquals(WindowSizeClass.Expanded, WindowSizeClass.fromWidth(841.dp))
assertEquals(WindowSizeClass.Expanded, WindowSizeClass.fromWidth(1200.dp))
assertEquals(WindowSizeClass.Expanded, WindowSizeClass.fromWidth(1920.dp))
}
@Test
fun boundaryAt600() {
assertEquals(WindowSizeClass.Compact, WindowSizeClass.fromWidth(599.9.dp))
assertEquals(WindowSizeClass.Medium, WindowSizeClass.fromWidth(600.dp))
}
@Test
fun boundaryAt840() {
assertEquals(WindowSizeClass.Medium, WindowSizeClass.fromWidth(840.dp))
assertEquals(WindowSizeClass.Expanded, WindowSizeClass.fromWidth(840.1.dp))
}
}