Skip to content

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

  1. Create and load an HTML file with the Quill editor and Codox initialization logic using a WebView.

  2. In Flutter, fetch the initial document state from the backend and initialize the Quill editor.

  3. In Flutter, set up listeners (callbacks) to respond to Codox client library session and error events.

  4. Explicitly start Codox session via a javascript interface.

  5. 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

  1. 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
  2. Navigate to the assets/ directory and create an HTML file:

    Terminal window
    cd assets/
    touch quill.html
  3. Create a directory for Codox library files:

    Terminal window
    mkdir codoxLib/
  4. Navigate to the codoxLib/ directory and create a package.json file:

    Terminal window
    cd codoxLib/
    npm init -y
  5. Install the library:

    Terminal window
    npm install @codoxhq/quill-provider@latest
  6. Copy the Codox library to the assets/ directory:

    Terminal window
    cp codoxLib/node_modules/@codoxhq/quill-provider/dist/index.js assets/codox.js

    The 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 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,
),
),
);
}
}
},
),
);
}
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 editor
function 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 instance
let quill = new Quill('#editor', {
// editor params
});
// function to start codox session
function 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.

...
@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(
// 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 instance
var quill = new Quill('#editor', {
// editor params
});
// callback for fetchDocOnNetworkReconnect hook
async function fetchDocOnNetworkReconnectHook() {
// invoke flutter handler and wait for response
const data = await window.flutter_inappwebview.callHandler(
'fetchDocOnNetworkReconnectHookHandler'
);
return data;
}
// callback for usersUpdate hook
function usersUpdateHook(data) {
const json = JSON.stringify(data);
// send data to flutter
window.flutter_inappwebview.callHandler(
'usersUpdateHookHandler',
json
);
}
// callback for contentChanged hook
function contentChangedHook(data) {
const json = JSON.stringify(data);
// send data to flutter
window.flutter_inappwebview.callHandler(
'contentUpdatedHookHandler',
json
);
}
// callback for codox error events
function onCodoxError(data) {
// pass data to flutter when errors from codox
window.flutter_inappwebview.callHandler(
'codoxErrorEventListener',
JSON.stringify(data)
);
}
// start codox session
function 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:

@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(
// 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