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()...
This works fine for three or four screens, but can quickly become unmanageable and good solutions can get forgotten.

If you launch the app from a device later, there's no way to check other solutions without rebuilding the project - obviously not ideal.

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*

* 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()
}
The interface is Parcelable to deal with Process Death.

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")
}
},
}
Using enums instead of sealed classes gives us a few advantages:

  • 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 

Saves the currently selected screen (MenuItem) or null and controls the BackHandler so it we can navigate back to the menu screen. Using rememberSaveable allows us to deal with process death.
@Composable
private fun rememberSelectedExampleBackHandler(): MutableState<MenuItem?> {
val selected =
rememberSaveable { mutableStateOf<MenuItem?>(null) }

BackHandler(enabled = selected.value != null) {
selected.value = null
}

return selected
}

Menu Buttons

The sets need to be manually added to ButtonsManual which uses ButtonSet to arrange and display the buttons. It's easy to adjust how this displays. I'll let the code speak for itself.
@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

ShowMenu tracks our current displayed page or displays MainMenu if null.

@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)
}

As well as the Menu itself, there's also a way to access individual components if you want to specifically test those.

From MainActivity, launch the menu via LightNavMenuV1.MenuApp() or specific activities via SpecificAppScaffolded() and SpecificApp(). 
@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:

This version doesn't have any animation so the transitions are instant. V2 has some added features that make things a little nicer. The animation features can be easily added into V1.

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

Popular Posts