A text-only Android home screen replacement. No wallpaper. No icons. No distractions.
Everything you need to navigate your phone — apps, contacts, settings — accessible as plain text, fast, and without visual noise.
Modern launchers are built around icons, widgets, animations, and notifications competing for your attention. Minimalist Launcher removes all of that. Every element on screen is a word or a number. You read it and act. Nothing else happens.
The design constraint is intentional: if something can't be expressed as text, it doesn't belong on the home screen.
The app registers as an Android home screen replacement. Pressing Home opens this app. The back button is swallowed on the home screen, as expected of any launcher.
All installed apps listed as plain text in a LazyColumn. Alphabetical and frequency-based sorting. Long-pressing any app opens a bottom sheet: pin, hide, group, or restrict. Hidden apps are stored in DataStore and filtered at query time. Icons are never loaded or rendered.
A single search bar at the top of the app drawer filters three sources simultaneously as you type:
- Apps — by label, debounced 150 ms
- Contacts — via
ContactsContract, permission requested lazily on first use - Settings — a static map of 17 common settings actions (Wi-Fi, Bluetooth, Display, etc.)
Pull down anywhere on the app list to re-focus the search bar. Back button clears search. Search auto-clears on launch or home press.
The home screen shows:
- Clock — large text, tappable to toggle 12 h / 24 h format
- Date — below the clock in a lighter weight
- Pinned shortcuts — up to 5 apps or contacts, stored in DataStore; long-press to remove; pin from the app drawer via long-press → "pin"
Swiping left from the home screen opens the app drawer. Swiping up launches Google Search. Pressing Home from anywhere returns to the home screen and clears any active search.
| Gesture | Action |
|---|---|
| Swipe left | Open app drawer |
| Swipe right | Return to home screen |
| Swipe up (home screen) | Google Search |
| Pull down (app list) | Focus search bar |
| Tap app | Launch |
| Long-press app | Pin · Hide · Group · Restrict |
| Tap clock | Toggle 12 h / 24 h |
| Long-press pinned item | Remove shortcut |
| Back (search active) | Clear search |
| Back (drawer, no search) | Return home |
Three-module Gradle project:
MinimalistLauncher/
├── app/ Activity, Application class, theme
├── core/ Data layer — repositories, models
└── feature/ UI layer — ViewModel, screen, composables
| Layer | Technology |
|---|---|
| Language | Kotlin 2.0 |
| UI | Jetpack Compose (BOM 2024.06) |
| State | StateFlow + combine |
| Storage | DataStore Preferences |
| Async | Kotlin Coroutines |
| Min SDK | API 26 (Android 8.0) |
| Target SDK | API 35 |
PackageManager ──► AppRepository ──► AppDrawerViewModel ──► AppDrawerScreen
DataStore ──► PreferencesRepository ──► │
ContactsContract ► ContactsRepository ──► │
▼
HorizontalPager
/ \
HomeScreenPage AppDrawerPage
The uiState StateFlow is built in two layers:
rawUiState— 5-flowcombineonDispatchers.Default: app list + preferences + selection state + raw search query + debounced search resultsuiState— 2-flowcombinelayering clock + pins on top ofrawUiState
This split ensures the 1-second clock tick never re-runs app filtering or sorting.
No icons, ever. loadIcon() and loadDrawable() are never called. queryIntentActivities is used for app discovery, not getInstalledPackages. This eliminates an entire class of memory and threading problems.
Duplicate package deduplication. queryIntentActivities returns one result per launcher Activity, not per package. On OEM devices (e.g. Realme/OPPO) a single package can expose two launcher activities, producing duplicate keys in the LazyColumn. The fix: .distinctBy { it.packageName } in AppRepository.
TextFieldValue over String in BasicTextField. Using a plain String as the BasicTextField value resets the cursor to position 0 on every recomposition. The second typed character then inserts before the first. TextFieldValue is held in local Compose state; only the text string is forwarded to the ViewModel.
Debounced search, split by cost. App and settings filtering runs immediately (in-memory, negligible). Contact queries hit ContentProvider, so they are debounced 150 ms via a shared StateFlow. Both resolve together so results appear in one batch.
Clock tick without re-filtering. The clock emits every second. Combining it directly with the app-list state would re-run filtering/sorting every second. Instead, rawUiState builds the sorted app list independently; a second combine layer adds the clock output via base.copy(...). Only the time strings are updated each tick.
Contacts permission requested lazily. READ_CONTACTS is declared in the manifest but never pre-requested. A LaunchedEffect inside the search page fires once when the query becomes non-empty and checks checkSelfPermission. If not granted, the system dialog appears. After approval, contactsVersion increments, which forces flatMapLatest to re-run the contact query with the current search text.
BackHandler priority via LIFO. MainActivity registers an always-enabled OnBackPressedCallback to swallow back presses (required for a home screen). Compose's BackHandler registers after that callback and therefore has higher priority (LIFO). When the search bar is active or the drawer is open, BackHandler intercepts the press instead.
| Step | Feature |
|---|---|
| 5 | Theming — Light / Dark / AMOLED, custom colors, font size, text alignment |
| 6 | Text widgets — weather (OpenWeatherMap) and next calendar event |
| 7 | Configurable gestures — remap swipe up/down/left/right/double-tap |
| 8 | Focus profiles — Work / Personal / Sleep with time-based scheduling |
| 9 | Usage tracking — per-app limits, time windows, friction screen, daily reports |
| 10 | Scratch pad, app lock (biometric/PIN), backup & restore, zero-telemetry audit |
All permissions are declared upfront and requested lazily — only when the feature that needs them is first used.
| Permission | Used for | When requested |
|---|---|---|
QUERY_ALL_PACKAGES |
App discovery | On launch |
READ_CONTACTS |
Contact search | First search |
READ_CALENDAR |
Calendar widget | Step 6 |
PACKAGE_USAGE_STATS |
Usage tracking | Step 9 |
SCHEDULE_EXACT_ALARM |
Profile scheduling | Step 8 |
USE_BIOMETRIC |
App lock | Step 10 |
INTERNET |
Weather API | Step 6 |
# Clone and open in Android Studio, or build from the command line:
cd MinimalistLauncher
./gradlew assembleDebug
# Install on a connected device
./gradlew installDebugRequires Android SDK with API 35 build tools. No external accounts or API keys needed for Steps 1–4.
Contributions are welcome when they preserve the launcher philosophy: text-only, distraction-free, and local-first.
Start with CONTRIBUTING.md, use the issue templates for bugs and feature requests, and follow the Code of Conduct. Before opening a pull request, run:
cd MinimalistLauncher
./gradlew checkFor security issues, please follow SECURITY.md instead of opening a public issue.
core/
data/
AppRepository.kt — app discovery, launch, dialer, settings, Google search
ContactsRepository.kt — contact search with permission guard
PreferencesRepository.kt — DataStore: sort order, hidden apps, launch counts,
clock format, 5 pinned slots
SettingsActions.kt — static map of 17 settings intents
model/
AppInfo.kt — package name + label
ClockFormat.kt — HOUR_12 / HOUR_24
PinnedItem.kt — App(packageName, label) | Contact(name, number)
SearchResult.kt — App | Contact | Setting
SortOrder.kt — ALPHABETICAL | FREQUENCY
feature/appdrawer/
AppDrawerUiState.kt — single source of truth for all UI state
AppDrawerViewModel.kt — combines 7 upstream flows into one StateFlow
AppDrawerScreen.kt — HorizontalPager + all composables
app/
LauncherApplication.kt — lazy repository singletons + DataStore delegate
MainActivity.kt — single activity, back-press handling, onNewIntent
ui/theme/ — Material3 color scheme, typography
MIT