zefyr

Quick Start

In this tutorial you’ll create a simple Flutter app that supports rich text editing with Zefyr. What you’ll learn:

  • How to create a new screen for the editor
  • Basic widget layout required by Zefyr
  • How to load and save documents using JSON serialization

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

02. Add Zefyr to your new project

Add zefyr package as a dependency to pubspec.yaml of your new project:

dependencies:
  zefyr: [latest_version]

And run flutter packages get. This installs zefyr and all required dependencies, including notus package which implements Zefyr’s document model.

Notus package is platform-agnostic and can be used outside of Flutter apps, that is, on the web or server-side.

03. Create editor page

We start by creating a StatefulWidget that will be responsible for handling all the state and interactions with Zefyr. 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 'package:flutter/material.dart';
import 'package:quill_delta/quill_delta.dart';
import 'package:zefyr/zefyr.dart';

class EditorPage extends StatefulWidget {
  @override
  EditorPageState createState() => EditorPageState();
}

class EditorPageState extends State<EditorPage> {
  /// Allows to control the editor and the document.
  ZefyrController _controller;

  /// Zefyr editor like any other input field requires a focus node.
  FocusNode _focusNode;

  @override
  void initState() {
    super.initState();
    // Here we must load the document and pass it to Zefyr controller.
    final document = _loadDocument();
    _controller = new ZefyrController(document);
    _focusNode = new FocusNode();
  }

  @override
  Widget build(BuildContext context) {
    // Note that the editor requires special `ZefyrScaffold` widget to be
    // one of its parents.
    return Scaffold(
      appBar: AppBar(title: Text("Editor page")),
      body: ZefyrScaffold(
        child: ZefyrEditor(
          padding: EdgeInsets.all(16),
          controller: _controller,
          focusNode: _focusNode,
        ),
      ),
    );
  }

  /// Loads the document to be edited in Zefyr.
  NotusDocument _loadDocument() {
    // For simplicity we hardcode a simple document with one line of text
    // saying "Zefyr Quick Start".
    // (Note that delta must always end with newline.)
    final Delta delta = Delta()..insert("Zefyr Quick Start\n");
    return NotusDocument.fromDelta(delta);
  }
}

Above example widget creates a page with an AppBar and Zefyr 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(QuickStartApp());
}

class QuickStartApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Quick Start',
      home: HomePage(),
      routes: {
        "/editor": (context) => EditorPage(),
      },
    );
  }
}

class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final navigator = Navigator.of(context);
    return Scaffold(
      appBar: AppBar(title: Text("Quick Start")),
      body: Center(
        child: FlatButton(
          child: Text("Open editor"),
          onPressed: () => navigator.pushNamed("/editor"),
        ),
      ),
    );
  }
}

Here is how it might look when we run the app and navigate to editor page:

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) {
    // Notus 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((_) {
      Scaffold.of(context).showSnackBar(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 Scaffold 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 `ZefyrScaffold` widget to be
    // present somewhere up the widget tree.
    return Scaffold(
      appBar: AppBar(
        title: Text("Editor page"),
        // <<< begin change
        actions: <Widget>[
          Builder(
            builder: (context) => IconButton(
              icon: Icon(Icons.save),
              onPressed: () => _saveDocument(context),
            ),
          )
        ],
        // end change >>>
      ),
      body: ZefyrScaffold(
        child: ZefyrEditor(
          padding: EdgeInsets.all(16),
          controller: _controller,
          focusNode: _focusNode,
        ),
      ),
    );
  }
}

We have to use Builder here for our icon button because we need access to build context which has access to Scaffold widget’s state.

Now we can reload our app, hit “Save” button and see the snack bar.

Since we now have this document saved to a file, let’s update our _loadDocument method to load saved file if it exists.