Skip to content

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

  1. Clone the React + Draft + Codox starter project, configure the Codox settings, and build it locally.
  2. In Flutter, load the locally built ReactJS application which contains JavaScript code that initializes the Draft editor and Codox client module.
  3. In Flutter, fetch the initial state from the backend and initialize the Draft editor.
  4. In Flutter, set up listeners (callbacks) to respond to Codox client library session and error events.
  5. Start Codox session
  6. 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.

Terminal window
mkdir assets

Step 2: Prepare a ReactJS App with Draft and Codox

  1. Check Node.js installation: Install and download Node.js.

  2. 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
  3. Install dependencies: Navigate to the project directory and install the required dependencies:

    Terminal window
    npm install
  4. Configure Codox settings: Update the codoxConfig variable in src/lexical/App.jsx with your unique docId, username, and apiKey:

    const codoxConfig = {
    docId: '[document id]', // Unique ID for the document
    username: '[unique username]', // Unique username
    apiKey: '[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.

  5. Build the ReactJS app: Run the following command to build the project:

    Terminal window
    npm run build
  6. Copy the build to assets: Copy the generated build directory to the assets/ directory of your Flutter project and rename it to draft/:

    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 rootBundle
import '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

src/components/editor/EditorContainer.jsx
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.

@override
Widget 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