Flutter Tutorial

Flutter: Medium-like Text Editor

Building a simplified version of the text editor used in the Medium mobile app

Jan Brunckhorst
Level Up Coding
Published in
6 min readSep 4, 2020

--

In the following tutorial, we‘re going to build a simplified version of the text editor used in the ‘New Story’ section of the Medium mobile app. Afterward, your app should look like this:

The final result in action

A short analysis of the ‘New Story’ Section in the Medium App

If you navigate to the ‘New story’ section in the Medium app, you’ll notice that the keyboard toolbar is apparent at the bottom of the screen even if the keyboard is not visible. When the keyboard appears, the toolbar moves up to display itself above the keyboard. If you create two text blocks, you’ll see that trying to select text from multiple blocks will fail. You can only do so for one text block at a time. Therefore, we can see that Medium probably uses a ListView of TextFields with different stylings.

Before Getting Started

First, lets’s create a new Flutter project

flutter create medium_text_editor

Now, we add the following packages to the pubspec.yaml inside the dependencies:

provider: ^4.1.3
community_material_icon: ^5.3.45
keyboard_visibility: ^0.5.6

and install the packages with

flutter packages get

Part 1: Text Formatting using a Keyboard Toolbar

Keep it simple and focus on what matters.
Don’t let yourself be overwhelmed.
- Confucius -

In the first part, we will keep the implementation as simple as possible. We will focus on displaying a keyboard toolbar that changes the text formatting of a text field on button clicks.

Text Formatting

We will only implement headings, plain text, bullet points, and quotes as text formatting options. Let’s specify the text style and padding for each option.

text_field.dart

Note: We used the Unicode character \u2002 as a prefix for the bullet points.

Customizable Keyboard Toolbar

Next, we will build the keyboard toolbar that we will display above the keyboard. The Toolbar is just a Row of IconButtons that set the selected formatting type on a click.

Note: We won’t use the keyboard_actions package as it is not very flexible and leads to lots of weird implementations.

toolbar.dart

Connecting the Keyboard Toolbar and Text Formatting

For now, state management is quite easy and only requires to update the selected text type. Since it will become more complicated in the second part, we’ll use the Provider framework for simplicity.

Note: The EditorProvider notifier will become more useful later on.

state_management.dart

Now that we defined our ChangeNotifier , we need to initialize our state using theChangeNotifierProvider.

To display the toolbar above the keyboard, we simply use Stack and position our SmartTextField with bottom: 56 and our Toolbar with bottom: 0. The resizeToAvoidBottomInset property of Scaffold will automatically adjust the layout if a keyboard is visible so that the toolbar is displayed above.

text_editor.dart

Style Matters

Since it’s always more pleasant to code if things don’t look like shit, we modify the theme in the main.dart.

main.dart

Ta-da🎉

Part 2: Going from Single-Line to a Full-Text Editor

Now that we implemented the single TextField case, it is time to build a fully functional text editor.

Enriching the State Management

As mentioned before, we need a ListView of TextFields . Therefore, we will store three lists in the EditorProvider :

  • List<SmartTextType> contains the formatting style for each block
  • List<TextEditingController> contains the controllers for each block that we can use to listen to user input
  • List<FocusNode> contains a FocusNode for each block that we will use to manage the focus of the TextFields

For simplicity and readability, we create the following five getters that we will use at multiple parts in the code:

  • length: returns the number of text blocks
  • focus: returns the index of the block that has the current focus
  • nodeAt: returns the FocusNode at a certain index
  • textAt: returns the TextEditingController at a certain index
  • typeAt: returns the SmartTextType at a certain index

Additionally, we will add a setFocus() method to update the keyboard toolbar when the user changes the focus to another text block and an insert() method to add a new text block.

Ok, let’s update our EditorProvider using the following implementation:

state_management.dart

Now that we changed the EditorProvider to store a list of text blocks, we need to update our TextEditor to display a list of SmartTextFields . To do so, go to the implementation of the TextEditor , and at line 31 replace the Consumer with the following code.

