Lawnchair design patterns
The Lawnchair module (everything found in lawnchair) represents the modern layer of the project. While the AOSP modules focuses on system-level constraints, this zone utilizes Modern Android Development (MAD) tools like Kotlin and Jetpack Compose.
However, Lawnchair follows a pragmatic MAD approach. Because we are bridging a modern UI with a legacy platform, we often prioritize development velocity and direct state access over strict architectural purity (such as "clean" MVVM or heavy Dependency Injection).
Preference-driven architecture
Most of Lawnchair's customization is driven by a centralized preference system. This is the "brain" of the launcher.
The Pattern:
prefs(Legacy): UsesSharedPreferencesfor basic types. Avoid adding new keys here.prefs2(Modern): UsesPreference Datastore. This is the preferred location for all new settings, supporting non-primitive data and default values fromconfig.xml.The preference key is the single source of truth. The UI and the launcher logic both observe the key to ensure consistency.
The manager pattern
Lawnchair utilizes thread-safe singletons to manage persistent subsystems that need to be accessible from both the View-based AOSP code and the Compose-based UI layer.
The Pattern:
Access major subsystems (e.g.,
FontCache,IconCache,LauncherAppState) via their staticINSTANCEorgetInstance(context)methods.These managers often handle their own internal caching and state invalidation based on system changes.
Pragmatic state management
In many modern apps, state flows from a data source through a repository to a ViewModel. In Lawnchair, we often bypass these layers to directly link the UI to our preference systems or internal registries.
Our UI structure follows a 70/20/10 complexity rule:
For simple settings, which use standard toggles and sliders, use direct state access via
getAdapter()in the Composable. Avoid ViewModels or extra layers.For more complex screens with localized logic, such as search filters or file picking, use
remember,derivedStateOf, andproduceStatewithin the Composable.Screens that mix multiple data sources or remote APIs must follow a full MVVM pattern using
AndroidViewModelandStateFlowto ensure the code remains readable.
Tips
Use
getAdapter()to bridge persistent storage and the UI. This provides aPreferenceAdapterthat handles state updates and writes automatically.Avoid creating ViewModels for simple settings screens. Use localized state or direct preference access instead.
Utilize
preferenceManager2()andpreferenceManager()top-level functions within Compose to access data.
Example:
Hybrid UI and interoperation
Lawnchair is in a state of "Compose-ification." We are incrementally migrating a View-based system to Jetpack Compose. This requires a hybrid approach where the two systems coexist and share state.
The Pattern:
Use
ComposeViewwithin XML layouts or programmatically to host new UI components inside legacy Launcher3 areas (e.g., custom search results).Use
AndroidViewwithin Compose to host legacy components that are too complex to rewrite immediately.Pass state objects or callbacks across the boundary. Avoid complex reactive streams (like RxJava) across this boundary unless already present in the legacy code.
Direct UI-coupled logic
For the majority of the project, we couple business logic and navigation directly to the UI components. This reduces the boilerplate required to manage a large number of independent settings and small features.
We only adopt strict MAD principles or MVVM if a screen is literally impossible to understand without them.
The Pattern:
Pass
NavControlleror state holders directly into Composable functions.Use
rememberandderivedStateOfwithin the UI layer to handle complex presentation logic that would traditionally live in a ViewModel.Directly trigger side effects (like updating system settings or restarting the launcher) from UI callbacks.
Example:
Simplified dependency injection
While we use frameworks like Dagger/Hilt in some areas, we frequently rely on singleton "Managers" and top-level getInstance(context) calls for core launcher components.
The Pattern:
Access major subsystems (e.g.,
IconCache,LauncherAppState) via their staticINSTANCEorgetInstance()methods.Use
LocalContext.currentin Compose to fetch the required context for these singleton accessors.Prioritize simplicity and fast initialization over strict constructor injection in the UI layer.
Last updated
Was this helpful?
