Tutorial
In this hands-on tutorial you will build a Bookmarks feature -- a reading list where users can save, view, and remove favorite links. Along the way you will use every core building block of the Vyuh Framework: FeatureDescriptor, routing with GoRoute, reactive state management with MobX, and the built-in dependency injection system.
Prerequisites
Before you begin, make sure you have:
- Flutter installed and working (
flutter doctorpasses) - Basic Dart knowledge (classes, async/await, generics)
- Familiarity with the Concepts section, especially Features & Plugins
What You'll Build
A self-contained Bookmarks feature that lets users save and manage a reading list. The tutorial covers:
- Creating a
FeatureDescriptor - Setting up client-side routes with
GoRoute(no CMS required) - Managing reactive state with MobX
- Using Vyuh's DI system to share a store across widgets
- Registering the feature in a Vyuh application
The result is a single-feature app you can run on any device or simulator.
Step 1: Create the Project
Start by creating a new Flutter project and adding the required Vyuh packages.
flutter create bookmarks_app
cd bookmarks_appAdd the framework packages:
flutter pub add vyuh_core vyuh_feature_developer go_router mobx flutter_mobx| Package | Purpose |
|---|---|
vyuh_core | Core framework APIs (FeatureDescriptor, runApp, DI) |
vyuh_feature_developer | Developer tools overlay for inspecting features |
go_router | Declarative routing |
mobx | Reactive state management |
flutter_mobx | Observer widget that rebuilds on state changes |
TIP
You do not need a CMS for this tutorial. Everything is client-side only. When you are ready to add CMS integration, see the What's Next section at the end.
Step 2: Define the Data Model
Create a file lib/bookmark.dart with a simple data class to represent a saved link.
// lib/bookmark.dart
class Bookmark {
final String id;
final String title;
final String url;
final DateTime addedAt;
Bookmark({
required this.id,
required this.title,
required this.url,
DateTime? addedAt,
}) : addedAt = addedAt ?? DateTime.now();
}Nothing Vyuh-specific here -- just a plain Dart class. The id field will be used to identify bookmarks when removing them.
Step 3: Create the Store
Create lib/bookmark_store.dart. This is where the application state lives, managed with MobX observables.
// lib/bookmark_store.dart
import 'package:mobx/mobx.dart';
import 'bookmark.dart';
class BookmarkStore {
/// The list of saved bookmarks. Because it is an [ObservableList],
/// any [Observer] widget reading it will rebuild automatically
/// when items are added or removed.
final bookmarks = ObservableList<Bookmark>();
/// A computed value that derives the count from the list length.
/// Computed values are recalculated only when their dependencies change.
late final bookmarkCount = Computed(() => bookmarks.length);
/// Adds a new bookmark. Wrapping the mutation in [runInAction]
/// ensures MobX batches the change and notifies observers once.
void addBookmark(String title, String url) {
runInAction(() {
bookmarks.add(Bookmark(
id: DateTime.now().millisecondsSinceEpoch.toString(),
title: title,
url: url,
));
});
}
/// Removes the bookmark with the given [id].
void removeBookmark(String id) {
runInAction(() {
bookmarks.removeWhere((b) => b.id == id);
});
}
}Key MobX concepts used here
ObservableList-- a reactive list. When its contents change, anyObserverwidget that reads it rebuilds automatically.Computed-- a derived value that recalculates only when its upstream observables change. Declared withlate finalso it is initialized once.runInAction-- wraps state mutations so MobX can batch changes and fire reactions after the action completes.
Want to learn more about MobX?
See the full State Management with MobX guide for advanced patterns like Action, async actions, and error handling.
Step 4: Build the UI
Create lib/bookmark_list_screen.dart. This is the main screen of the feature.
// lib/bookmark_list_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:vyuh_core/vyuh_core.dart';
import 'bookmark_store.dart';
class BookmarkListScreen extends StatefulWidget {
const BookmarkListScreen({super.key});
@override
State<BookmarkListScreen> createState() => _BookmarkListScreenState();
}
class _BookmarkListScreenState extends State<BookmarkListScreen> {
/// Retrieve the store from the global DI container.
/// It was registered during feature init (see Step 5).
late final store = vyuh.di.get<BookmarkStore>();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Bookmarks'),
actions: [
// Show the bookmark count in the app bar
Observer(
builder: (_) => Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text('${store.bookmarkCount.value} saved'),
),
),
),
],
),
body: Observer(
builder: (_) {
if (store.bookmarks.isEmpty) {
return const Center(
child: Text(
'No bookmarks yet.\nTap + to add one.',
textAlign: TextAlign.center,
),
);
}
return ListView.builder(
itemCount: store.bookmarks.length,
itemBuilder: (context, index) {
final bookmark = store.bookmarks[index];
return ListTile(
leading: const Icon(Icons.bookmark),
title: Text(bookmark.title),
subtitle: Text(bookmark.url),
trailing: IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => store.removeBookmark(bookmark.id),
),
);
},
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddDialog(context),
child: const Icon(Icons.add),
),
);
}
void _showAddDialog(BuildContext context) {
showDialog(
context: context,
builder: (_) => _AddBookmarkDialog(
onAdd: (title, url) => store.addBookmark(title, url),
),
);
}
}The Add Bookmark Dialog
Add the following dialog widget in the same file (or in its own file if you prefer).
// (continued in lib/bookmark_list_screen.dart)
class _AddBookmarkDialog extends StatefulWidget {
final void Function(String title, String url) onAdd;
const _AddBookmarkDialog({required this.onAdd});
@override
State<_AddBookmarkDialog> createState() => _AddBookmarkDialogState();
}
class _AddBookmarkDialogState extends State<_AddBookmarkDialog> {
final _titleController = TextEditingController();
final _urlController = TextEditingController();
@override
void dispose() {
_titleController.dispose();
_urlController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Add Bookmark'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _titleController,
decoration: const InputDecoration(labelText: 'Title'),
textCapitalization: TextCapitalization.sentences,
),
const SizedBox(height: 8),
TextField(
controller: _urlController,
decoration: const InputDecoration(labelText: 'URL'),
keyboardType: TextInputType.url,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () {
final title = _titleController.text.trim();
final url = _urlController.text.trim();
if (title.isNotEmpty && url.isNotEmpty) {
widget.onAdd(title, url);
Navigator.of(context).pop();
}
},
child: const Text('Add'),
),
],
);
}
}TIP
The Observer widget is the only part of the widget tree that rebuilds when observables change. The Scaffold, AppBar, and FloatingActionButton are not re-built -- only the list and the count badge react to state changes. This is one of the key advantages of MobX's granular reactivity.
Step 5: Define the Feature
Create lib/feature.dart. The FeatureDescriptor ties the store, routes, and metadata together into a self-contained unit.
// lib/feature.dart
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:vyuh_core/vyuh_core.dart';
import 'bookmark_list_screen.dart';
import 'bookmark_store.dart';
final feature = FeatureDescriptor(
name: 'bookmarks',
title: 'Bookmarks',
description: 'A reading list feature for saving favorite links',
icon: Icons.bookmark,
init: () async {
// Register the store on the global DI container so any widget
// in the app can retrieve it with vyuh.di.get<BookmarkStore>().
vyuh.di.register(BookmarkStore());
},
routes: () async => [
GoRoute(
path: '/bookmarks',
builder: (_, __) => const BookmarkListScreen(),
),
],
);Two things happen here:
initregisters theBookmarkStoreon Vyuh's DI container. This runs once during the framework's bootstrap phase, before any route is rendered.routesexposes a single/bookmarksroute that renders the list screen. Because this is a plainGoRoute, no CMS is involved.
When to use GoRoute vs CMSRoute
Use GoRoute when the screen is fully client-side and does not need CMS content. Use CMSRoute when you want the CMS to control the page layout and content. See Routes & Navigation for details.
Step 6: Wire It Into the App
Replace the contents of lib/main.dart with the following:
// lib/main.dart
import 'package:flutter/material.dart';
import 'package:vyuh_core/vyuh_core.dart' as vc;
import 'package:vyuh_feature_developer/vyuh_feature_developer.dart' as dev;
import 'feature.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
vc.runApp(
initialLocation: '/bookmarks',
features: () => [
feature,
dev.feature,
],
);
}initialLocationtells the router which route to display at startup.featuresis a function returning the list of features to load. We include the developer feature (dev.feature) alongside our bookmarks feature so you can inspect the running app.
WARNING
Make sure initialLocation matches a path declared in one of your features. If no feature owns that path, you will see a routing error at launch.
Step 7: Run the App
Run the app on your preferred device or simulator:
flutter runAfter the framework finishes initializing, you should see:
- An app bar with the title "Bookmarks" and a count badge showing "0 saved".
- An empty state message in the center of the screen.
- A floating action button (+) in the bottom-right corner.
Tap the FAB to open the add dialog. Enter a title and URL, then tap Add. The bookmark appears in the list instantly -- no manual refresh needed, thanks to MobX reactivity. Tap the delete icon on any bookmark to remove it.
You now have a fully functional Vyuh feature running as a standalone app.
Recap
Here is a summary of the files you created and what each one does:
| File | Purpose |
|---|---|
lib/bookmark.dart | Data model |
lib/bookmark_store.dart | Reactive state (MobX store) |
lib/bookmark_list_screen.dart | UI (list screen + add dialog) |
lib/feature.dart | FeatureDescriptor (DI setup + routes) |
lib/main.dart | App entry point (vc.runApp) |
The flow is: main.dart boots the framework with your feature list. Each feature's init runs (registering the store on DI), then routes are collected and handed to GoRouter. When the user navigates to /bookmarks, the BookmarkListScreen retrieves the store from DI and renders the list reactively.
What's Next
Now that you have a working feature, here are several directions to take it further:
Add CMS integration -- Convert the
GoRouteto aCMSRouteand manage bookmark display from Sanity. See Routes & Navigation.Add custom content types -- Turn
Bookmarkinto aContentItemwith a CMS schema so editors can curate reading lists. See Content Types.Add actions -- Create a "Save Bookmark" action that can be triggered from any CMS-configured button. See Actions.
Use scoped DI -- Tie the
BookmarkStorelifetime to the route instead of the global container, so it is created when the route mounts and disposed when it unmounts. See Scoped DI.Explore more examples -- Browse complete sample apps in the Examples section.