DEV Community

Cover image for Introducing TourCompose: The Easiest Way to Add Interactive Tours to Your Android Compose App (Without Losing Your Sanity)
Antonio Huerta Reyes
Antonio Huerta Reyes

Posted on

Introducing TourCompose: The Easiest Way to Add Interactive Tours to Your Android Compose App (Without Losing Your Sanity)

I get it – you're busy, you want to see results NOW, and you probably have 17 other tabs open. So here's what you came for:

🎯 Live Demo: Try it on GitHub

📦 Repository: github.com/AntonioHReyes/TourCompose

Quick Start: Just add implementation("com.github.AntonioHReyes:TourCompose:1.0.1") to your gradle

Got 2 minutes? Copy-paste the complete working example below and you'll have a working tour in your app.

Want the full story and all the juicy details? Keep reading – I promise it's worth it (and slightly entertaining). Otherwise, you know what to do. 👇


🤷‍♂️ The Full Story (For Those Who Like Context)

Let's be honest – adding onboarding tours to your Android app has traditionally been about as fun as debugging a null pointer exception at 3 AM. You've probably been there: wrestling with complex libraries, writing endless configuration files, and somehow ending up with tour bubbles that look like they time-traveled from 2010.

Well, grab your coffee ☕ and prepare to be pleasantly surprised. Meet TourCompose – the library that finally makes creating beautiful app tours as easy as it should have been all along.

🤔 The Problem We All Know Too Well

Picture this: Your PM walks over (or pings you on Slack if you're lucky enough to work remotely) and says, "Hey, can we add some onboarding to help users understand the app? Should be super quick, right?"

Internal screaming intensifies

You know what comes next:

  • Hours of documentation reading 📚
  • Complex XML configurations that make you question your life choices
  • Tour bubbles that somehow always appear in the wrong place
  • Styling that looks like it was designed by a caffeinated developer at 2 AM (spoiler: it probably was)
  • That one edge case where the tour breaks completely, but only on Samsung devices

Sound familiar? Yeah, we've all been there.

🚀 Enter TourCompose: Your New Best Friend

TourCompose was born from the simple idea that creating app tours shouldn't require a computer science degree. It's designed by developers, for developers who just want things to work without having to sacrifice their weekends to the onboarding gods.

What Makes It Special?

  • Zero complex setup – Because life's too short for 50-line configuration files
  • Works with Material Theme out of the box – Your designer will actually thank you
  • Flexible but not overwhelming – Customize when you need to, ignore when you don't
  • Actually tested in real apps – Not just in "hello world" examples

📦 Setup So Easy, Your Intern Could Do It

First, add this to your app's build.gradle.kts (yes, that's really all you need):

dependencies {
    implementation("com.github.AntonioHReyes:TourCompose:1.0.1")
}
Enter fullscreen mode Exit fullscreen mode

Then add JitPack to your settings.gradle.kts (because apparently we all agreed that having 47 different repositories is totally normal):

dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven { url = uri("https://jitpack.io") } // The hero we need
    }
}
Enter fullscreen mode Exit fullscreen mode

That's it. No, seriously. No additional permissions, no manifest entries, no sacrificial offerings to the Android gods.

💡 The "Holy Grail" Example: Copy, Paste, Ship

Remember how I mentioned this should be easy? Here's a complete, working example that you can literally copy and paste into a new project and it'll work. I'm not kidding – I actually tested this one:

MainActivity.kt


