Flutter Guide for Codox + QuillJS
This guide shows you how to integrate Codox with QuillJS in a Flutter application using the InAppWebView plugin.
GitHub
Find the full integration example here.
Main Steps of the Integration
-
Create and load an HTML file with the Quill editor and Codox initialization logic using a WebView.
-
In Flutter, fetch the initial document state from the backend and initialize the Quill editor.
-
In Flutter, set up listeners (callbacks) to respond to Codox client library session and error events.
-
Explicitly start Codox session via a javascript interface.
-
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 -
Navigate to the assets/ directory and create an HTML file:
Terminal window cd assets/touch quill.html -
Create a directory for Codox library files:
Terminal window mkdir codoxLib/ -
Navigate to the codoxLib/ directory and create a package.json file:
Terminal window cd codoxLib/npm init -y -
Install the library:
Terminal window npm install @codoxhq/quill-provider@latest -
Copy the Codox library to the assets/ directory:
Terminal window cp codoxLib/node_modules/@codoxhq/quill-provider/dist/index.js assets/codox.jsThe
assets/
directory should now look like this:Terminal window assets/├── quill.html└── codox.js
Step 2: Initialize QuillJS Editor
Add the following code to quill.html
to set up the Quill editor:
<!DOCTYPE html><html lang="en"> <head> <title>Codox+QuillJS Integration</title> <!-- Include Quill CSS --> <link href="https://cdn.quilljs.com/1.3.7/quill.snow.css" rel="stylesheet" /> <!-- Include Quill Library --> <script src="https://cdn.quilljs.com/1.3.7/quill.js"></script> <!-- Import Codox Library --> <script src="file:///android_asset/flutter_assets/assets/codox.js"></script> </head> <body> <!-- Editor Container --> <div id="editor"></div>
<script> var toolbarOptions = [ ['bold', 'italic', 'underline', 'strike'], [{ header: 1 }, { header: 2 }], [{ list: 'ordered' }, { list: 'bullet' }], ['link', 'image'], ];
var quill = new Quill('#editor', { modules: { toolbar: toolbarOptions }, theme: 'snow', placeholder: 'Start typing...', }); </script> </body></html>
Step 3: Initialize InAppWebView in Flutter
To display the HTML/Javascript 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, ), ), ); } } }, ), ); }
Future<String> _loadLocalHtml() async { try { // Load the local HTML file content final String htmlString = await rootBundle.loadString('assets/quill.html'); if (htmlString.isEmpty) { throw Exception('HTML content is empty'); } return htmlString; } catch (e) { print('Error loading HTML file: $e'); return ''; } }}
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 QuillJS editor component. The following code shows one way to set up this up from the Flutter app.
Add an initialization function in your HTML/Javascript quill.html
to expose a method for Flutter to set up the editor with initial content.
var quill = new Quill('#editor', { ...options,});// callback to set initial state for editorfunction setQuillInitState(initQuillState) { // Set initial content to Quill quill.setContents(initQuillState, 'silent');}
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 { // set initial quill editor state String initQuillStateJSON = ""; // json is expected here // invoke javascript function and pass state as arg await _webViewController.evaluateJavascript( source: "setQuillInitState($initQuillStateJSON)"); }, ); } } }, ), ); }}
Start Codox Session
To start or joining a Codox session, you need to explicitly call codox.start
to connect to the session.
Setup start session code in quill.html
:
let codox = null;
// create Quill Editor instancelet quill = new Quill('#editor', { // editor params});
// function to start codox sessionfunction startCodox(/* can pass params here, e.g. docId, apiKey, username */) { // create new instance codox = new Codox();
// create codox config const codoxConfig = { app: 'quilljs', editor: quill, docId: 'demo_docId', //this is the unique id used to distinguish different documents username: 'demo_username', //unique user name apiKey: '[your apiKey here]', // codox apiKey }; // async start codox codox.start(codoxConfig);}
Then, invoke this function from Flutter after the page has been fully loaded using the onLoadStop
callback from InAppWebView, in lib/main.dart
. Note that starting the session proceeds after the initial document state has been passed into the editor.
... @overrideWidget 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( // init params ), initialOptions: InAppWebViewGroupOptions( // init options ), onWebViewCreated: (controller) { _webViewController = controller; }, onLoadStop: (controller, url) async { /** * When page is fully loaded: * - set initial state to editor * - start codox */
// set initial quill editor state String initQuillStateJSON = ""; await _webViewController.evaluateJavascript( source: "setQuillInitState($initQuillStateJSON)");
/** * Start codox. */ await _webViewController.evaluateJavascript( source: "startCodox()"); }, ); } } }, ), );} ...
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.
In quill.html
, define callback functions and include them in the config object passed to Codox. These callbacks are triggered internally when specific events occur and serve as bridges to invoke corresponding handlers in the Flutter app.
var codox = null; // codox instance will be assigned
// create Quill Editor instancevar quill = new Quill('#editor', { // editor params});
// callback for fetchDocOnNetworkReconnect hookasync function fetchDocOnNetworkReconnectHook() { // invoke flutter handler and wait for response const data = await window.flutter_inappwebview.callHandler( 'fetchDocOnNetworkReconnectHookHandler' ); return data;}// callback for usersUpdate hookfunction usersUpdateHook(data) { const json = JSON.stringify(data); // send data to flutter window.flutter_inappwebview.callHandler( 'usersUpdateHookHandler', json );}// callback for contentChanged hookfunction contentChangedHook(data) { const json = JSON.stringify(data); // send data to flutter window.flutter_inappwebview.callHandler( 'contentUpdatedHookHandler', json );}
// callback for codox error eventsfunction onCodoxError(data) { // pass data to flutter when errors from codox window.flutter_inappwebview.callHandler( 'codoxErrorEventListener', JSON.stringify(data) );}
// start codox sessionfunction startCodox() { // create new instance codox = new Codox();
// subscribe to codox error events and pass callback codox.on('error', onCodoxError);
// demo username - random number generation let username = `user_${Math.round(Math.random() * 1000)}`; // create codox config const codoxConfig = { // ...
// provide hooks callbacks in config hooks: { fetchDocOnNetworkReconnect: fetchDocOnNetworkReconnectHook, usersUpdate: usersUpdateHook, contentChanged: contentChangedHook, }, };
// async start codox codox.start(codoxConfig);}
On the Flutter side you need to setup handlers to manage JavaScript messages sent from Codox, like the following example in lib/main.dart
:
@overrideWidget 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( // initt data params ), initialOptions: InAppWebViewGroupOptions( // init options ), onWebViewCreated: (controller) { _webViewController = controller; /** * Add listeners to codox hooks: * - 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) { String data = args[0]; print("[contentUpdatedHookHandler]: $data"); // get full state json dynamic fullState = _webViewController.evaluateJavascript( source: "getQuillEditorState()"); print("quill editor full state json: $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