SnapNotify

Project Url: iVamsi/SnapNotify
Introduction: ๐Ÿš€ A lightweight, thread-safe Snackbar library for Jetpack Compose with zero-ceremony setup. Show snackbars from anywhere with beautiful theming, queue management, and optional Hilt integration. 100% Kotlin.
More: Author   ReportBugs   
Tags:

Kotlin Compose License Maven Central Tests Coverage

A drop-in Snackbar solution for Jetpack Compose that brings back the simplicity of the View system while leveraging modern Compose patterns.

๐ŸŽฌ Demo

SnapNotify Demo SnapNotify in action: Simple messages, themed styling, custom designs, and queue management

๐Ÿš€ The Problem

Jetpack Compose's built-in Snackbar system requires significant boilerplate:

  • Manual SnackbarHostState management
  • Passing CoroutineScope everywhere
  • Complex setup in every screen
  • Thread-safety concerns when calling from background threads
  • Cumbersome queue management for multiple messages
// Traditional Compose approach - lots of boilerplate ๐Ÿ˜”
@Composable
fun MyScreen() {
    val snackbarHostState = remember { SnackbarHostState() }
    val scope = rememberCoroutineScope()

    Scaffold(
        snackbarHost = { SnackbarHost(snackbarHostState) }
    ) {
        // Your content
        Button(
            onClick = {
                scope.launch {
                    snackbarHostState.showSnackbar("Message")
                }
            }
        ) {
            Text("Show Snackbar")
        }
    }
}

โœจ The Solution

SnapNotify eliminates all the boilerplate with a clean, thread-safe API:

// SnapNotify approach - zero ceremony! ๐ŸŽ‰
@Composable
fun MyScreen() {
    Button(
        onClick = { SnapNotify.show("Message") }
    ) {
        Text("Show Snackbar")
    }
}

๐Ÿ›  Installation

Add to your build.gradle.kts:

dependencies {
    implementation("io.github.ivamsi:snapnotify:1.0.2")
}

Hilt Integration (Optional): SnapNotify works with or without Hilt! If you use Hilt, no additional setup needed.

๐ŸŽฏ Quick Start

1. Setup (Flexible Usage)

Wrap your content with SnapNotifyProvider where you want snackbar functionality:

App-wide snackbars:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            SnapNotifyProvider {
                MyAppContent() // Entire app has snackbar access
            }
        }
    }
}

Screen-specific snackbars:

@Composable
fun MyScreen() {
    SnapNotifyProvider {
        // Only this screen and its children can show snackbars
        ScreenContent()
    }
}

Feature-scoped snackbars:

@Composable
fun ShoppingCart() {
    Card {
        SnapNotifyProvider {
            // Snackbars appear within this card's bounds
            CartItems()
            AddToCartButton()
        }
    }
}

Custom styled snackbars:

@Composable
fun MyScreen() {
    SnapNotifyProvider(
        style = SnackbarStyle.error() // Pre-built error styling
    ) {
        ScreenContent()
    }
}

2. Usage (Anywhere in your app)

// Simple message
SnapNotify.show("Operation completed successfully!")

// With action button
SnapNotify.show("Error occurred", "Retry") { 
    retryOperation() 
}

// With custom duration
SnapNotify.show("Long message", duration = SnackbarDuration.Long)

// Clear all pending messages
SnapNotify.clearAll()

// Themed styling methods
SnapNotify.showSuccess("Operation completed!")
SnapNotify.showError("Something went wrong!")
SnapNotify.showWarning("Please check your input!")
SnapNotify.showInfo("Here's some information!")

// Themed methods with actions
SnapNotify.showSuccess("File saved!", "View") { openFile() }
SnapNotify.showError("Upload failed", "Retry") { retryUpload() }

// Custom styling for specific messages
val customStyle = SnackbarStyle(
    containerColor = Color(0xFF6A1B9A),
    contentColor = Color.White,
    actionColor = Color(0xFFE1BEE7)
)
SnapNotify.showStyled("Custom styled message!", customStyle)

๐ŸŒŸ Key Features

โœ… Flexible Setup

  • Use SnapNotifyProvider at any level of your app hierarchy
  • App-wide, screen-specific, or feature-scoped snackbars
  • No SnackbarHostState or CoroutineScope management needed

โœ… Thread-Safe

  • Call from ViewModels and Composables safely
  • Background thread support with proper synchronization
  • No more IllegalStateException crashes

โœ… Smart Queue Management

  • Handles multiple rapid Snackbar triggers gracefully
  • No overlapping or lost messages
  • Singleton architecture prevents duplicate displays

โœ… Lifecycle-Aware

  • Survives configuration changes
  • Prevents memory leaks
  • Automatic provider deduplication

โœ… Action Support

  • Optional action buttons with callbacks
  • Seamless integration with your business logic

