Light Navigation Front End - LightNavMenu v1
Light Navigation Menu v1
A Lightweight Front-End Test Menu for Jetpack Compose
Why This Exists
I like to spin up small test projects and often need to manage many different versions. I used to switch screens manually in my MainActivity, like this:
Surface(
) {
// Project01()
// Project02() // this is interesting
// Project03()
// Project04() // Don't use this one
// Project05()
// AmazingTest001() // Best version before 32
// AmazingTest002()
// AmazingTest032() // Keep this one?
// AmazingTest042() // is this the right one?
// Project01()
// Project01()...
Solution - LightNavMenu
A lightweight, zero-dependency navigation menu where you can keep references to all your tests / apps.
This is not meant to replace proper navigation for real applications, but is is purely a clean, simple tool you can paste into any project to quickly organize pages, demos, or experiments.
Here are the main advantages to this design:
- Extremely lightweight
- Easy to use:
- Auto-generated set menu entries
- Easy to copy/paste between projects
- Easy customization for sets
- Infinitely nestable
- Survives process death*
- Auto-generated set menu entries
- Easy to copy/paste between projects
- Easy customization for sets
* Scroll position inside screens is not restored — only which screen you’re on
Full code listing - LightNavMenu v1 GitHub Gist
V1 Overview
- Home -> Page Sets -> Pages (or Apps if you prefer)
Pages (or Apps) are grouped into sets to easily find similar tests or projects.
The Interface
private interface MenuItem: Parcelable {
val buttonLabel: String
@Composable fun Content()
}
Example Set (using MenuItem Interface)
Here is an example of a set containing two pages / apps.
@Parcelize
private enum class ExampleSet1: MenuItem {
EXAMPLE1 {
override val buttonLabel: String = "Example 1"
@Composable
override fun Content() {
SharedExampleScreens.ExampleScreen1()
}
},
EXAMPLE2 {
override val buttonLabel: String = "Example 2"
@Composable
override fun Content() {
SharedExampleScreens.ExampleScreen2("from LNM v1")
}
},
}
- built-in .entries (so we can automatically collect content)
- compile-time completeness
- no need to manually maintain lists
- can be easily copied and adjusted if more sets need adding
rememberSelectedExampleBackHandler
@Composable
private fun rememberSelectedExampleBackHandler(): MutableState<MenuItem?> {
val selected =
rememberSaveable { mutableStateOf<MenuItem?>(null) }
BackHandler(enabled = selected.value != null) {
selected.value = null
}
return selected
}
Menu Buttons
@Composable
private fun MenuItemButton(
entry: MenuItem,
onSelected: (entry: MenuItem) -> Unit,
) {
Button(
onClick = { onSelected(entry) }
) {
Text(entry.buttonLabel)
}
}
@Composable
private fun ButtonSet(
title: String,
items: List<MenuItem>,
onSelected: (entry: MenuItem) -> Unit
) {
Text(title)
items.forEach {
MenuItemButton(it, onSelected)
}
Spacer(Modifier.padding(16.dp))
}
@Composable
private fun ButtonsManual(
onSelected: (entry: MenuItem) -> Unit
) {
ButtonSet(
title = "Set 1",
items = ExampleSet1.entries,
onSelected
)
ButtonSet(
title = "Set 2",
items = ExampleSet2.entries,
onSelected
)
}
Menu App
@Composable
private fun MainMenu(selectedItem: MutableState<MenuItem?>) {
val onSelected: (entry: MenuItem) -> Unit =
{ entry -> selectedItem.value = entry }
MenuScaffold{
Title()
Spacer(Modifier.padding(8.dp))
ExtraContent()
ButtonsManual(onSelected)
}
}
@Composable
fun ShowMenu() {
val selectedItem = rememberSelectedExampleBackHandler()
selectedItem.value?.Content() ?: MainMenu(selectedItem)
}
@Composable
fun SpecificApp() { // manually change as required
ExampleSet1.EXAMPLE1.Content()
}
@Composable
fun SpecificAppScaffolded() {
Scaffold {
Column(
Modifier.padding(it)
) {
SpecificApp()
}
}
}
@Composable
fun MenuApp() {
HomeScreen.ShowMenu()
}
Demonstration
Here's a short clip of the menu in action:
NavMenuV1 Code Listing
The full version is available on my LightNavMenu v1 GitHub Gist.
You’ll notice the entire implementation is wrapped inside an object. This allows you to:
- copy different versions around easily and avoid name
- quickly experiment with multiple menu structures
Background and Example Screens
I've made a separate Gist for the Background and for the Example Screens.
Dependencies
Project and app gradle files need parcelize (apply false for project only):
Project:
plugins {
...
// added
alias(libs.plugins.kotlin.parcelize) apply false
}
App:
plugins {
...
// added
alias(libs.plugins.kotlin.parcelize)
}
You will also need this in your libs.versions.toml:
[plugins]
...
# added
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
Development
Design Decisions
I have a similar menu I use for real projects (based on the Navigation library) which works a little differently. In that, the UI and all the menu items are stored externally. This means I can use one menu template and change the UI and data as required.
With LightNavMenu, the reason everything is stored within one object here is so you can easily move things around and copy them. As soon as you have multiple files, this gets a little more troublesome. If you only have one menu in the project, it's very easy to extract the UI and the DataItems into other files if preferred.
I prioritized simplicity and easy of use / customization. You have a lot of flexibility to adapt this as required.
BackStack Mistake
While I was first working on this, I ended up having a BackStack list of activities. I only really thought about it properly when I started dealing with animations (part v2) and then used a single reference to the current content instead.
Rather than a BackStack, a single reference is all we need as we can embed menus within menus.
All Links
More - v2
I aim to follow up with the v2 post fairly soon. I'll update this with a link once it is ready. If you use V1 and want to change to V2 in the future, you can keep the exact same data structures and customization very easily so there's no reason not to give v1 a try!

Comments
Post a Comment