Flutter Guide for Codox + QuillJS
This guide explains how to add basic Codox integration with QuillJS into Flutter application with
InAppWebView plugin
GitHub
Full integration example can be found here
Main Steps of the Integration
- Flutter loads local html, which contains prepared javascript code with quill editor and codox init
- flutter app invokes js function to setup initial editor state.
- flutter app adds listeners (callbacks) for codox hooks and error events.
- flutter app triggers js function which starts codox with config.
- When codox hook is triggered in javascript, the flutter corresponding callback is invoked and data is passed to flutter.
Basic Integration
Begin by preparing local files in the project.
Create seprate directory assets/ in project root to store html and javascript files
mkdir assets
Prepare local html
Navigate to created directory assets/ and create local html file quill.html there
cd assets/ touch quill.html
Download Codox lib
Create separate directory codoxLib in the project root for downloading Codox lib
mkdir codoxLib/
For downloading Codox Lib need NodeJS to be installed in local machine. Before proceed to next step, check if nodejs is installed
Navigate to codoxLib/ and create package.json file for javascript dependencies
cd codoxLib/
touch package.json
Insert Codox lib @codoxhq/quill-provider dependency into package.json file
{ "dependencies": { "@codoxhq/quill-provider": "^1.0.1" }}
Install Codox lib locally
# will create node_modules/ dir and download codox provider inside npm install
Copy downloaded Codox lib file into assets/ to place the lib with local html file
# copy from source dir to local target dir with renaming to codox.js cp codoxLib/node_modules/@codoxhq/quill-provider/dist/index.js assets/codox.js
At this step, should have assets/ directory with following structure:
assets/ quill.html codox.js
QuillJS Editor init
In local html quill.html add the following init Quill editor code.
<!DOCTYPE html><html lang="en"> <head> <title>Codox+QuillJS Integration</title> <!-- Include quill css stylesheet --> <link href="https://cdn.quilljs.com/1.3.7/quill.snow.css" rel="stylesheet" /> <!-- Include the Quill library --> <script src="https://cdn.quilljs.com/1.3.7/quill.js"></script> <!-- Import codox lib core from local file --> <script src="file:///android_asset/flutter_assets/assets/codox.js"></script> </head> <body> <!-- Create the editor container --> <div id="editor"></div>
<script> // create editor toolbar options var toolbarOptions = [ [ 'bold', 'italic', 'underline', 'strike', 'blockquote', 'code-block', 'image', 'clean', ], [{ header: 1 }, { header: 2 }], [{ list: 'ordered' }, { list: 'bullet' }], ['link'], [{ script: 'sub' }, { script: 'super' }], [{ intent: '-1' }, { intent: '+1' }], [{ direction: 'rtl' }], [{ size: ['small', false, 'large', 'huge'] }], [{ header: [1, 2, 3, 4, 5, 6, false] }], [{ color: [] }, { background: [] }], [{ font: [] }], [{ align: [] }], ];
// create Quill Editor instance var quill = new Quill('#editor', { modules: { toolbar: toolbarOptions, // tollbar options }, theme: 'snow', // css theme placeholder: 'Enter some text...', }); </script> </body></html>
InAppWebView init
In flutter app in lib/main.dart init InAppWebView with local html with Quill editor load.
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!, /** * IMPORTANT: * Setting baseUrl is essential for codox sync to work: * base url is considered by codox as "domain" which is allowed by codox subscription. * Example: codox subscription has whitelisted domain "flutter_demo.app", configured in codox dashboard, * then here need to specify baseUrl with http prefix, like "http://flutter_demo.app" */ 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 ''; } }}
Initial Editor Content
Before initiating or joining a Codox session, the latest version of the document content must be available and must be setup in editor. The following code shows how to set up initial content from flutter app into quill editor.
In quill.html need to add javascript function to set init content to quill editor:
var quill = new Quill('#editor', { ...options,});// callback to set initial state for editorfunction setQuillInitState(initQuillState) { // Set initial content to Quill quill.setContents(initQuillState, 'silent');}
In flutter in lib/main.dart need to add “onLoadStop” callback to InAppWebView and invoke javascript inside it. OnLoadStop is invoked when page is fully loaded.
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
Setup start session code in quill.html.
var codox = null; // codox instance will be assigned
// create Quill Editor instancevar 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);}
Setup flutter code to invoke javascript to start codox session inside OnLoadStop callback:
...
@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()"); }, ); } } }, ), );} ...
Codox hooks and error events
To subscribe to codox hooks and error events, need to add related code both on javascript and flutter sides.
In quill.html need to add hooks and error events callbacks to codox config. The callbacks contain code which sends data to flutter
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 = { 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 // provide hooks callbacks in config hooks: { fetchDocOnNetworkReconnect: fetchDocOnNetworkReconnectHook, usersUpdate: usersUpdateHook, contentChanged: contentChangedHook, }, };
// async start codox codox.start(codoxConfig);}
On flutter side need to implement the handling of javascript messages
@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"); }); }, // restt of params ); } } }, ), ); }
Github
Working integration example can be found here