Flutter Guide for Codox + Lexical
This guide explains how to add basic Codox integration with Lexical into Flutter application with InAppWebView plugin
GitHub
A full integration example is available at GitHub.
Main Steps of the Integration
- Clone the React + Lexical + 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 Lexical editor and Codox client module.
- In Flutter, fetch the initial state from the backend and initialize the Lexical 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 Lexical and Codox
-
Check Node.js installation: Install and download Node.js.
-
Clone the starter project: Use the following command to clone the a React + Lexical + Codox starter project:
Terminal window git clone https://github.com/codoxhq/mobile-samples/tree/master/starters/flutter-reactjs-lexical-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 tolexical/
:Terminal window cp -r build/ ../[your_flutter_project]/assets/lexical/Your
assets/
directory should now look like this:Terminal window assets/└── lexical/├── 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 'package:flutter/material.dart';import 'package:flutter_inappwebview/flutter_inappwebview.dart';
Future main() async { runApp(MaterialApp(home: const MyWebViewPage()));}
class MyWebViewPage extends StatefulWidget { const MyWebViewPage({Key? key}) : super(key: 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 && 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, allowUniversalAccessFromFileURLs: true, ), ), ); } return const Center(child: CircularProgressIndicator()); }, ), ); }
Future<String> _loadLocalHtml() async { try { // Load the local HTML file content final String htmlString = await rootBundle.loadString("assets/lexical/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/lexical/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/lexical/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 Lexical 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.initLexicalEditor
code from the ReactJS
see starter code
window.initLexicalEditor = function (initState) { try { validateStateStructure(initState, LEXICAL_NODES_TO_REGISTER); setInitLexicalState(initState); // This starts Codox sync } catch (err) { console.error('Error initializing Lexical editor:', err); }};
Invoke Initialization from Flutter
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 initLexicalStateJSON = jsonEncode(initLexicalStateRaw); await _webViewController.evaluateJavascript( source: "window.initLexicalEditor($initLexicalStateJSON);"); }, ); } } }, ), ); }}
Step 5: Start Codox Session
After the initial Lexical 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>( future: _loadLocalHtml(), 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 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) { // 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 ); } } }, ), ); }
Optional: Content Insert Blacklisting
The ReactJS starter includes an optional callback for content blacklisting. For more details, refer to the Lexical integration guide.
To implement this in Flutter, register the corresponding handler in the onWebViewCreated
callback of InAppWebView
:
@overrideWidget build(BuildContext context) { return Scaffold( body: FutureBuilder<String>( future: _loadLocalHtml(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { if (snapshot.hasData) { return InAppWebView( onWebViewCreated: (controller) { _webViewController = controller;
// rest of handlers...
_webViewController.addJavaScriptHandler( handlerName: "onBlacklistedInsertHandler", callback: (args) { print( "[onBlacklistedInsertHandler] blacklisted content detected"); }); }, // rest of params ); } } }, ), ); }
Github
Working integration example can be found here