Build a Floating Action Button Menu in Jetpack Compose

Jetpack Compose, Google's modern toolkit for building native UI, has transformed Android development by offering a declarative approach. Among its various features, implementing a Floating Action Button (FAB) and extending it into a dynamic menu is a task every Android developer should master. In this blog post, we will explore advanced techniques to create a FAB menu using Jetpack Compose, leveraging best practices and optimizing for performance and user experience.

Why Use a FAB Menu?

The Floating Action Button is a widely recognized UI pattern that represents a primary action on a screen. Extending it into a FAB menu can:

  • Improve navigation: Allow access to multiple related actions from a single entry point.

  • Save screen space: Keep the UI clean and uncluttered while providing quick access to key features.

  • Enhance user experience: Add interactive and visually appealing animations that delight users.

Jetpack Compose simplifies the creation of such components, making it easier to build, animate, and manage the state of a FAB menu.

Setting Up Your Jetpack Compose Project

Before diving into the implementation, ensure your project is ready to use Jetpack Compose. Add the necessary dependencies to your build.gradle file:

// Module-level build.gradle
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

dependencies {
    implementation "androidx.compose.ui:ui:1.x.x"
    implementation "androidx.compose.material:material:1.x.x"
    implementation "androidx.compose.animation:animation:1.x.x"
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.x.x"
    implementation "androidx.activity:activity-compose:1.x.x"
}

Update your minSdk version to at least 21 as Compose requires it.

Building the FAB Menu

Step 1: Define the FAB Menu State

A FAB menu typically toggles between open and closed states. Use a MutableState to track this state:

@Composable
fun rememberFabMenuState(): MutableState<Boolean> {
    return remember { mutableStateOf(false) }
}

Step 2: Create the Main FAB

The primary FAB acts as the trigger for the menu. Use the FloatingActionButton component and animate its icon based on the menu state:

@Composable
fun MainFab(isMenuOpen: Boolean, onClick: () -> Unit) {
    FloatingActionButton(onClick = onClick) {
        Icon(
            imageVector = if (isMenuOpen) Icons.Default.Close else Icons.Default.Add,
            contentDescription = null
        )
    }
}

Step 3: Design the FAB Menu Items

Define the menu items as a list of composables, each representing a distinct action. Use animations to position and reveal them:

@Composable
fun FabMenuItems(isMenuOpen: Boolean, menuItems: List<Pair<String, () -> Unit>>) {
    Column(
        verticalArrangement = Arrangement.spacedBy(16.dp),
        modifier = Modifier.padding(bottom = 72.dp) // Adjust to position items correctly
    ) {
        menuItems.forEachIndexed { index, item ->
            AnimatedVisibility(
                visible = isMenuOpen,
                enter = slideInVertically(initialOffsetY = { it * (index + 1) }) + fadeIn(),
                exit = slideOutVertically(targetOffsetY = { it * (index + 1) }) + fadeOut()
            ) {
                FloatingActionButton(onClick = item.second) {
                    Text(item.first)
                }
            }
        }
    }
}

Step 4: Combine the Components

Bring everything together in a parent composable:

@Composable
fun FabMenu() {
    val isMenuOpen = rememberFabMenuState()

    Box(modifier = Modifier.fillMaxSize()) {
        // Place FAB menu items
        FabMenuItems(
            isMenuOpen = isMenuOpen.value,
            menuItems = listOf(
                "Action 1" to { /* Action 1 logic */ },
                "Action 2" to { /* Action 2 logic */ },
                "Action 3" to { /* Action 3 logic */ }
            )
        )

        // Place main FAB at the bottom-right corner
        Box(
            modifier = Modifier
                .fillMaxSize()
                .padding(16.dp),
            contentAlignment = Alignment.BottomEnd
        ) {
            MainFab(isMenuOpen = isMenuOpen.value) {
                isMenuOpen.value = !isMenuOpen.value
            }
        }
    }
}

Advanced Techniques and Best Practices

1. Use Motion and Animations Thoughtfully

Jetpack Compose’s animation APIs like AnimatedVisibility, animateFloatAsState, and Modifier.animateContentSize() allow smooth transitions. Avoid excessive animations that may slow down the app.

2. Optimize for Accessibility

  • Add meaningful contentDescription to all icons and buttons.

  • Ensure keyboard navigation and screen readers work seamlessly.

FloatingActionButton(
    onClick = onClick,
    contentDescription = if (isMenuOpen) "Close menu" else "Open menu"
) {
    Icon(
        imageVector = if (isMenuOpen) Icons.Default.Close else Icons.Default.Add,
        contentDescription = null
    )
}

3. Maintain Code Reusability

Extract reusable components and avoid hardcoding values. Use @Composable functions to encapsulate UI logic and parameters for high configurability.

4. Handle Performance Efficiently

  • Use LazyColumn if the menu has a large number of items.

  • Avoid overloading recompositions by properly scoping remember.

Testing Your FAB Menu

Testing composables is essential for maintaining robust UI:

Write Unit Tests

Use Compose testing libraries to validate UI behavior:

@get:Rule
val composeTestRule = createComposeRule()

@Test
fun testFabMenuToggle() {
    composeTestRule.setContent {
        FabMenu()
    }

    // Assert initial state
    composeTestRule.onNodeWithContentDescription("Open menu").assertExists()

    // Perform click and assert new state
    composeTestRule.onNodeWithContentDescription("Open menu").performClick()
    composeTestRule.onNodeWithContentDescription("Close menu").assertExists()
}

Conclusion

Building a Floating Action Button menu in Jetpack Compose is both intuitive and powerful. By leveraging state management, animations, and Compose’s modern UI toolkit, you can create engaging and user-friendly FAB menus for your Android applications. Implement the best practices outlined here to ensure performance, accessibility, and scalability in your apps.

Feel free to explore and extend this example to fit your app’s unique requirements. Happy coding!