Fluo

A Flutter SDK for building beautiful onboarding flows with minimal code.

This SDK and documentation are optimized for coding agents (Claude, Cursor, Copilot, etc.) to quickly understand and generate onboarding flows. Just describe what you want and let your agent build it.

Example prompt:

Create an onboarding flow for my meditation app using Fluo. I want:

  • A welcome screen with a calming message
  • A question asking about their experience level (new to meditation, some experience, regular practice)
  • A question about their main goals (reduce stress, sleep better, improve focus, manage anxiety - multiple choice)
  • A question asking what time they prefer to meditate (morning, afternoon, evening)
  • A features screen highlighting guided sessions, sleep stories, and progress tracking
  • An info screen about enabling notifications for daily reminders
  • A rating screen at the end

Installation

flutter pub add fluo

Step 1: Create a FluoNavigator

Start with a FluoNavigator. It handles navigation and displays a progress bar.

import 'package:fluo/fluo_navigator.dart';

class OnboardingScreen extends StatelessWidget {
  final navigatorKey = GlobalKey<FluoNavigatorState>();

  @override
  Widget build(BuildContext context) {
    return FluoNavigator(
      key: navigatorKey,
      initialRoute: 'welcome',
      totalSteps: 3,
      onGenerateRoute: (settings) {
        Widget page;

        if (settings.name == 'welcome') {
          page = Center(child: Text('Welcome!'));
        } else {
          page = Center(child: Text('Unknown'));
        }

        return MaterialPageRoute(
          settings: settings,
          builder: (context) => page,
        );
      },
    );
  }
}

Step 2: Add a question screen

Use FluoQuestionScreen to ask questions with selectable answers.

import 'package:fluo/fluo_question_screen.dart';

if (settings.name == 'role') {
  page = FluoQuestionScreen(
    title: 'What describes you best?',
    answers: [
      FluoAnswer(
        id: 'developer',
        title: 'Developer',
        icon: Icon(Icons.code, size: 16),
      ),
      FluoAnswer(
        id: 'designer',
        title: 'Designer',
        icon: Icon(Icons.palette, size: 16),
      ),
      FluoAnswer(
        id: 'other',
        title: 'Other',
        icon: Icon(Icons.person, size: 16),
      ),
    ],
    onContinue: (answerIds) {
      navigatorKey.currentState!.pushNamed('next_screen');
    },
  );
}

For multiple selection, set singleChoice: false:

FluoQuestionScreen(
  title: 'What are your interests?',
  singleChoice: false,  // Shows a Continue button
  answers: [...],
  onContinue: (answerIds) {
    // answerIds contains all selected answers
  },
)

Step 3: Other template screens

Fluo provides 4 template screens. They all work similarly.

FluoInfoScreen

Display information with an icon and subtitle.

import 'package:fluo/fluo_info_screen.dart';

FluoInfoScreen(
  icon: Icon(Icons.check_circle, size: 80),
  title: 'You're all set!',
  subtitle: 'Your account has been created.',
  onContinue: () {
    navigatorKey.currentState!.pushNamed('next');
  },
)

FluoFeaturesScreen

Display a list of features with checkmarks.

import 'package:fluo/fluo_features_screen.dart';

FluoFeaturesScreen(
  title: 'What you get',
  features: [
    'Unlimited projects',
    'Priority support',
    'Advanced analytics',
  ],
  onContinue: () {
    navigatorKey.currentState!.pushNamed('next');
  },
)

FluoRatingScreen

Prompt users to rate your app.

import 'package:fluo/fluo_rating_screen.dart';

FluoRatingScreen(
  title: 'Enjoying the app?',
  appStoreId: '123456789',
  userPhoto: AssetImage('assets/reviewer.png'),
  userName: 'John Doe',
  userReview: 'This app changed my life!',
  onContinue: () {
    navigatorKey.currentState!.pushNamed('next');
  },
)

Step 4: Add a custom screen

You can use any widget as a screen. Just make sure to call navigatorKey.currentState!.pushNamed() to navigate.

if (settings.name == 'custom') {
  page = MyCustomScreen(
    onContinue: () {
      navigatorKey.currentState!.pushNamed('next');
    },
  );
}

Step 5: Mix templates and custom screens

Combine template screens and custom screens in any order.

onGenerateRoute: (settings) {
  Widget page;

  if (settings.name == 'intro') {
    page = MyCustomIntroScreen(onContinue: () => goTo('question1'));
  }

  if (settings.name == 'question1') {
    page = FluoQuestionScreen(
      title: 'How did you hear about us?',
      answers: [...],
      onContinue: (ids) => goTo('features'),
    );
  }

  if (settings.name == 'features') {
    page = FluoFeaturesScreen(
      title: 'What you get',
      features: [...],
      onContinue: () => goTo('custom_setup'),
    );
  }

  if (settings.name == 'custom_setup') {
    page = MyCustomSetupScreen(onContinue: () => goTo('done'));
  }

  return MaterialPageRoute(settings: settings, builder: (_) => page);
}