import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tonyakitori.apps.tourcompose.TourCompose
import com.tonyakitori.apps.tourcompose.controller.TourComposeController
import com.tonyakitori.apps.tourcompose.controller.TourComposeStep
import com.tonyakitori.apps.tourcompose.controller.TourComposeWrapper
import com.tonyakitori.apps.tourcompose.settings.bubbleContent.bubbleContentBasicSettings
import java.util.UUID

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                val tourController = remember { SimpleTourController() }

                TourComposeWrapper(tourController = tourController) {

                    val currentStep by tourController.currentStep.collectAsState(initial = null)

                    Column(
                        modifier = Modifier
                            .fillMaxSize()
                            .padding(32.dp),
                        verticalArrangement = Arrangement.spacedBy(32.dp),
                        horizontalAlignment = Alignment.CenterHorizontally
                    ) {

                        Text(
                            text = "Welcome to my awesome app!",
                            style = MaterialTheme.typography.headlineMedium,
                            modifier = Modifier.tourStepIndex("welcome-tour", 0)
                        )

                        Text(
                            text = "This app does amazing things that will change your life, cure your anxiety, and probably make you a better person.",
                            style = MaterialTheme.typography.bodyLarge,
                            modifier = Modifier.padding(16.dp)
                        )

                        Button(
                            onClick = {
                                // This button obviously does something super important
                                println("Button clicked! 🎉")
                            },
                            modifier = Modifier.tourStepIndex("welcome-tour", 1)
                        ) {
                            Text("Do Something Amazing")
                        }

                        Card(
                            modifier = Modifier
                                .fillMaxWidth()
                                .tourStepIndex("welcome-tour", 2)
                        ) {
                            Column(modifier = Modifier.padding(16.dp)) {
                                Text(
                                    text = "Pro Feature™",
                                    style = MaterialTheme.typography.titleMedium
                                )
                                Text(
                                    text = "This card contains features so advanced that we needed a tour just to explain them.",
                                    style = MaterialTheme.typography.bodyMedium
                                )
                            }
                        }

                        Spacer(modifier = Modifier.weight(1f))

                        Button(
                            onClick = { tourController.startTour("welcome-tour") },
                            colors = ButtonDefaults.buttonColors(
                                containerColor = MaterialTheme.colorScheme.secondary
                            )
                        ) {
                            Text("🎯 Start the Tour")
                        }
                    }

                    // Here's where the magic happens ✨
                    TourCompose(
                        componentRectArea = currentStep?.componentRect,
                        bubbleContentSettings = currentStep?.bubbleContentSettings
                    )
                }
            }
        }
    }
}