โœ… Comprehensive Styling

  • Pre-built themes: Success, Error, Warning, Info with semantic colors
  • Themed methods: showSuccess(), showError(), showWarning(), showInfo()
  • Custom styling: showStyled() for per-message customization
  • Full customization: Colors, shapes, elevation, typography
  • Provider-level defaults: Set default styles for entire sections
  • Material3 integration: Seamless with your app's design system

๐Ÿ“– Detailed Usage

Basic Messages

// Simple text message
SnapNotify.show("Profile updated successfully!")

// Different durations
SnapNotify.show("Quick message", duration = SnackbarDuration.Short)
SnapNotify.show("Important info", duration = SnackbarDuration.Long)
SnapNotify.show("Persistent message", duration = SnackbarDuration.Indefinite)

Action Buttons

// With action callback
SnapNotify.show("Message deleted", "Undo") {
    undoDelete()
}

// Error handling with retry
SnapNotify.show("Network error", "Retry") {
    viewModel.retryNetworkCall()
}

// Clear pending messages when navigating or context changes
SnapNotify.clearAll()

๐ŸŽจ Themed Messages

Quick, semantic styling with pre-built themes:

// Success messages (green theme)
SnapNotify.showSuccess("Profile updated successfully!")
SnapNotify.showSuccess("File uploaded!", "View") { openFile() }

// Error messages (red theme)
SnapNotify.showError("Network connection failed!")
SnapNotify.showError("Save failed", "Retry") { attemptSave() }

// Warning messages (orange theme)
SnapNotify.showWarning("Low storage space!")
SnapNotify.showWarning("Unsaved changes", "Save") { saveChanges() }

// Info messages (blue theme)
SnapNotify.showInfo("New feature available!")
SnapNotify.showInfo("Update available", "Download") { startUpdate() }

๐ŸŽจ Custom Styling

Per-message custom styling:

val customStyle = SnackbarStyle(
    containerColor = Color(0xFF6A1B9A),
    contentColor = Color.White,
    actionColor = Color(0xFFE1BEE7),
    shape = RoundedCornerShape(16.dp),
    elevation = 12.dp
)

SnapNotify.showStyled("Custom styled message!", customStyle)
SnapNotify.showStyled("With action", customStyle, "Action") { doAction() }

Styling Options

Pre-built themes:

// Success theme (green)
SnapNotifyProvider(style = SnackbarStyle.success()) {
    // Content
}

// Error theme (red)
SnapNotifyProvider(style = SnackbarStyle.error()) {
    // Content
}

// Warning theme (orange)
SnapNotifyProvider(style = SnackbarStyle.warning()) {
    // Content
}

// Info theme (blue)
SnapNotifyProvider(style = SnackbarStyle.info()) {
    // Content
}

Custom styling:

SnapNotifyProvider(
    style = SnackbarStyle(
        containerColor = Color(0xFF9C27B0),
        contentColor = Color.White,
        actionColor = Color(0xFFE1BEE7),
        shape = RoundedCornerShape(16.dp),
        elevation = 12.dp,
        messageTextStyle = MaterialTheme.typography.bodyLarge.copy(
            fontWeight = FontWeight.Medium
        ),
        actionTextStyle = MaterialTheme.typography.labelLarge.copy(
            fontWeight = FontWeight.Bold
        )
    )
) {
    // Content with custom-styled snackbars
}

Clean Architecture Usage

Following best practices, handle UI notifications in the presentation layer:

// โœ… Recommended: ViewModels handle notifications
class ProfileViewModel : ViewModel() {
    private val repository = UserRepository()

    fun saveProfile() {
        viewModelScope.launch {
            try {
                repository.saveProfile()
                SnapNotify.showSuccess("Profile saved!")
            } catch (e: Exception) {
                SnapNotify.showError("Save failed", "Retry") { saveProfile() }
            }
        }
    }
}

