Skip to content

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

  1. Clone the React + Lexical + 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 Lexical editor and Codox client module.
  3. In Flutter, fetch the initial state from the backend and initialize the Lexical 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 Lexical 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 + Lexical + Codox starter project:

    Terminal window
    git clone https://github.com/codoxhq/mobile-samples/tree/master/starters/flutter-reactjs-lexical-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 lexical/:

    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.

@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 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:

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