void goTo(String route) {
  navigatorKey.currentState!.pushNamed(route);
}

Step 6: Branching with stepIncrement

Skip steps based on user answers using stepIncrement.

FluoQuestionScreen(
  title: 'Are you a professional?',
  answers: [
    FluoAnswer(id: 'yes', title: 'Yes', icon: Icon(Icons.work)),
    FluoAnswer(id: 'no', title: 'No', icon: Icon(Icons.person)),
  ],
  onContinue: (answerIds) {
    if (answerIds.first == 'yes') {
      // Go to professional questions (1 step)
      navigatorKey.currentState!.pushNamed(
        'pro_details',
        stepIncrement: 1,
      );
    } else {
      // Skip professional questions, jump ahead (3 steps)
      navigatorKey.currentState!.pushNamed(
        'general_info',
        stepIncrement: 3,
      );
    }
  },
)

The progress bar will update accordingly based on stepIncrement.


Styling

Each screen has its own style class. Pass a style to customize appearance.

FluoQuestionScreen(
  style: FluoQuestionScreenStyle(
    backgroundColor: Colors.white,
    titleTextStyle: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
    continueButtonStyle: FilledButton.styleFrom(backgroundColor: Colors.blue),
  ),
  title: '...',
  answers: [...],
  onContinue: (ids) {},
)

Available style classes:

  • FluoQuestionScreenStyle + FluoAnswerStyle
  • FluoInfoScreenStyle
  • FluoFeaturesScreenStyle
  • FluoRatingScreenStyle
  • FluoNavigatorStyle
  • FluoSignInStyle (for sign-in flows)

Check the source code for all available style properties.


Authentication (optional)

If you need authentication, add the localization delegate and use the sign-in methods.

Add FluoLocalizations.delegate to your app:

MaterialApp(
  // ...other properties...
  localizationsDelegates: const [
    FluoLocalizations.delegate,
    // ...other delegates...
  ],
)

Initialize Fluo and start a sign-in flow:

import 'package:fluo/fluo.dart';

// Initialize once at app start
await Fluo.initWithApiKey('YOUR_API_KEY');
await Fluo.instance.loadAppConfig();

// Start a sign-in flow
Fluo.instance.signInWithEmail(
  context: context,
  onExit: () => print('User cancelled'),
  onUserReady: () => print('User signed in!'),
);

// Or use Mobile/Google/Apple
Fluo.instance.signInWithMobile(
  context: context,
  onExit: () => print('User cancelled'),
  onUserReady: () => print('User signed in!'),
);

// For mobile sign-in, Fluo uses Prelude (https://prelude.so) for SMS delivery.
// Create an API key on Prelude and add it to your Fluo dashboard.

Fluo.instance.signInWithGoogle(
  context: context,
  onBeforeSessionCreation: () => showLoadingDialog(),
  onUserReady: () => hideLoadingAndContinue(),
);

// For Google sign-in, configure your Google OAuth credentials in the Fluo dashboard.

Fluo.instance.signInWithApple(
  context: context,
  onBeforeSessionCreation: () => showLoadingDialog(),
  onUserReady: () => hideLoadingAndContinue(),
);

// For Apple sign-in, configure your Apple Developer credentials in the Fluo dashboard.

macOS setup

Add this key to both macos/Runner/DebugProfile.entitlements and macos/Runner/Release.entitlements:

<dict>
	<key>com.apple.security.network.client</key>
	<true/>
</dict>

Useful methods

// Check if user is ready (valid session + complete user attributes)
if (Fluo.instance.isUserReady()) { /* ... */ }

// Check if a session exists
if (Fluo.instance.hasSession()) { /* ... */ }

// Get a fresh access token (auto-refreshes if expired)
final accessToken = await Fluo.instance.getAccessToken();

// Sign out
await Fluo.instance.clearSession();

// User data
Fluo.instance.session!.user.id;        // "jzi8w7bdou4m0kq"
Fluo.instance.session!.user.email;     // "peter.parker@marvel.com"
Fluo.instance.session!.user.firstName; // "Peter"
Fluo.instance.session!.user.lastName;  // "Parker"

Integrating with Firebase

Select 'Firebase' for your backend. Fluo forwards user info to Firebase Authentication and creates a users table in Firestore.

if (Fluo.isInitialized) {
  final fluoSession = Fluo.instance.session;
  if (fluoSession != null) {
    final firebaseToken = fluoSession.firebaseToken!;
    await FirebaseAuth.instance.signInWithCustomToken(firebaseToken);
  }
}

Integrating with Supabase

Select 'Supabase' for your backend. Fluo forwards user info to Supabase Authentication and a users table.