// โœ… Clean: Repositories focus on data operations
class UserRepository {
    suspend fun saveProfile(): Result<Unit> {
        return try {
            api.updateProfile()
            Result.success(Unit)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

// โœ… Alternative: Direct from Composables
@Composable
fun SaveButton() {
    val viewModel: ProfileViewModel = hiltViewModel()

    Button(
        onClick = {
            viewModel.saveProfile()
            // SnapNotify calls handled in ViewModel
        }
    ) {
        Text("Save")
    }
}

๐Ÿ— Architecture & Best Practices

SnapNotify follows clean architecture principles with proper separation of concerns:

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   SnapNotify    โ”‚โ”€โ”€โ”€โ–ถโ”‚ SnackbarManager  โ”‚โ”€โ”€โ”€โ–ถโ”‚ SnapNotifyVM    โ”‚
โ”‚   (Public API)  โ”‚    โ”‚   (Singleton)    โ”‚    โ”‚   (ViewModel)   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                โ”‚                        โ”‚
                                โ–ผ                        โ–ผ
                       โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                       โ”‚  Message Queue   โ”‚    โ”‚ SnapNotify UI   โ”‚
                       โ”‚   (StateFlow)    โ”‚    โ”‚  (Composable)   โ”‚
                       โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Architectural Layers

  • Presentation Layer: ViewModels and Composables handle SnapNotify calls
  • Domain Layer: Use Cases return Result<T> or throw exceptions
  • Data Layer: Repositories focus purely on data operations
  • UI Layer: SnapNotifyProvider manages display and styling

Design Principles

  • Thread Safety: Internal mutex synchronization for concurrent access
  • Clean Architecture: UI concerns separated from business logic
  • Reactive Streams: StateFlow-based message queue management
  • Optional DI: Works with or without dependency injection frameworks
  • Single Responsibility: Each component has a focused purpose

๐Ÿ”ง Configuration

Custom Styling

SnapNotifyProvider(
    modifier = Modifier.padding(16.dp) // Custom positioning
) {
    MyAppContent()
}

Hilt Integration (Optional)

SnapNotify works without any DI framework. If you use Hilt in your project, it integrates seamlessly - no additional setup needed!

๐Ÿ“‹ API Reference

SnapNotify Object

object SnapNotify {
    // Basic messages
    fun show(message: String, duration: SnackbarDuration = SnackbarDuration.Short)
    fun show(
        message: String, 
        actionLabel: String, 
        onAction: () -> Unit,
        duration: SnackbarDuration = SnackbarDuration.Short
    )

    // Custom styled messages
    fun showStyled(
        message: String, 
        style: SnackbarStyle,
        duration: SnackbarDuration = SnackbarDuration.Short
    )
    fun showStyled(
        message: String, 
        style: SnackbarStyle,
        actionLabel: String, 
        onAction: () -> Unit,
        duration: SnackbarDuration = SnackbarDuration.Short
    )

    // Themed messages
    fun showSuccess(message: String, duration: SnackbarDuration = SnackbarDuration.Short)
    fun showSuccess(
        message: String, 
        actionLabel: String, 
        onAction: () -> Unit,
        duration: SnackbarDuration = SnackbarDuration.Short
    )

    fun showError(message: String, duration: SnackbarDuration = SnackbarDuration.Short)
    fun showError(
        message: String, 
        actionLabel: String, 
        onAction: () -> Unit,
        duration: SnackbarDuration = SnackbarDuration.Short
    )

    fun showWarning(message: String, duration: SnackbarDuration = SnackbarDuration.Short)
    fun showWarning(
        message: String, 
        actionLabel: String, 
        onAction: () -> Unit,
        duration: SnackbarDuration = SnackbarDuration.Short
    )

    fun showInfo(message: String, duration: SnackbarDuration = SnackbarDuration.Short)
    fun showInfo(
        message: String, 
        actionLabel: String, 
        onAction: () -> Unit,
        duration: SnackbarDuration = SnackbarDuration.Short
    )

    // Management
    fun clearAll()
}

SnapNotifyProvider Composable

@Composable
fun SnapNotifyProvider(
    modifier: Modifier = Modifier,
    style: SnackbarStyle? = null,
    content: @Composable () -> Unit
)

SnackbarStyle Data Class

data class SnackbarStyle(
    val containerColor: Color = Color.Unspecified,
    val contentColor: Color = Color.Unspecified,
    val actionColor: Color = Color.Unspecified,
    val shape: Shape? = null,
    val elevation: Dp? = null,
    val messageTextStyle: TextStyle? = null,
    val actionTextStyle: TextStyle? = null
) {
    companion object {
        @Composable fun default(): SnackbarStyle
        @Composable fun success(): SnackbarStyle
        @Composable fun error(): SnackbarStyle
        @Composable fun warning(): SnackbarStyle
        @Composable fun info(): SnackbarStyle
    }
}

๐Ÿงช Testing

SnapNotify includes comprehensive test coverage with 45+ test cases covering 100% of the public API:

Test Coverage

  • โœ… Public API Methods: All 13 SnapNotify methods tested
  • โœ… Styling System: Complete SnackbarStyle functionality
  • โœ… Message Queueing: Thread-safe queue management
  • โœ… Action Callbacks: User interaction handling
  • โœ… Integration Tests: End-to-end behavior verification

Running Tests

# Run unit tests
./gradlew :snapnotify:test

# Run all tests including UI tests
./gradlew :snapnotify:check

# View test report
open snapnotify/build/reports/tests/testDebugUnitTest/index.html

The comprehensive test suite ensures reliability and catches regressions during development.

๐Ÿ“ฑ Example App

Check out the SnapNotifyDemo module for a complete demo:

# Build and install demo app
./gradlew :SnapNotifyDemo:installDebug

๐Ÿ“„ License

Copyright 2025 Vamsi Vaddavalli

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Apps
About Me
GitHub: Trinea
Facebook: Dev Tools