Riddle on Flutter
In this example, we will show you how to integrate a Riddle into a Flutter app. We will use the webview_flutter
package to display the Riddle in a WebView. The Riddle will be embedded using the Riddle URL.
There are two ways to use Riddle in your app:
- Riddle Showcase in a WebView in your app
- Your own website in a WebView in your app
Riddle Showcase in a WebView in your app
In this example, the Riddle Showcase URL will open in a WebView in your app.
What you need for this:
- A WebView in your app
- The Riddle Showcase URL: https://www.riddle.com/embed/a/ + your Riddle ID
The Riddle Showcase URL
The Riddle Showcase URL is made up of two parts:
- https://www.riddle.com/embed/a/
- Your Riddle ID
To find your Riddle ID:
- In your Riddle, go to PUBLISH.
- Click on Embed / Showcase.
- Click on GET THE CODE.
In this code you will find the Riddle Showcase URL. - Copy the Riddle Showcase URL.
- Enter the Riddle Showcase URL in this line:
late final String url;
RiddleWebViewComponent({
super.key,
required riddleId,
}) {
url = 'https://www.riddle.com/embed/a/$riddleId';
}
Riddle in your own web page in a WebView in your app
In this example, your own web page is opened in a WebView in your app. You can then embed Riddle in this webpage.
What you need for this:
- A WebView in your app
- Your own website with embedded Riddle
- Your domain must be activated for Riddle
Your own website with embedded Riddle
You can embed Riddle in your own website. It is important that you use the normal embed code.
Your domain must be activated for Riddle
For Riddle to work on your domain, it must be activated on Riddle. You can find out how this works here.
Enter your own URL in this line:
final String url = = 'https://www.example.com';
JavaScript to communicate with your app
In order for your app to communicate with Riddle, you need to run JavaScript in your WebView. This JavaScript then sends data to your app.
Riddle sends JavaScript postMessages when the status of the Riddle changes. You can receive these postMessages in your app and then respond accordingly.
/// JavaScript injizieren
Future<void> _injectJavaScript() async {
final script = """
// Sample JavaScript: Send message to Flutter
window.addEventListener("message", (event) => {
if (
event.origin !== "https://www.riddle.com" ||
!event.data.isRiddle2Event ||
!event.data.category ||
!event.data.category.startsWith("RiddleTrackEvent")
) {
return;
}
window.postMessageHandler.postMessage(JSON.stringify(event.data));
});
""";
await _controller.runJavaScript(script);
debugPrint('JavaScript was injected.');
}
Example Code - RiddleWebViewComponent
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_storytime/models/RiddleEvents.dart';
import 'package:webview_flutter/webview_flutter.dart';
class RiddleWebViewComponent extends StatefulWidget {
late final String url;
RiddleWebViewComponent({
super.key,
required riddleId,
}) {
url = 'https://www.riddle.com/embed/a/$riddleId';
}
@override
RiddleWebViewComponentState createState() => RiddleWebViewComponentState();
}
class RiddleWebViewComponentState extends State<RiddleWebViewComponent> {
late final WebViewController _controller;
late final List<RiddleEvent> _riddleEvents = [];
final _riddleEventController = StreamController<RiddleEvent>.broadcast();
/// Exposed stream for RiddleEvents
Stream<RiddleEvent> get riddleEventStream => _riddleEventController.stream;
@override
void initState() {
super.initState();
// Configure WebViewController
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
onPageStarted: (String url) {
debugPrint('Page is loading: $url');
},
onPageFinished: (String url) {
debugPrint('Page fully loaded: $url');
_injectJavaScript(); // Inject JavaScript
},
onWebResourceError: (WebResourceError error) {
debugPrint('Error loading the page: ${error.description}');
},
),
)
..addJavaScriptChannel(
'postMessageHandler', // Channelname
onMessageReceived: (message) {
_handleJavaScriptEvent(message.message); // Call up dart function
},
)
..loadRequest(
Uri.parse(widget.url),
);
}
@override
void dispose() {
_riddleEventController.close();
super.dispose();
}
/// Dart function that is triggered by JavaScript events
void _handleJavaScriptEvent(String jsonMessage) {
// Example: Processing JSON data
try {
debugPrint('Received JavaScript event Json: $jsonMessage');
final riddleEventJson = jsonDecode(jsonMessage);
final riddleEvent = RiddleEvent.fromJson(riddleEventJson);
debugPrint('Riddle-Event: ${riddleEvent.action}');
setState(() {
_riddleEvents.add(riddleEvent);
});
// Emit event via the stream
_riddleEventController.add(riddleEvent);
} catch (e) {
debugPrint('Error when processing JavaScript data: $e');
}
}
/// JavaScript injizieren
Future<void> _injectJavaScript() async {
final script = """
// Sample JavaScript: Send message to Flutter
window.addEventListener("message", (event) => {
if (
event.origin !== "https://www.riddle.com" ||
!event.data.isRiddle2Event ||
!event.data.category ||
!event.data.category.startsWith("RiddleTrackEvent")
) {
return;
}
window.postMessageHandler.postMessage(JSON.stringify(event.data));
});
""";
await _controller.runJavaScript(script);
debugPrint('JavaScript was injected.');
}
@override
Widget build(BuildContext context) {
return WebViewWidget(controller: _controller);
}
}
How to use the RiddleWebViewComponent
In the following example, we will show you how to use the RiddleWebViewComponent
in a Flutter app. The RiddleWebViewComponent
is used in the CreateStoryScreen
to display the Riddle in a WebView. The CreateStoryScreen
is a StatefulWidget that generates a story based on the user's input in the Riddle. The generated story is then saved in the database. The RiddleWebViewComponent
listens to the Riddle events and saves them in a list. When the Riddle is finished, the CreateStoryScreen
generates the story based on the user's input and saves it in the database. The CreateStoryScreen
uses the OpenAI
package to generate the story.
The Riddle could be a quiz, a survey, a poll, or any other type of interactive content. All the Questions and Answers are saved in the RiddleEvents list. The RiddleEvents list is then used to generate a Promt for the OpenAI API. The Promt is used to generate the story.
Example Promt
Create a story based on the following inputs:
1. Theme of the story: adventure
2. Main character: A dragon named Flamy
3. Location of the story: A magical forest
4. Goal of the main character: Find the greatest treasure in the world
5. Antagonist: A cunning goblin
6. Tone and style: Funny and entertaining
7. Length of the story: Mini (about 100 words)
8. Special details: The dragon is afraid of water
Use this information to create a creative, entertaining and readable story.
Example Code - CreateStoryScreen
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:dart_openai/dart_openai.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:flutter_storytime/models/RiddleEvents.dart';
import 'package:flutter_storytime/repositories/chapter_repository.dart';
import 'package:flutter_storytime/repositories/story_repository.dart';
import 'package:flutter_storytime/widgets/RiddleWebViewComponent.dart';
import '../models/story.dart';
import '../models/chapter.dart';
class CreateStoryScreen extends StatefulWidget {
const CreateStoryScreen({super.key});
@override
_CreateStoryScreenState createState() => _CreateStoryScreenState();
}
class _CreateStoryScreenState extends State<CreateStoryScreen> {
final storyRepo = StoryRepository();
final chapterRepo = ChapterRepository();
final _promptController = TextEditingController();
bool _isGenerating = false;
String? _generatedStory;
FocusNode _focusNode = FocusNode();
final List<RiddleEvent> _riddleEvents =
[]; // List for saving the RiddleEvents
final GlobalKey<RiddleWebViewComponentState> _webViewKey =
GlobalKey<RiddleWebViewComponentState>();
final String riddleId = dotenv.env['RIDDLE_ID']!;
final appBarTitle = 'page_title_story_create'.tr();
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_focusNode.requestFocus();
}
final webViewState = _webViewKey.currentState;
if (webViewState != null) {
if (kDebugMode) {
debugPrint('WebViewState found: $webViewState');
}
_subscribeToRiddleEvents(webViewState);
} else {
if (kDebugMode) {
debugPrint('WebViewState is not available');
}
}
});
}
@override
void dispose() {
_promptController.dispose();
_focusNode.dispose();
super.dispose();
}
/// Subscribe to the stream of the RiddleWebViewComponent and save events
void _subscribeToRiddleEvents(RiddleWebViewComponentState webViewState) {
if (kDebugMode) {
debugPrint('Subscribing to Riddle events');
}
webViewState.riddleEventStream.listen((event) {
if (event.action == 'Form_Submit') {
if (kDebugMode) {
debugPrint('Riddle-riddleEventStream: ${event.formAnswers}');
}
}
if (event.action == 'CoreMetrics' && event.name == 'Finish') {
if (kDebugMode) {
debugPrint('Riddle finished. Generating story...');
}
_generateStory(); // Generate the story
return;
}
setState(() {
_riddleEvents.add(event); // Add the event to the list
});
if (kDebugMode) {
debugPrint('Received event: ${event.category}');
}
});
}
Future<void> _generateStory() async {
// Filter all Block_Submit and Form_Submit events
final blockSubmitEvents = _riddleEvents
.where((event) =>
event.action == 'Block_Submit' || event.action == 'Form_Submit')
.toList();
var prompt = 'promt_first_line'.tr();
// Iterate over all Block_Submit events and add them to the prompt
for (final event in blockSubmitEvents) {
if (event.action == 'Block_Submit') {
// event.blockContent.title can be html content. convert it to plain text
final plainTextTitle =
event.blockContent?.title?.replaceAll(RegExp(r'<[^>]*>'), '');
prompt += '\n\n$plainTextTitle: ${event.answer}';
} else if (event.action == 'Form_Submit') {
for (final formAnswer in event.formAnswers!) {
// formAnswer.blockTitle can be html content. convert it to plain text
final plainTitle =
formAnswer.blockTitle?.replaceAll(RegExp(r'<[^>]*>'), '');
prompt += '\n\n$plainTitle: ${formAnswer.data}';
}
}
}
prompt += 'promt_last_line'.tr();
setState(() => _isGenerating = true);
if (kDebugMode) {
debugPrint('Prompt: $prompt');
}
try {
// OpenAI API call
final response = await OpenAI.instance.chat
.create(
model: "gpt-4o",
messages: [
OpenAIChatCompletionChoiceMessageModel(
role: OpenAIChatMessageRole.system,
content: [
OpenAIChatCompletionChoiceMessageContentItemModel.text(
"promt_role_system".tr())
], // String for the message
),
OpenAIChatCompletionChoiceMessageModel(
role: OpenAIChatMessageRole.user,
content: [
OpenAIChatCompletionChoiceMessageContentItemModel.text(prompt)
], // Prompt for the story
),
],
maxTokens: 1000,
)
.timeout(
const Duration(seconds: 300), // Timeout to 5 minutes
onTimeout: () {
throw Exception("request_timeout".tr());
},
);
// Access to the generated response
final message = response.choices.first.message;
String generatedStory;
// Check whether `content` is a list or a string
if (message.content is String) {
generatedStory = message.content as String;
} else if (message.content
is List<OpenAIChatCompletionChoiceMessageContentItemModel>) {
generatedStory = (message.content
as List<OpenAIChatCompletionChoiceMessageContentItemModel>)
.map((item) => item.text)
.join('\n');
} else {
throw Exception('unknown_content_type'
.tr(args: [message.content.runtimeType.toString()]));
}
if (kDebugMode) {
debugPrint("Generated Story: $generatedStory");
}
setState(() {
_generatedStory = generatedStory;
_isGenerating = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('story_generated_success'.tr())),
);
// Save the story
await _saveStory();
} catch (e) {
setState(() => _isGenerating = false);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('story_generation_error'.tr(args: [e.toString()]))),
);
}
}
Future<void> _saveStory() async {
if (_generatedStory == null || _generatedStory!.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('no_story_to_save'.tr())),
);
return;
}
final story = Story(
title: "generated_story_title".tr(),
summary: _generatedStory!.split('\n').take(3).join('\n'), // First 3 lines
);
final storyId = await storyRepo.addStory(story);
final chapters = [
Chapter(text: _generatedStory!, media: null, storyId: storyId)
];
await chapterRepo.addChapters(chapters);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('story_saved_success'.tr())),
);
// Navigate to the HomeScreen
Navigator.pushNamed(context, '/home');
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(appBarTitle),
),
body: RiddleWebViewComponent(
key: _webViewKey,
riddleId: riddleId,
),
);
}
}
Download the example
You can download the example code from here.