Initial SDK Setup
This guide covers installation, initialization, using widgets (Bubble/Card), the Standalone Player, callbacks/listeners and analytics, with parameter explanations and examples. iOS callbacks are aligned with the native SDKs (listeners and analytics).
Compatibility
- Flutter: 3.0.0+
- Dart SDK: 2.17.0+
- iOS: 12.0+
- Android: API 21+
Installation
Adding dependency
JOIN Stories Flutter SDK is available via Pub dev repository. In order to integrate SDK in your app, please specify JOIN Stories dependency in your pubspec.yaml
file.
dependencies:
flutter:
sdk: flutter
join_stories_flutter: ^<latest_version>
SDK Installation
TeamId and widget alias
You will need your team id and the widgets' alias you want to integrate. You can find both of them in the Integration tab of your widget mobile on studio.
To create a widget, check the documentation
Call early (Splash/Launch)
import 'package:join_stories_flutter/join_stories_flutter.dart';
await JOINStories.initialize(teamId: '<your_team_id>');
// Optional if an API Key is provided by JOIN
await JOINStories.initialize(teamId: '<your_team_id>', apiKey: '<api_key>');
- teamId (String, required): your JOIN team identifier
- apiKey (String, optional): JOIN API key
With Flutter, simply call JOINStories.initialize(teamId: ..., apiKey: ...). The plugin forwards the API key to the native SDKs.
Built-in Widgets
import 'package:flutter/material.dart';
import 'package:join_stories_flutter/join_stories_flutter.dart';
BubbleWidget(alias: 'widget-alias')
Good job !
You have integrated your first component and are ready to see stories in your application.
Customization
JoinStoriesView is the default view (or trigger). It displays stories in a bubble format like Instagram. You can use other formats like Card. For more info, see UI Customizations.

Player Standalone Mode
If you don't need to display bubbles (or cards) and just want to open the player following an action (event, button click, etc.), you can use the player in standalone mode. Simply call a method to open the player as follows :
import 'package:join_stories_flutter/join_stories_flutter.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await JOINStories.initialize(teamId: '<your_team_id>');
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Standalone JOIN Stories')),
body: Center(
child: ElevatedButton(
onPressed: () async {
await JOINStories.startPlayer('widget-alias');
},
child: const Text('Open stories'),
),
),
),
);
}
}
It will open on the first story of the given widget.
Refreshing a JOIN Stories widget (Bubble or Card)
Use a controller to trigger a manual refresh of a rendered widget without rebuilding it. The controller is attached automatically when the native view is created, and its refresh()
method re-fetches and re-renders content.
- Controllers: BubbleController for BubbleWidget, CardController for CardWidget.
- Attach: Pass the controller via the widget’s controller parameter; it will be attached on native view creation.
- Call:
await controller.refresh()
to re-fetch data and update the view. - When to refresh:
- After user state changes: login/logout, profile update
- After segmentation changes: JOINStories.setSegmentationKey(...)
- After tracking changes: JOINStories.setTrackingUserId(...)
- On pull-to-refresh or explicit “Refresh” actions
- On app resume: if you need fresh content when returning to the app
- Prerequisites: Ensure
JOINStories.initialize(...)
has completed before rendering or refreshing widgets.
Quick start (BubbleWidget)
import 'package:flutter/material.dart';
import 'package:join_stories_flutter/join_stories_flutter.dart';
class BubbleSection extends StatefulWidget {
const BubbleSection({super.key});
@override
State<BubbleSection> createState() => _BubbleSectionState();
}
class _BubbleSectionState extends State<BubbleSection> with WidgetsBindingObserver {
final BubbleController bubbleController = BubbleController();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
// Optional: refresh on app resume
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
bubbleController.refresh();
}
}
@override
Widget build(BuildContext context) {
return Column(
children: [
BubbleWidget(
alias: 'widget-alias',
controller: bubbleController,
// Optional: call immediately after native view is created
onNativeViewCreated: (_) => bubbleController.refresh(),
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () async {
await bubbleController.refresh();
},
child: const Text('Refresh stories'),
),
],
);
}
}
Quick start (CardWidget – list or grid)
class CardSection extends StatefulWidget {
const CardSection({super.key});
@override
State<CardSection> createState() => _CardSectionState();
}
class _CardSectionState extends State<CardSection> {
final CardController cardController = CardController();
@override
Widget build(BuildContext context) {
return Column(
children: [
// Use isGrid: true for a grid
CardWidget(
alias: 'widget-alias',
isGrid: false,
controller: cardController,
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () async {
await cardController.refresh();
},
child: const Text('Refresh cards'),
),
],
);
}
}
Refresh after updating segmentation or user tracking
// Example: update segmentation and refresh the visible widgets
await JOINStories.setSegmentationKey('vip_user');
// If you’re showing multiple widgets, call refresh on each controller in view
await bubbleController.refresh();
await cardController.refresh();
// Example: update tracking user ID then refresh
await JOINStories.setTrackingUserId('user-123');
await bubbleController.refresh();
Best practices and notes
- Keep controllers stable: Create controllers once (e.g., as
final
fields inState
), not insidebuild()
. Passing a new controller each rebuild detaches the previous one. - Wait for attachment:
refresh()
is a no-op until the native view is attached. UseonNativeViewCreated
if you need an immediate refresh on first render. - Avoid redundant calls: Debounce user-triggered refresh actions to prevent excessive requests.
- Threading:
refresh()
is async;await
it if subsequent logic depends on completion. - Multiple widgets: Maintain one controller per widget instance; refresh them independently as needed.
Updated 7 days ago