Flutter Guide for Codox + Draft
This guide explains how to add basic Codox integration with Draft into Flutter application with InAppWebView plugin
GitHub
Full integration example can be found here
Main Steps of the Integration
- Clone the React + Draft + Codox starter project, configure the Codox settings, and build it locally.
- In Flutter, load the locally built ReactJS application which contains JavaScript code that initializes the Draft editor and Codox client module.
- In Flutter, fetch the initial state from the backend and initialize the Draft editor.
- In Flutter, set up listeners (callbacks) to respond to Codox client library session and error events.
- Start Codox session
- When Codox triggers hooks in the JavaScript code, corresponding callbacks in Flutter are invoked, passing the necessary data.
Step-by-Step Integration Tutorial
Step 1: Set Up Local Files
The first step is to prepare the local files for your project. Create a directory named assets/
in the root of your Flutter project to store HTML and JavaScript files.
mkdir assets
Step 2: Prepare a ReactJS App with Draft and Codox
-
Check Node.js installation: Install and download Node.js.
-
Clone the starter project: Use the following command to clone the a React + Draft + Codox starter project:
Terminal window git clone https://github.com/codoxhq/mobile-samples/tree/master/starters/flutter-reactjs-draft-codox -
Install dependencies: Navigate to the project directory and install the required dependencies:
Terminal window npm install -
Configure Codox settings: Update the
codoxConfig
variable insrc/lexical/App.jsx
with your uniquedocId
,username
, andapiKey
:const codoxConfig = {docId: '[document id]', // Unique ID for the documentusername: '[unique username]', // Unique usernameapiKey: '[codox apiKey]', // API key from Codox// Keep the rest of the configuration unchanged};Note: In this example, the
codoxConfig
is defined inside the React app. You can also pass these parameters Flutter app into the ReactJS app for more dynamic control. -
Build the ReactJS app: Run the following command to build the project:
Terminal window npm run build -
Copy the build to assets: Copy the generated build directory to the
assets/
directory of your Flutter project and rename it todraft/
:Terminal window cp -r build/ ../[your_flutter_project]/assets/draft/Your
assets/
directory should now look like this:Terminal window assets/└── draft/├── index.html├── static/│ ├── js/│ └── css/└── ...other files
Step 3: Initialize InAppWebView in Flutter
To display the ReactJS app in Flutter, we will use the InAppWebView plugin. The following code in lib/main.dart
sets this up:
import 'dart:async';import 'dart:io';import 'package:flutter/material.dart';import 'package:flutter/services.dart' show rootBundle; // Import rootBundleimport 'package:flutter_inappwebview/flutter_inappwebview.dart';
Future main() async { WidgetsFlutterBinding.ensureInitialized(); runApp( MaterialApp( theme: ThemeData(useMaterial3: true), home: const MyWebViewPage(), ), );}
class MyWebViewPage extends StatefulWidget { const MyWebViewPage({super.key});
@override _MyWebViewPageState createState() => _MyWebViewPageState();}
class _MyWebViewPageState extends State<MyWebViewPage> { late InAppWebViewController _webViewController;
@override Widget build(BuildContext context) { return Scaffold( body: FutureBuilder<String>( future: _loadLocalHtml(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { if (snapshot.hasData) { return InAppWebView( initialData: InAppWebViewInitialData( data: snapshot.data!, /** * Setting baseUrl to a domain whitelisted in * the Subscription dashboard */ baseUrl: Uri.parse("http://[domain from Codox subscription]"), mimeType: 'text/html', encoding: 'utf-8', ), initialOptions: InAppWebViewGroupOptions( crossPlatform: InAppWebViewOptions( javaScriptEnabled: true, // Important: this must be enabled for js to work in local html useOnLoadResource: true, allowUniversalAccessFromFileURLs: true, allowFileAccessFromFileURLs: true, ), ), onLoadStop: (controller, url) async { /** * When page is fully loaded: * - inject js/css scripts * * Reason for separate injection of js/css is that when flutter loads local html with custom baseURL, * the js/css are not loaded by default. * */ injectJsCss(controller); }, ); } } }, ), ); }
Future<String> _loadLocalHtml() async { try { // Load the local HTML file content final String htmlString = await rootBundle.loadString('assets/draft/index.html'); if (htmlString.isEmpty) { throw Exception('HTML content is empty'); } return htmlString; } catch (e) { print('Error loading HTML file: $e'); return ''; } } // Inject JS and CSS using JavaScript void injectJsCss(InAppWebViewController controller) async { // Inject the CSS file await controller.evaluateJavascript(source: """ var link = document.createElement('link'); link.rel = 'stylesheet'; link.href = 'file:///android_asset/flutter_assets/assets/draft/static/css/main.css'; // path to CSS document.head.appendChild(link); """);
// Inject the JS file await controller.evaluateJavascript(source: """ var script = document.createElement('script'); script.src = 'file:///android_asset/flutter_assets/assets/draft/static/js/main.js'; // path to JS document.body.appendChild(script); """); }}
Step 4: Initialize the Editor State
Before starting or joining a Codox session, the latest version of the document content must be available to the Draft editor component. The following code shows one way to set up this up from the Flutter app.
Add an initialization function in your ReactJS app to expose a method for Flutter to set up the editor with initial content. You can use the following window.initDraftEditor
code from the ReactJS starter project
window.initDraftEditor = function (initState) { try { // Convert json state to draft state const draftContentState = convertFromRaw(initState);
// Convert const draftState = EditorState.createWithContent(draftContentState); // save to local state setLocalEditorState(draftState); // launch codox session startCodoxSession(); } catch (err) { console.error('[initDraftEditor] error: ', err); }};
// other code...
Then, invoke this function from Flutter after the page has been fully loaded using the onLoadStop
callback from InAppWebView .
class _MyWebViewPageState extends State<MyWebViewPage> {
...
@override Widget build(BuildContext context) { return Scaffold( body: FutureBuilder<String>( ... builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { if (snapshot.hasData) { return InAppWebView( ...
onLoadStop: (controller, url) async { // init editor with codox with initial state String initDraftStateJSON = jsonEncode(initDraftStateRaw); await _webViewController.evaluateJavascript( source: "window.initDraftEditor($initDraftStateJSON);"); }, ); } } }, ), ); }}
Step 5: Start Codox Session
After the initial Draft editor state is set up, a Codox session should launch automatically based on the parameters in the config
object discussed in Step 2.
Optional: Codox hooks and error events
You can listen to events from Codox to allow the Flutter app to respond to content change notifications, member change notifications, and errors triggered within the Codox session. On the Flutter side you need to setup handlers to manage JavaScript messages sent from Codox, like the following example.
@overrideWidget build(BuildContext context) { return Scaffold( body: FutureBuilder<String>( builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { if (snapshot.hasData) { return InAppWebView( initialData: InAppWebViewInitialData( // init data params ), initialOptions: InAppWebViewGroupOptions( // init options ), onWebViewCreated: (controller) { _webViewController = controller; /** * Add listeners to codox events: * - contentUpdated hook * - usersUpdate * - fetchDocOnNetworkReconnectHook * * Add listener to codox errors */
_webViewController.addJavaScriptHandler( handlerName: "usersUpdateHookHandler", callback: (args) { String users = args[0]; print("[usersUpdateHookHandler]: $users"); });
_webViewController.addJavaScriptHandler( handlerName: "contentUpdatedHookHandler", callback: (args) { // get full state json String fullState = args[0]; print("[contentUpdatedHookHandler]: $fullState"); });
_webViewController.addJavaScriptHandler( handlerName: "fetchDocOnNetworkReconnectHookHandler", callback: (args) { print( "[fetchDocOnNetworkReconnectHookHandler] invoked");
// fetch state from backend // response must match schema: {content: {ops: [...]}, timestamp} return fetchedData; });
_webViewController.addJavaScriptHandler( handlerName: "codoxErrorEventListener", callback: (args) { String data = args[0]; print("[codoxErrorEventListener]: $data"); }); }, // rest of params ); } } }, ), ); }
Github
A working integration example can be found here