Quickstart Guide#
Install the Flutter SDK, initialize it, and display your first Stories row with a visible loading/result state. This is a task guide; List Components is the widget/controller reference.
Prerequisites#
- Flutter 3.35.0+ and Dart 3.9.0+, below Dart 4, as declared by the four packages.
- A Storyteller API key supplied by the Storyteller team for the tenant you are integrating.
- A valid category ID from that same tenant, containing published, currently available Stories. Obtain it through your Storyteller CMS/team; the placeholder below is not a real ID.
- A device/emulator with network access. These plugins support Android and iOS; a Flutter web or desktop build does not provide the native Storyteller implementation.
- Core SDK: iOS 13.0+ or Android API 24+. The optional GAM, AdMob and VAST Flutter plugins declare iOS 15.0+ and Android API 24+. See Ads.
- From
12.1.2, Android builds use the Storyteller Android 12.x line: Kotlin 2.1+ and Jetpack Compose BOM2026.06.01(Compose 1.11.4), which also needs compileSdk 35+ and Android Gradle Plugin 8.6.0+. Don't force older Compose versions in your app.
SDK support and sample-app requirements are separate. Flutter itself and the selected plugins also impose build-tool requirements; the plugin podspecs declare Swift 6 language mode. Use a compatible Flutter/Xcode toolchain. Follow the Showcase README for the sample app's own requirements.
1) Install the SDK#
The current published package line is 12.2.0. Add the dependency to your app's
pubspec.yaml:
dependencies:
storyteller_sdk: ^12.2.0
flutter pub get
The Changelog lists the changes in 12.2.0 and earlier
releases.
For iOS, add both Storyteller CocoaPods sources above the CocoaPods CDN in the
app's ios/Podfile:
source 'https://github.com/getstoryteller/storyteller-sdk-ios-podspec.git'
source 'https://github.com/getstoryteller/storyteller-lottie-ios-podspec.git'
source 'https://cdn.cocoapods.org/'
platform :ios, '13.0'
Use iOS 15.0 or higher if you also include a Google Ads or VAST Flutter plugin. Resolve pods through your normal Flutter/iOS build. Optional ad plugins require additional application configuration described in Ads.
2) Initialize Storyteller#
Call WidgetsFlutterBinding.ensureInitialized() before plugin methods. Await
Storyteller.initialize(...) and check its success result before mounting or
loading Storyteller views. The complete example below keeps initialization loading,
failure and retry visible in the application instead of abandoning startup.
externalId is optional; supply your own user's identifier when integrating
identity (Working with Users). Configure privacy explicitly with
eventTrackingOptions when required (Privacy and Tracking).
3) Render a Storyteller widget#
Replace both placeholders in this complete lib/main.dart. The row gets a nonzero
height and is mounted only after successful initialization. The native row loads
automatically when created. The button requests another load through its
controller; the reload Future is a command acknowledgement, while
onDataLoadComplete reports the actual load outcome.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:storyteller_sdk/storyteller_sdk.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const MaterialApp(home: StorytellerStart()));
}
class StorytellerStart extends StatefulWidget {
const StorytellerStart({super.key});
@override
State<StorytellerStart> createState() => _StorytellerStartState();
}
class _StorytellerStartState extends State<StorytellerStart> {
bool _ready = false;
bool _initializing = true;
String? _error;
@override
void initState() {
super.initState();
unawaited(_initialize());
}
Future<void> _initialize() async {
setState(() {
_initializing = true;
_error = null;
});
try {
final result = await Storyteller.initialize('YOUR_API_KEY');
if (!mounted) return;
setState(() {
_ready = result.success;
_initializing = false;
_error = result.success ? null : result.message;
});
} catch (error) {
if (!mounted) return;
setState(() {
_initializing = false;
_error = error.toString();
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Storyteller')),
body: _ready
? const StoriesSection()
: Center(
child: _initializing
? const CircularProgressIndicator()
: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(_error ?? 'Initialization failed'),
TextButton(
onPressed: _initialize,
child: const Text('Retry initialization'),
),
],
),
),
);
}
}
class StoriesSection extends StatefulWidget {
const StoriesSection({super.key});
@override
State<StoriesSection> createState() => _StoriesSectionState();
}
class _StoriesSectionState extends State<StoriesSection> {
final _controller = StorytellerListViewController();
bool _loading = true;
String _status = 'Loading stories';
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (_loading) const LinearProgressIndicator(),
Text(_status),
SizedBox(
height: 200,
child: StorytellerStoriesRowView(
controller: _controller,
categories: const ['YOUR_CATEGORY_ID'],
onDataLoadStarted: () {
if (!mounted) return;
setState(() {
_loading = true;
_status = 'Loading stories';
});
},
onDataLoadComplete: (success, error, dataCount) {
if (!mounted) return;
setState(() {
_loading = false;
_status = success
? (dataCount == 0
? 'No published stories'
: 'Loaded $dataCount stories')
: 'Could not load stories: $error';
});
},
),
),
TextButton(
onPressed: _loading ? null : () => _controller.reloadData(),
child: const Text('Reload stories'),
),
],
);
}
}
Expected result#
After initialization succeeds, the application shows a Stories row with a height of 200 logical pixels.
onDataLoadComplete(true, ..., dataCount) updates the status to Loaded N stories
when content is available, and the tiles can open the native player. A successful
load with zero items shows No published stories; initialization success alone
does not establish that your category contains available content. A failed load
shows the error and allows another reload.
If initialization fails, inspect result.message and confirm the key and network
access. If the row stays empty, check the category/tenant, published content and
layout first. See Troubleshooting for symptom-led checks.
Next steps#
- More experiences: List Components, Home, Embedded Clips, Cards
- Appearance: Themes
- Events: Analytics and Event Streams
- Navigation: Deep Linking
- Monetization: Ads