Quick Start
Quick Start
In this tutorial you’ll create a simple Flutter app that supports rich text editing with Fleather. What you’ll learn:
- How to create a new screen for the editor
- Basic widget layout required by Fleather
- How to load and save documents using JSON serialization
Create a new Flutter project
If you haven’t installed Flutter yet then install it first.
Create a new project using Terminal and flutter create
command:
$ flutter create myapp
$ cd myapp
For more methods of creating a project see official documentation.
Add Fleather to your project
Add fleather
& quill_delta
package as a dependency to pubspec.yaml
of your new project:
dependencies:
fleather: [ latest_version ]
quill_delta: [ latest_version ]
quill_delta
provide the tools to create and manipulate Delta
s, more on this here
And run flutter packages get
. This installs fleather, quill_delta and all
required dependencies, including parchment package which
implements Fleather’s document model.
Parchment package is platform-agnostic and can be used outside of Flutter apps (in web or server-side Dart projects).
Create editor page
We start by creating a StatefulWidget
that will be responsible for handling all the state and
interactions with Fleather. In this example we’ll assume that there is dedicated editor page in our
app.
Create a new file lib/src/editor_page.dart
and type in (or paste) the following:
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:quill_delta/quill_delta.dart';
import 'package:fleather/fleather.dart';
class EditorPage extends StatefulWidget {
const EditorPage({super.key});
@override
EditorPageState createState() => EditorPageState();
}
class EditorPageState extends State<EditorPage> {
/// Allows to control the editor and the document.
FleatherController? _controller;
/// Fleather editor like any other input field requires a focus node.
late FocusNode _focusNode;
@override
void initState() {
super.initState();
// Here we must load the document and pass it to Fleather controller.
final document = _loadDocument();
_controller = FleatherController(document);
_focusNode = FocusNode();
}
@override
Widget build(BuildContext context) {
// Note that a default tools bar is provided
// Here we decide to position it at the buttom for mobile device and
// at the top for desktops
return Scaffold(
appBar: AppBar(title: const Text('Editor page')),
body: Column(
children: [
if (!Platform.isAndroid && !Platform.isIOS)
FleatherToolbar.basic(controller: _controller),
Expanded(
child: FleatherEditor(
padding: const EdgeInsets.all(16),
controller: _controller,
focusNode: _focusNode,
),
),
if (Platform.isAndroid || Platform.isIOS)
FleatherToolbar.basic(controller: _controller),
],
),
);
}
/// Loads the document to be edited in Fleather.
ParchmentDocument _loadDocument() {
// For simplicity we hardcode a simple document with one line of text
// saying "Fleather Quick Start".
// (Note that delta must always end with newline.)
final Delta delta = Delta()..insert('Fleather Quick Start\n');
return ParchmentDocument.fromDelta(delta);
}
}
Above example widget creates a page with an AppBar
and Fleather editor in its body. We also
initialize our editor with a simple one-line document.
Now we need to wire it up with our app. Open lib/main.dart
and replace autogenerated contents with
this:
import 'package:flutter/material.dart';
import 'src/editor_page.dart';
void main() {
runApp(const QuickStartApp());
}
class QuickStartApp extends StatelessWidget {
const QuickStartApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Quick Start',
home: const HomePage(),
routes: {
"/editor": (context) => const EditorPage(),
},
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
final navigator = Navigator.of(context);
return Scaffold(
appBar: AppBar(title: const Text("Quick Start")),
body: Center(
child: TextButton(
child: const Text("Open editor"),
onPressed: () => navigator.pushNamed("/editor"),
),
),
);
}
}
Save document to JSON file
At this point we can already edit the document and apply styles, however if we navigate back from this page our changes will be lost. Let’s fix this and add a button which saves the document to the device’s file system.
First we need a function to save the document. Update lib/src/editor_page.dart
as follows:
// change: add these two lines to imports section at the top of the file
import 'dart:convert'; // access to jsonEncode()
import 'dart:io'; // access to File and Directory classes
class EditorPageState extends State<EditorPage> {
// change: add after _loadDocument()
void _saveDocument(BuildContext context) {
// Parchment documents can be easily serialized to JSON by passing to
// `jsonEncode` directly
final contents = jsonEncode(_controller!.document);
// For this example we save our document to a temporary file.
final file = File('${Directory.systemTemp.path}/quick_start.json');
// And show a snack bar on success.
file.writeAsString(contents).then((_) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Saved.')),
);
});
}
}
This function converts our document using jsonEncode()
function and writes the result to a
file quick_start.json
in the system’s temporary directory.
Note that File.writeAsString
is an asynchronous method and returns Dart’s
Future
. This is why we register a completion callback with a call to
Future.then
.
One more important bit here is that we pass BuildContext
argument to
_saveDocument
. This is required to get access to our page’s ScaffoldMessenger
state, so that we can show
a SnackBar
.
Now we just need to add a button to the AppBar, so we need to modify build
method as follows:
class EditorPageState extends State<EditorPage> {
// change: replace build() method with following
@override
Widget build(BuildContext context) {
// Note that the editor requires special `FleatherScaffold` widget to be
// present somewhere up the widget tree.
return Scaffold(
appBar: AppBar(
title: Text("Editor page"),
// <<< begin change
actions: <Widget>[
IconButton(
icon: const Icon(Icons.save),
onPressed: () => _saveDocument(context),
),
],
// end change >>>
),
body: FleatherScaffold(
child: FleatherEditor(
padding: EdgeInsets.all(16),
controller: _controller,
focusNode: _focusNode,
),
),
);
}
}
Now we can reload our app, hit “Save” button and see the snack bar.
Load document from JSON file
Since we now have this document saved to a file, let’s update our
_loadDocument
method to load saved file if it exists.
class EditorPageState extends State<EditorPage> {
// change: replace _loadDocument() method with following
/// Loads the document asynchronously from a file if it exists, otherwise
/// returns default document.
Future<ParchmentDocument> _loadDocument() async {
final file = File(Directory.systemTemp.path + "/quick_start.json");
if (await file.exists()) {
final contents = await file.readAsString();
return ParchmentDocument.fromJson(jsonDecode(contents));
}
final Delta delta = Delta()
..insert("Fleather Quick Start\n");
return ParchmentDocument.fromDelta(delta);
}
}
We had to convert this method to be async because file system operations are asynchronous. This
breaks our initState
logic so we need to fix it next. However we can no longer
initialize FleatherController
in initState
and therefore can’t display the editor until document
is loaded.
One way to fix this is to show loader animation while we are reading our document from file. But
first, we still need to update initState
method:
class EditorPageState extends State<EditorPage> {
// change: replace initState() method with following
@override
void initState() {
super.initState();
_focusNode = FocusNode();
_loadDocument().then((document) {
setState(() {
_controller = FleatherController(document);
});
});
}
}
We initialize _controller
only when our document is fully loaded from the file system. An
important part here is to update _controller
field inside of
setState
call as required by Flutter’s StatefulWidget
’s contract.
The only thing left is to update build()
method to show loader animation:
class EditorPageState extends State<EditorPage> {
// change: replace build() method with following
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Editor page'),
actions: <Widget>[
IconButton(
icon: const Icon(Icons.save),
onPressed: () => _saveDocument(context),
),
],
),
// Note that the editor requires special `FleatherScaffold` widget to be
// one of its parents.
body: _controller == null
? const Center(child: CircularProgressIndicator())
: Column(
children: [
if (!Platform.isAndroid && !Platform.isIOS)
FleatherToolbar.basic(controller: _controller!),
Expanded(
child: FleatherEditor(
padding: const EdgeInsets.all(16),
controller: _controller!,
focusNode: _focusNode,
),
),
if (Platform.isAndroid || Platform.isIOS)
FleatherToolbar.basic(controller: _controller!),
],
),
);
}
}
If we save changes now and reload the app we should see something like this.
Note that in your tests you’ll likely not notice any loading animation at all. This is because reading a tiny file from disk is too fast. For the above recording we added an artificial delay of 1 second in order to demonstrate loading. If you’d like to replicate this, we’ll leave implementation of this task to you as an exercise.