Tutorial: Build a Markdown Editor Flutter App With the Flet Python Framework

Build Flutter apps using Python

Henri Ndonko
Better Programming

--

Photo by Kelly Sikkema on Unsplash

Flet is simply a python framework that offers the possibility of building cross-platform applications in the programming language you like. At the time of writing, it has been implemented only for Python, but support for other languages such as JavaScript/TypeScript, Go, and C# (.NET) have been planned as per their roadmap.

Flet is based on Flutter, which means that you build your UI using Flet controls, which are based on Flutter widgets.

Quick Introduction

This is a fun and easy project to make. I like it because you can make it your own — and it can be useful too. The editor discussed in this article will run locally and update in real-time as you type. Awesome, right? Feel free to scroll down to the last sections to see the final output.

I will try my best to stay as beginner-friendly as possible by moving step-by-step.

I will equally make sure to add necessary comments in the various code blocks to better ease your understanding.

Installing Flet — “Hello, World!”

Flet is available as a python package, meaning its installation is very similar to those of other packages. Just make sure you have Python installed, then run this command from your terminal to grab the latest version:

pip install flet

It is recommended to do this in a Virtual Environment.

After the installation is done, create a new file and name it as you wish (ex: my-editor) and run this basic “Hello, World!” code using your favorite IDE to make sure you are good to go.

import flet as ft

def main(page: ft.Page):
page.add(ft.Text(value="Hello, world!"))

ft.app(target=main)
Hello World with Flet — Dark Mode
Hello World — Dark Mode
Hello world with Flet — Light Mode
Hello World — Light Mode

Throughout this article, I will be importing Flet with the alias ft, just like Numpy is commonly aliased np or Pandas pd. Alright! Let’s dive right in.

The execution of that code block opens a native window containing the rendered output. If you wish, you could tell Flet to render that output in your browser just by modifying the last line of code to:

ft.app(target=main, view=ft.WEB_BROWSER)  # a port could also be specified

Application’s base

To begin, let’s give our application a title and add an Appbar.

import flet as ft

def main(page: ft.Page):
page.title = "Markdown Editor" # title of application/page

page.appbar = ft.AppBar(
title=ft.Text("Markdown Editor", color=ft.colors.WHITE), # title of the AppBar, with a white color
center_title=True, # we center the title
bgcolor=ft.colors.BLUE, # a color for the AppBar's background
)
page.add(ft.Text(value="Hello, world!")) # some placeholder content (nothing will be shown without this line)

ft.app(target=main)

You should see something similar to one of the images below:

Hello World with Appbar — Dark Mode
Hello World with Appbar — Dark Mode
Hello World with Appbar — Light Mode
Hello World with Appbar — Light Mode

I said ‘one of the images’ because Flet uses your system’s theme by default. For consistency, let’s use the dark theme. So, let’s explicitly set the theme_mode.

# ...
page.title = "Markdown Editor
page.theme_mode = "dark" # there are only 3 possible values(self-explanatory): "dark", "light" and "system"
# ...

