Presenting a tutorial or feature showcase conditionally (only the first time) in a Flutter BLoC-managed app requires a clean separation between persistence logic (checking if they’ve seen it), state management (the BLoC event flow), and the UI (the showcaseview library).
Here is a step-by-step implementation guide.
1. Dependencies
Add these to your pubspec.yaml:
dependencies:
flutter_bloc: ^8.1.0
showcaseview: ^3.0.0
shared_preferences: ^2.2.0 # To remember if the user saw it
2. The Persistence Layer
You need a way to store whether the user has already completed the tutorial. A simple repository handles this:
class TutorialRepository {
static const _key = 'has_seen_home_showcase';
Future<bool> hasSeenTutorial() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(_key) ?? false;
}
Future<void> markTutorialAsSeen() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_key, true);
}
}
3. The BLoC / Cubit
The BLoC handles the logic of checking the repository when the Home Screen loads and updating it when the showcase ends.
// State
class TutorialState {
final bool shouldShowTutorial;
TutorialState({this.shouldShowTutorial = false});
}
// Cubit
class TutorialCubit extends Cubit<TutorialState> {
final TutorialRepository repository;
TutorialCubit(this.repository) : super(TutorialState());
Future<void> checkTutorialStatus() async {
final hasSeen = await repository.hasSeenTutorial();
if (!hasSeen) {
emit(TutorialState(shouldShowTutorial: true));
}
}
Future<void> completeTutorial() async {
await repository.markTutorialAsSeen();
emit(TutorialState(shouldShowTutorial: false));
}
}
4. The Home Screen Implementation
The Home Screen needs to be wrapped in a ShowCaseWidget. We use BlocListener to trigger the showcase only when the Cubit says so.
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
// 1. Create GlobalKeys for the features you want to highlight
final GlobalKey _profileKey = GlobalKey();
final GlobalKey _addFeatureKey = GlobalKey();
@override
void initState() {
super.initState();
// 2. Check if tutorial should run
context.read<TutorialCubit>().checkTutorialStatus();
}
@override
Widget build(BuildContext context) {
// 3. Wrap everything in ShowCaseWidget
return ShowCaseWidget(
onFinish: () => context.read<TutorialCubit>().completeTutorial(),
builder: Builder(
builder: (showcaseContext) {
return BlocListener<TutorialCubit, TutorialState>(
listener: (context, state) {
if (state.shouldShowTutorial) {
// 4. Start showcase after the frame is rendered
WidgetsBinding.instance.addPostFrameCallback((_) {
ShowCaseWidget.of(showcaseContext).startShowCase([
_profileKey,
_addFeatureKey,
]);
});
}
},
child: Scaffold(
appBar: AppBar(
title: const Text('Home'),
actions: [
// 5. Wrap specific widgets with Showcase
Showcase(
key: _profileKey,
title: 'Profile',
description: 'Click here to edit your profile',
child: const Icon(Icons.person),
),
],
),
body: Center(
child: Showcase(
key: _addFeatureKey,
title: 'New Feature',
description: 'This is the cool new feature!',
child: ElevatedButton(
onPressed: () {},
child: const Text('Explore'),
),
),
),
),
);
},
),
);
}
}
Key Considerations
- The Context Trap:
ShowCaseWidget.of(context)requires a context below theShowCaseWidgetin the tree. That is why thebuilder: Builder(...)pattern is used above—it provides a freshBuildContextthat can “see” theShowCaseWidget. - PostFrameCallback: You cannot trigger the showcase during the build phase.
WidgetsBinding.instance.addPostFrameCallbackensures the UI is fully rendered before the overlay appears. - Login Flow: Ensure the
TutorialCubitis provided high enough in the tree (e.g., at theMaterialApplevel or as soon as the user logs in) so it is ready when theHomeScreenis pushed. - Resetting: For testing, you can add a “Reset Tutorial” button in your settings that calls a method in the Repository to clear the
SharedPreferenceskey.
Pro-Tip: Hydrated BLoC
If you want to skip the TutorialRepository and SharedPreferences boilerplate, you can use the hydrated_bloc package. It will automatically persist your TutorialState to local storage, allowing you to simply check state.hasSeen directly without manual async fetching.