Consumer<EditorProvider>(
builder: (context, state, _) {
return ListView.builder(
itemCount: state.length,
itemBuilder: (context, index) {
return Focus(
onFocusChange: (hasFocus) {
if (hasFocus) state.setFocus(state.typeAt(index));
},
child: SmartTextField(
type: state.typeAt(index),
controller: state.textAt(index),
focusNode: state.nodeAt(index),
)
);
}
);
}
)

Note: We used the Focus widget to easily listen to focus changes and update the selectedType of the EditorProvider

Creating & Deleting Text Blocks

Okay, now that we updated our state management and text editor to store and display a list of text blocks, we need to implement the logic of creating and deleting blocks. If the user presses enter, we need to create a new TextField and switch the focus to it. If the user presses the backspace key to remove a text block, we need to erase the text or merge it with the text block above.

We will first go over the logic and code snippets on how to handle the two events. Afterward, I’ll show you the full code you need to copy into the insert method of the EditorProvider . So bear with me!

Handling the Remove Event

To detect the remove event, we’ll use a small trick. We use the zero-width space Unicode character \u200B as a reference for the start of a new line via

final TextEditingController controller = TextEditingController(
text: '\u200B' + (text?? '')
);

If the user then presses the backspace key and removes the starting character, i.e. \u200B , we detect a remove event, delete the focused text block, and move the focus to the text block above.

if (!controller.text.startsWith('\u200B')) {
final int index = _text.indexOf(controller);
if (index > 0) {
textAt(index-1).text += controller.text;
nodeAt(index-1).requestFocus();
_text.removeAt(index);
_nodes.removeAt(index);
_types.removeAt(index);
}
}

Note: We concatenate the text of the two text blocks via += controller.text so that the user can easily merge two separate blocks into one.

Handling the Enter Event

Next, we need to detect when the user pressed the Enter key. Since our TextFields use the multiline keyboard type (i.e. keyboardType: TextInputType.multiline ), we can check if the TextEditingController contains the Unicode character \n which represents a line break. If it does, we gonna split the text and move the part after the \n character to the next text block that we create on-the-go. This approach is very important, as it allows the user to split an existing text block into two parts. Here you can see the implementation of the logic described above.

if(controller.text.contains('\n')) {
final int index = _text.indexOf(controller);
List<String> _split = controller.text.split('\n');
controller.text = _split.first;
insert(
index: index+1,
text: _split.last
);
nodeAt(index+1).requestFocus();
}

When pressing enter on a bullet point, it is very intuitive that another one appears below. So, we modify the call of the insert function a bit, as you can see in the following snippet.

insert(
index: index+1,
text: _split.last,
type: typeAt(index) == SmartTextType.BULLET
? SmartTextType.BULLET
: SmartTextType.T
);

The Implementation

Combining the above steps leads us to the following implementation of the TextEditingController and Listener that we can paste into the beginning of the insert function of the EditorProvider.

addListener

Hiding the Keyboard Toolbar

While we only implemented a simplified version of the text editor from the Medium mobile app, there is one thing that we can easily improve. In the medium app, the toolbar is visible at the bottom even if the keyboard is not. This is certainly not a problem, but from a design point of view, not very appealing. But good news, it is easy to fix using the keyboard_visibility package.

First, import the keyboard_visibility package to text_editor.dart via

import 'package:keyboard_visibility/keyboard_visibility.dart';

and then add the following to _TextEditorState:

bool showToolbar = false;@override
void initState() {
super.initState();
KeyboardVisibilityNotification().addNewListener(
onChange: (isVisible) {
setState(() {
showToolbar = isVisible;
});
},
);
}
@override
void dispose() {
KeyboardVisibilityNotification().dispose();
super.dispose();
}

Then, also in _TextEditorState , wrap the second Positioned widget with the following if-statement:

if (showToolbar) Positioned(
bottom: 0,
...

And, that’s it!

Full Code

You can find the final code here. Or check out Peter Aleksander Bizjak repository who converted my idea into a usable flutter library.

--

--