// This controller is so simple, it's almost embarrassing how easy this is
class SimpleTourController : TourComposeController() {
    init {
        addTour(
            flowId = "welcome-tour",
            steps = listOf(
                TourComposeStep(
                    id = UUID.randomUUID().toString(),
                    bubbleContentSettings = bubbleContentBasicSettings(
                        title = "👋 Hey There!",
                        description = "This is your app's main title. It's probably the first thing users see, so we made it big and friendly. Good choice!",
                        primaryButtonText = "Makes Sense",
                        onPrimaryClick = { nextStep() },
                        onDismiss = { stopTour() }
                    )
                ),
                TourComposeStep(
                    id = UUID.randomUUID().toString(),
                    bubbleContentSettings = bubbleContentBasicSettings(
                        title = "🚀 The Action Button",
                        description = "This button is where the magic happens. Click it to do something amazing (results may vary, but we're optimistic).",
                        primaryButtonText = "Got It!",
                        secondaryButtonText = "Wait, Go Back",
                        onPrimaryClick = { nextStep() },
                        onSecondaryClick = { previousStep() },
                        onDismiss = { stopTour() }
                    )
                ),
                TourComposeStep(
                    id = UUID.randomUUID().toString(),
                    bubbleContentSettings = bubbleContentBasicSettings(
                        title = "💎 Premium Content",
                        description = "This card showcases your premium features. It's fancy, it's important, and now your users know exactly where to find it!",
                        primaryButtonText = "Awesome, I'm Done!",
                        secondaryButtonText = "Show Me Again",
                        onPrimaryClick = { stopTour() },
                        onSecondaryClick = { previousStep() },
                        onDismiss = { stopTour() }
                    )
                )
            )
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

🎉 Run It and Watch the Magic

Seriously, that's it. Copy that code, create a new Compose project, paste it in, and hit run. You'll get:

  • Beautiful, themed tour bubbles that actually look good ✨
  • Navigation that just works (forward, backward, dismiss) 🔄
  • Automatic component highlighting that finds your UI elements like a GPS for tours 📍

🎨 But Wait, There's More! (Customization Without the Headache)

The "I Just Want Different Colors" Solution and the "I Want It to Match My Theme" Solution

// Your designer will think you're a wizard 🧙‍♂️
val contentBubbleColors = defaultBubbleContentColors().copy(
    titleTextColor = MaterialTheme.colorScheme.error
)
Enter fullscreen mode Exit fullscreen mode

The "I Need This Tour to Look Like It Came From the Future" Solution

TourCompose(
    componentRectArea = currentStep?.componentRect,
    bubbleContentSettings = currentStep?.bubbleContentSettings,
    tourComposeProperties = TourComposeProperties.getDefaultInstance().copy(
        spotlightColors = DefaultSpotlightColors(
            overlayBackgroundColor = Color.Black.copy(alpha = 0.9f),
            overlayBorderColor = Color.Cyan
        ),
        dialogBubbleColors = DefaultDialogBubbleColors(
            backgroundColor = Color.Black,
            borderColor = Color.Cyan
        )
    )
)
Enter fullscreen mode Exit fullscreen mode

🤓 For the Overachievers: Advanced Features

Smart Component Targeting

Just slap .tourStepIndex("your-tour-id", stepNumber) on any Composable and boom – it's part of your tour:

// Works on literally anything
Text("Hello", modifier = Modifier.tourStepIndex("intro", 0))
Button(onClick = {}, modifier = Modifier.tourStepIndex("intro", 1)) {}
LazyColumn(modifier = Modifier.tourStepIndex("intro", 2)) {}
YourCustomComposable(modifier = Modifier.tourStepIndex("intro", 3))
Enter fullscreen mode Exit fullscreen mode

Multiple Tours (Because Why Not?)

// Welcome tour for new users
tourController.startTour("welcome-tour")

// Feature tour for when you add something cool
tourController.startTour("new-feature-tour")

// "Please rate us" tour for when you're feeling brave
tourController.startTour("desperate-for-ratings-tour")
Enter fullscreen mode Exit fullscreen mode

Custom Bubble Content (For the Creative Souls)

Want to add progress bars, ratings, custom buttons, or a dancing cat GIF? TourCompose supports custom bubble content that can be as wild as your imagination (and your designer's caffeine level).

🎯 Getting Started (The TL;DR Version)

  1. Add the dependency (2 lines in your gradle files)
  2. Copy the example above (literally copy-paste, we won't judge)
  3. Run it and smile (because it actually works)
  4. Customize to your heart's content (or don't, the defaults are pretty nice)
  5. Ship it (and maybe buy yourself a coffee to celebrate)

🔗 Links for the Impatient

🚀 Final Thoughts

Creating good onboarding shouldn't be harder than building the actual app features. TourCompose exists because we got tired of fighting with tour libraries instead of building cool stuff.

Whether you're a solo indie developer building the next big thing, or part of a team shipping features to millions of users, TourCompose has your back. It's simple enough for quick prototypes and powerful enough for production apps.

So go ahead, add those tours your PM has been asking for. Make your app more user-friendly. Help your users discover features they didn't know existed. And do it all without losing your sanity or your weekend.

Your users (and your mental health) will thank you. 🙌

Ready to make onboarding tours that don't suck? Give TourCompose a spin – we promise it's more fun than debugging memory leaks!


P.S. – If you end up using TourCompose and it saves you time, star the repo. It makes us feel good about our life choices and motivates us to keep making cool stuff. Plus, your GitHub activity graph will look more impressive. Win-win! ⭐

Top comments (2)

Collapse
 
luca_nicoletti profile image
Luca Nicoletti

Does it work on Kotlin Multiplatform?

Collapse
 
tonyakitori profile image
Antonio Huerta Reyes

nop, I`m sorry