## 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](https://flutter.dev/docs/get-started/install). Create a new project using Terminal and `flutter create` command: ```shell $ flutter create myapp $ cd myapp ``` For more methods of creating a project see [official documentation](https://flutter.dev/docs/get-started/test-drive). ### 02. Add Zefyr to your project Add `zefyr` package as a dependency to `pubspec.yaml` of your new project: ```yaml dependencies: zefyr: [latest_version] ``` And run `flutter packages get`. This installs [zefyr](https://pub.dev/packages/zefyr) and all required dependencies, including [notus](https://pub.dev/packages/notus) package which implements Zefyr's document model. > Notus package is platform-agnostic and can be used outside of Flutter apps > (in web or server-side Dart projects). ### 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: ```dart 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 { /// 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 = ZefyrController(document); _focusNode = 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: ```dart 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: ### 04. 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: ```dart // 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 { // 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: ```dart class EditorPageState extends State { // 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: [ 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 `BuildContext` which has access to `Scaffold` widget's state. Now we can reload our app, hit "Save" button and see the snack bar. ### 05. 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. ```dart class EditorPageState extends State { // change: replace _loadDocument() method with following /// Loads the document asynchronously from a file if it exists, otherwise /// returns default document. Future _loadDocument() async { final file = File(Directory.systemTemp.path + "/quick_start.json"); if (await file.exists()) { final contents = await file.readAsString(); return NotusDocument.fromJson(jsonDecode(contents)); } final Delta delta = Delta()..insert("Zefyr Quick Start\n"); return NotusDocument.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 `ZefyrController` 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: ```dart class EditorPageState extends State { // change: replace initState() method with following @override void initState() { super.initState(); _focusNode = FocusNode(); _loadDocument().then((document) { setState(() { _controller = ZefyrController(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: ```dart class EditorPageState extends State { // change: replace build() method with following @override Widget build(BuildContext context) { // If _controller is null we show Material Design loader, otherwise // display Zefyr editor. final Widget body = (_controller == null) ? Center(child: CircularProgressIndicator()) : ZefyrScaffold( child: ZefyrEditor( padding: EdgeInsets.all(16), controller: _controller, focusNode: _focusNode, ), ); return Scaffold( appBar: AppBar( title: Text("Editor page"), actions: [ Builder( builder: (context) => IconButton( icon: Icon(Icons.save), onPressed: () => _saveDocument(context), ), ) ], ), body: body, ); } } ``` 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.