Adopt Material 3 Expressive: adaptive navigation, list-detail pane, expressive components#484
Merged
Conversation
Documents the plan to adopt adaptive navigation (NavigationSuiteScaffold + adaptive-navigation3 list-detail), MaterialExpressiveTheme with the expressive motion scheme, and Expressive components on the value-editor screens. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Enum option lists can be long, so use a proper scrollable selectable list instead of ToggleButtonGroup, gated behind a dogfooded boolean feature toggle (read via toggles-flow) so the new behavior can be tested at runtime. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Task-by-task plan covering adaptive navigation, list-detail pane, expressive components on the value editors, and the toggle-gated enum list, derived from the approved design spec. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
MaterialExpressiveTheme and MotionScheme are public in material3 1.5.0-alpha20 (not yet in 1.4.0/BOM 2026.05.01), so pin material3 explicitly. That version requires compileSdk/targetSdk 37 and Compose 1.12.0-alpha; pin Robolectric tests to SDK 36 to match Robolectric 4.16.x's maximum. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes the ModalNavigationDrawer/hamburger-menu pattern from ApplicationEntry and replaces it with NavigationSuiteScaffold, which automatically renders a bottom NavigationBar on compact screens and a NavigationRail on expanded screens. The Navigation 3 NavBackStack is hoisted into a new TogglesApp composable and passed down to Navigation(), keeping it as the single source of truth. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Wire ListDetailSceneStrategy into NavDisplay so the configurations list and value-editor entries render side-by-side on expanded screens and as push navigation on compact screens. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a dependency constraint on androidx.concurrent:concurrent-futures to require 1.2.0, resolving a consistent-resolution conflict between profileinstaller:1.4.0 (which pins 1.1.0) and androidx.test:core:1.7.0 (which requires 1.2.0) that caused assembleAndroidTest to fail. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment on lines
+158
to
163
| MaterialExpressiveTheme( | ||
| colorScheme = colorScheme, | ||
| typography = Typography, | ||
| motionScheme = MotionScheme.expressive(), | ||
| content = content | ||
| ) |
Owner
Author
There was a problem hiding this comment.
Not a problem. CI compiles with -Werror / allWarningsAsErrors and the build job is green — this call site emits no opt-in warning for MaterialExpressiveTheme or MotionScheme.expressive in the Compose version used here, so there is nothing to opt into.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Matches the design spec and the codebase convention; the toggle flow is ContentObserver-backed, so collection should pause while the activity is stopped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
resolveFlow ran resolver.resolve() — including ContentResolver insert mutations for a missing configuration — on the collector's thread. For the in-process dogfood case there is no binder hand-off, so it ran on main and Room's main-thread guard crashed. Add .flowOn(ioDispatcher) so resolution and its mutations always run on IO. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The value editors rendered the title twice: once in the TopAppBar (a static type label) and once as a headline in the body. Drop the body heading and set the TopAppBar title to the toggle key (viewState.title). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dialog-mode editors rendered their full-screen Scaffold inside the Dialog, so a short editor (e.g. boolean) stretched to ~full screen with a large empty area. Add a shared ToggleEditorDialog container that sizes to content, clamped to [200dp, 560dp]; list editors cap and scroll via weight(fill = !asDialog). The non-dialog (pane/full-screen) Scaffold path is unchanged. Scope editor still pending extraction into its own module. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Moves ScopeView/ScopeViewModel (and their string resources) out of toggles-app into a dedicated module, mirroring the other configuration editor modules, and gives the Scope editor the same content-wrapped dialog presentation as the others (asDialog branch + LazyColumn weight(fill = !asDialog)). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The detailPlaceholder was a bare Text, so on a tablet it rendered top-center overlapping the status bar instead of centred in the detail pane. Wrap it in a fillMaxSize Box with center alignment and a muted style. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…njection CI compiles with -Werror and lint warningsAsErrors. Suppress the deprecated ButtonGroup overload usage in the four editors (the supported overload needs a different overflow DSL; tracked as follow-up), reorder BooleanValueView params so the required asDialog precedes the defaulted viewModel (ComposeParameterOrder), and hoist MainViewModel to a Navigation parameter (ComposeViewModelInjection). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Regenerate baselines (NotShrinkingResources, SimilarGradleDependency, the relocated DenyListedApi for the extracted scope module, and noop/sample issues) so the warningsAsErrors lint pass is green. No source behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The androidTest source set also compiles with -Werror in CI; suppress the deprecated createEmptyComposeRule (the v2 overload swaps in StandardTestDispatcher and would change test timing). Unblocks the emulator-tests compile step. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Derive the highlighted destination from the back stack (so it stays correct after Back) and pop to an existing top-level route instead of appending duplicate entries on re-tap. Mark the nav-item icon contentDescription null since the label already provides the accessible name (avoids double announcement). Addresses Copilot review comments on PR #484. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ertion The test crashed in CI with 'component was not created' because the Hilt and compose rules had no explicit order; order HiltAndroidRule first and inject() in @before. Also replace the racy 'No applications found.' empty-state assertion — reading the dogfood toggle on launch self-registers this app — with a stable check that the Applications home renders. Verified locally on an emulator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ted up from) Per Material guidance a dialog is dismissed, not navigated up from, so it has no up/back navigation icon. Remove the navigationIcon from ToggleEditorDialog; the editors are dismissed via their own Revert/Save actions, the scrim, or system back. The non-dialog pane/full-screen path keeps the up-arrow (it is a navigated destination). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The configurations list previously jammed a Material 3 SearchBar (a component designed to expand into an overlay) into a TopAppBar title with expanded=false and empty content, plus an unremembered `searching` flag that reset every recomposition. Swap it for the proper M3 Expressive search components (material3 1.5.0-alpha20): AppBarWithSearch in the top bar (keeping the back navigation icon and overflow menu) and ExpandedFullScreenSearchBar for the expanded results surface, both sharing one inputField bound to a TextFieldState. The TextFieldState bridges into the existing ViewModel query pipeline via snapshotFlow, so the SavedStateHandle-backed queryString/Room LIKE filter remains the source of truth. Also tidy ConfigurationViewModel: TextUtils.isEmpty -> isEmpty(). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Use collectAsStateWithLifecycle() to seed the search text field instead of reading StateFlow.value directly in composition. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ode) In adaptive list-detail layout, the back arrow in the configurations detail pane should only appear in single-pane mode. Use LocalListDetailSceneScope and its scaffoldTransitionScope.targetState.secondary to detect whether the list pane is currently visible and suppress the navigation icon accordingly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…e mode) Adds rememberShowNavigationIconInDetailPane/ExtraPane() helpers to compose-theme that check LocalListDetailSceneScope to determine whether the pane a navigation icon would lead back to is already visible. All five leaf editors (Boolean, String, Integer, Enum, Scope) now suppress their back arrow in three-pane mode. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ane state PaneScaffoldTransitionScope does not expose targetState directly; it must be accessed via scaffoldStateTransition (a Compose Transition<ThreePaneScaffoldValue>) which does expose targetState. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Split the pane-state read across lines to stay under the 120-char limit, and render the back arrow via an always-present navigationIcon lambda with a conditional body (matching the leaf editors) instead of a nullable if-expression. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
toggles-flow observes the Toggles ContentProvider via a callbackFlow. If a contentResolver.query() throws (provider process died, mid-upgrade, or — under instrumentation tests — the provider's Hilt component torn down between tests), the exception propagated out of the flow and crashed the host app. This is what broke the emulator-tests job: ExampleInstrumentationTest2 launches MainActivity, which dogfoods the editor-presentation toggle through toggles-flow, and the background observation raced with Hilt teardown. Wrap the three provider queries in safeQuery(), treating a thrown RuntimeException like a null cursor so the client degrades to the supplied default instead of crashing. The intentional error() invariants in queryConfiguration (which fire after a successful query) are preserved. Adds ProviderThrowsTest, which registers a provider that throws on query and asserts the flow still emits the default. Verified it fails without the fix with the same IllegalStateException signature seen in CI. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
ModalNavigationDrawerwithNavigationSuiteScaffold— auto-renders a bottomNavigationBaron compact screens and aNavigationRailon expanded screensmaterial3-adaptive-navigation3scene strategy toNavDisplayso the configurations list and value editors render side-by-side on expanded screens (tablet/foldable) and as push navigation on compactMaterialExpressiveTheme: swapsMaterialThemeforMaterialExpressiveThemewithMotionScheme.expressive()throughout the app; requires material3 1.5.0-alpha20 (pinned — public Expressive APIs not yet in BOM 2026.05.01) and compileSdk/targetSdk 37Switch+ button row replaced withToggleButtonGroup(Off/On) +ButtonGroup(Revert/Save)ButtonGroupEnumValueViewModelinjectsToggles(via the already-wired Hilt binding) and reads"expressive_enum_list"as a reactive boolean flag; when ON the enum option list showsRadioButtonselection instead of theLinkicon — flip the toggle under these.eelde.togglesapp to compare at runtimeDependency changes
2026.03.01→2026.05.011.5.0-alpha20(Expressive APIs public here, not in 1.4.0/BOM)material3-adaptive-navigation-suite,material3-adaptive,material3-adaptive-layout,material3-adaptive-navigation3 1.3.0-beta02@Config(sdk = [36])) to match Robolectric 4.16.x maxTest plan
./gradlew detektpasses./gradlew testpasses./gradlew assembleAndroidTestcompilesNavigationBarwith Applications / Licenses / Help; tapping a toggle pushes the value editor full-screen; back returns to the listNavigationRailvisible; configurations list and value editor render side-by-sideLinkicon list); in Toggles opense.eelde.togglesapp → flipexpressive_enum_listON; reopen enum config →RadioButtonlist🤖 Generated with Claude Code