I omit some code lines (using #…) to save some space.

User Interface (UI) Structure

Now, let me explain the structure of our UI before we move on. I personally hate complex UIs, certainly because they are hard for normal users (beginners) to understand. So we will opt for a simple one.

On the Left Hand Side (LHS) of the editor, we will have an area for our markdown input.

And as you may have figured out, we will have a preview area on the editor’s Right Hand Side (RHS).

Let’s start with the LHS.

The editor’s Left Hand Side (LHS)

When we talk of ‘text input’ in app development, the first control/widget we think of is eventually an input control/widget. Flet has one called TextField, which we will use for the job.

We will create a TextField and store it in a variable named text_field as shown below.

text_field = ft.TextField(
value="## Hello from Markdown", # the initial value in the field (a simple Markdown code to test)
multiline=True, # True means: it will be possible to have many lines of text
expand=True, # tells the field to 'expand' (take all the available space)
border_color=ft.colors.TRANSPARENT, # makes the border of the field transparent(invisible), creating an immersive effect
)

Here is the entire code till this point:

import flet as ft

def main(page: ft.Page):
page.title = "Markdown Editor" # the title of our app
page.theme_mode = "dark"

page.appbar = ft.AppBar(
title=ft.Text("Markdown Editor", color=ft.colors.WHITE), # title of the AppBar with a white color
center_title=True, # we center the title
bgcolor=ft.colors.BLUE, # a color for the AppBar's background
)

text_field = ft.TextField(
value="## Hello from Markdown", # the initial value in the field (a simple Markdown code to test)
multiline=True, # True means: it will be possible to have many lines of text
# on_change=md_update,
expand=True, # tells the field to 'expand' (take all the available space)
border_color=ft.colors.TRANSPARENT, # makes the border of the field transparent(invisible), creating an immersive effect
)

page.add(text_field) # replaced the placeholder with our TextField (the LHS content)

ft.app(target=main)
Editor’s Left Hand Side (LHS)

The Editor’s Right Hand Side (RHS)

We made the LHS work, now let’s move to the RHS. Recall it will be a section containing the preview of the inputted markdown. We are going to make use of Flet’s Markdown control. We will create a variable for that, and add it to the page.

def main(page: ft.Page):
# ...

text_field = ft.TextField(...)
md = ft.Markdown(
value=text_field.value, # make its value be equal to the content of our text_field
selectable=True, # to make the rendered markdown selectable
extension_set="gitHubWeb",
on_tap_link=lambda e: page.launch_url(e.data), # what happens when a link is clicked: a browser tab is opened up, with the link's URL
)

page.add(
ft.Row( # we use the row here, so everything fits on a line
controls=[
text_field,
ft.Container( # we use the container here, to take advantage of its content alignment property
ft.Column( # we use the column here, to take advantage of its scroll property
[md],
scroll="hidden", # we make the Markdown scrollable
),
expand=True, # we make it fill up all the available space
alignment=ft.alignment.top_left, # align the column
)
],
vertical_alignment=ft.CrossAxisAlignment.START,
expand=True, # we make it fill up all the available space
) # a row containing our text_field on the LHS and Markdown on the RHS
)

You may have noticed that I added the two sections (LHS and RHS) into a Row. This is because we want the result to be on one line. Without the Row, the markdown will go below the TextField!

Some controls are mostly used inorder to take advantage of some of their properties.

Execute your code.

Don’t you think we should separate the LHS from the RHS? Maybe by simply adding a line between them? In Flet, this is possible using the VerticalDivider control.

Let’s update our Row, inserting this control.

# ...
page.add(
ft.Row( # we use the row here, so everything fits on a line
controls=[
text_field,

ft.VerticalDivider(color=ft.colors.RED), # a red vertical line of seperation

ft.Container( # we use the container here, to take advantage of its content alignment property
ft.Column( # we use the column here, to take advantage of its scroll property
[md],
scroll="hidden", # we make the Markdown scrollable
),
expand=True, # we make it fill up all the available space
alignment=ft.alignment.top_left, # align the column
)
],
vertical_alignment=ft.CrossAxisAlignment.START,
expand=True, # we make it fill up all the available space
) # a row containing our text_field on the LHS and Markdown on the RHS
)

The two areas are now easier to distinct. Great, right?

Editor’s LHS and RHS

But did you notice something? If you didn’t, entering some text in the LHS has no effect on the RHS.

This is because we are not listening to the changes in TextField.

To listen up to changes, we need to have a -callback- function, which will be called whenever there is a change in the TextField.

Below is our simple callback:

def update_preview(e):
"""
Updates the RHS(markdown/preview) when the content of the textfield changes.
:param e: the event that triggered the function
:type e: ControlEvent
"""
md.value = text_field.value
page.update()

But that’s not all we need! We have to tell the text_field we will be listening to its changes. This is possible by setting its on_change property to our callback.

text_field = ft.TextField(
# ...
on_change=update_preview
)

If you got lost along the line, please check this gist for the complete code. Below is a capture of the final result:

Final Output
Final Output

In just about 60 lines of code(counting doc-strings and whitespaces), we built something awesome!

Note that you could eventually package this app as a standalone executable file, or deploy it on the web. That’s the cross-platform nature of Flet.

Do you want to try it? I deployed an online version here.

It’s your turn!

We are at the end of this article, but you could try going a little further with this project by:

  • giving the RHS/preview a top Padding, so it could be better aligned with the content of the TextField;
  • add a button in the AppBar to change the page’s theme_mode(from light to dark and vice-versa);
  • using the FilePicker to save the content of the TextField (maybe as .txt or .md) into the user’s device;
  • using the FilePicker control to import/load the content of selected text files from the user’s device, directly into the TextField;
  • …(your imagination has no limits!)…

You can use this as a solution/reference if you get stuck. Please, let me know if you tried it or if you have any questions. I will be happy to help.

By the way, I started a Youtube channel, please subscribe:

Thanks for reading.

--

--