if (Fluo.isInitialized) {
  final fluoSession = Fluo.instance.session;
  if (fluoSession != null) {
    final supabaseSession = fluoSession.supabaseSession!;
    await Supabase.instance.client.auth.recoverSession(supabaseSession);
  }
}

Integrating with any backend

Select 'Custom' for your backend. The general idea is to use the JWT access token provided by Fluo to get a unique user id via the "sub" JWT claim.

Here is a full example to understand how it works:

  1. Wherever you need it, call Fluo.instance.getAccessToken() to get the JWT access token generated by Fluo and send it to your backend.
import 'dart:convert';
import 'package:http/http.dart' as http;

// Example of a function that gets a user. If the user
// doesn't exist yet, it should create it first.
Future<User> getOrCreateUser() async {
  final accessToken = await Fluo.instance.getAccessToken();
  final response = await http.post(
    Uri.parse('https://your-backend.com/api/user/me'),
    // Send the JWT access token to securely authenticate
    // the user and retrieve the user id.
    headers: {
      'authorization': 'Bearer $accessToken',
    },
    // Send the user data to create or update the user if
    // the object does not exist yet.
    body: jsonEncode(Fluo.instance.session.user),
  );
  return User.fromJson(jsonDecode(response.body));
}
  1. In your backend, decode the access token to get the JWT payload.
const jwt = require("jsonwebtoken")

// This is your JWT secret key (do not share it with anyone)
// You can find it on https://dashboard.fluo.dev/backend
const SECRET_KEY = "YOUR_SECRET_KEY"

// Following on the example, here is the corresponding endpoint.
// Note that this is simplified and does not handle all edge cases.
app.post("/api/user/me", async (req, res) => {
  const accessToken = req.headers["authorization"].split(" ")[1]

  // Decode the access token using your secret key
  const payload = jwt.verify(accessToken, SECRET_KEY)

  // 'payload.sub' contains a unique user id generated by Fluo
  const userId = payload.sub

  // Find the user by id
  let user = await User.findOne({ id: userId }) // or { fluoId: userId }

  // If the user doesn't exist, create it
  if (!user) {
    const { email, mobileE164, mobileIso2, firstName, lastName } = req.body
    user = await User.create({
      id: userId,
      email: email,
      mobileE164: mobileE164,
      mobileIso2: mobileIso2,
      firstName: firstName,
      lastName: lastName,
    })
  }

  return res.status(200).json(user)
})
  1. If you need to go further, here is a complete example of the payload. For example, for increased security, you might want to verify that the token has not expired.
{
  "sub": "2rztxukf57pnjz9", // user id
  "iat": 1744039599, // issued at
  "exp": 1744043199, // expires 1 hour after being issued
  "iss": "fluo.dev", // issuer
}

Libraries

api/api_client
api/models/api_error
api/models/app_config
api/models/apple_web_options
api/models/auth_method
api/models/google_client_id
api/models/partial_session
api/models/registration_step
api/models/session
api/models/user
fluo
Provides the Fluo SDK for Flutter applications.
fluo_features_screen
fluo_info_screen
fluo_navigator
fluo_onboarding
fluo_question_screen
fluo_rating_screen
fluo_sign_in_style
fluo_theme
l10n/fluo_localizations
l10n/fluo_localizations_ar
l10n/fluo_localizations_de
l10n/fluo_localizations_en
l10n/fluo_localizations_es
l10n/fluo_localizations_fa
l10n/fluo_localizations_fr
l10n/fluo_localizations_hi
l10n/fluo_localizations_it
l10n/fluo_localizations_ja
l10n/fluo_localizations_ko
l10n/fluo_localizations_nl
l10n/fluo_localizations_pl
l10n/fluo_localizations_pt
l10n/fluo_localizations_ru
l10n/fluo_localizations_sv
l10n/fluo_localizations_tr
l10n/fluo_localizations_zh
l10n/localized
l10n/localized_countries_ar
l10n/localized_countries_de
l10n/localized_countries_en
l10n/localized_countries_es
l10n/localized_countries_fa
l10n/localized_countries_fr
l10n/localized_countries_hi
l10n/localized_countries_it
l10n/localized_countries_ja
l10n/localized_countries_ko
l10n/localized_countries_nl
l10n/localized_countries_pl
l10n/localized_countries_pt
l10n/localized_countries_pt_br
l10n/localized_countries_ru
l10n/localized_countries_sv
l10n/localized_countries_tr
l10n/localized_countries_zh
managers/country_manager
managers/session_manager
presentation/auth/auth_navigator
presentation/auth/enter_code_screen
presentation/auth/enter_email_screen
presentation/auth/enter_mobile_screen
presentation/register/enter_first_name_screen
presentation/register/enter_last_name_screen
presentation/register/register_navigator
stubs/google_sign_in_web_stub
widgets/clear_suffix_button
widgets/continue_button
widgets/countries_list
widgets/ink_well_color
widgets/mobile_input
widgets/round_button
widgets/single_input_screen