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:

The Riddle Showcase URL

The Riddle Showcase URL is made up of two parts:

To find your Riddle ID:

  1. In your Riddle, go to PUBLISH.
  2. Click on Embed / Showcase.
  3. Click on GET THE CODE.
    In this code you will find the Riddle Showcase URL.
  4. Copy the Riddle Showcase URL.
  5. 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';
}

showcase url

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.