Building Writings with Flutter Web
11 min read
Last spring I needed a very simple app that I could use to write and share content with. I started using some of the existing apps but I found them a bit broader than what I needed. I needed something as simple as a text editor. A bit smarter. So I decided to build one myself. It's called Writings.
For the stack I decided on:
- Flutter as UI framework
- Firebase as Authentication and Database service
- Node.js for BE
- Netlify/Github actions for the operations
I haven't built a complete web app so far, and I didn't know any framework to use. But I knew Flutter. Flutter is a multi-platform framework that supports Web too. So I thought Ok, Writings is a simple enough to start experimenting with it. One thing led to another and I somehow reached the MVP.
With this post I want to share my experience with Flutter as a Web framework building my tool.
Note: this experience is only about Flutter Web and not Flutter as a framework completely. I honestly believe that in the sea of multi-platform mobile frameworks, Flutter is simply superior. For web, it's an interesting case.
The weird things
The development experience working on a web app is a bit different than for a mobile app. For example, on Web there is no long-press context menu as it's on mobile. Or the navigation itself, it works differently. How about the authentication? What if the user tries to access a webpage from the address bar, that should open only if the user is authenticated?
These were all questions I had to answer one by one.
The first thing I neglected was the navigation. The navigation on a web app is nested and the user has the possibility to always go in the history, as many steps as the browser allows. Think of the back button on your browser and how if you long press on it you see a context menu with your history stack. Well this has to be handled properly doing a web app.
For Flutter the navigation API is really complex. I have experience with it only for a mobile app and I didn't want to get blocked trying to make it work for web. So after quite a few attempts to cover all navigational cases, I finally decided to go with a 3rd party library called Fluro. I don't know if it's good or bad. I use it because it simplified my efforts and solved my problem. It's very simple library. It's missing features like route guards or rerouting (if a page is behind an auth wall), but those can get solved independently.
It's not very difficult to defend this argument but: Text input for Flutter is a shady zone and one of the biggest pain points of the framework. Things move to the better direction but at the moment there are some very basic use cases broken because of this.
For example: while you are writing in a multi-line text input field, the text selection is not working as expected. See on the following GIF, I want to select the text between the first and second click.
I hold Shift and click, but the text range is not selected. Something that I definitely use for any web app with text input.
dart:html) internal library, but I didn't manage to test this workaround. Even though by the time of the publishing of the closed beta I will have to. Looks like the topic overall is missing proper documentation.
The (undo/redo) History stack
Looks like as if the Cmd+Z/Cmd+Shift+Z operations are possible only limited amount of times. Not sure how it works. There is no clear configurational point for the undo/redo stack. I can't go deep in history pressing Ctrl+Z. It works for 2-3 steps back and that's pretty much it. Again, I didn't find anything documented about this, but I see some issues that are still open.
To show text with Flutter, you can use two widgets:
SelectableText. They do what they are named for. The first things I noticed using
SelectableText was that the triple click does not select the whole text. You can select one word only, as it seems the widget handles only two clicks (the third one is ignored). Also, I ran onto some issues with how the highlighting of a selected text looks (the blue area that surrounds the selected text). This is how it looks like by default:
Notice the spacing between the highlighted lines. The solution is to set the
selectionHeightStyle parameter to it (
Rich Text Rendering
There should be a separate article for this but the summary is: at the moment there are some cool solutions built by smart people out there, but they are either technically heavy and over-complicated or poor in performance. As this is a huge topic I also left it out from the MVP and will be my main focus until Writings reaches out public Beta.
The clash with the browser
Any web developer uses the browser during development, almost as much as they use VSCode. Well that is not completely possible with Flutter because there is no much point in doing so.
Browser text-search (Ctrl+F)
Then comes the Search of text. Which technically works. But it looks ugly. See this:
For some reason the words are highlighted elsewhere, then when the search is closed the right word is highlighted. 🤷♂️
Low cross control with the browser
If for some reason you need to intercept some of the browser's events (like eg. a user intention, like 'close the tab' or 'refresh the page') and perform some modal action on top, then that might be a problem. In my case, I wanted to intercept the
beforeunload event from the Window and show a modal dialog as confirmation in case the user has unsaved changes. What is
There is a hack on how to listen to these events from Flutter, but you cannot control them. You can execute something when the user closes the tab, but you cannot prevent that. You cannot show a modal dialog and wait upon the results. I tried a lot but didn't succeed doing this.
A Flutter Web app will not recognize out of the box the trackpad gestures. feature (swipe with two fingers to the lef or right on the trackpad). The same goes for the pinch, known as zoom-in gesture. Doesn't work out of the box.
There are some workarounds, but in case you don't really need this, not worth it implementing it yourself. Again, there are some open issues for some of them, but no signs of acceptance.
It's a client app
There is a significant difference between how a mobile and a web app execute their remote requests. On mobile, communication is solely client ↔ server, and there is no additional layer of security on top of it (like the Browser is for Web). At the beginning I got struck with some CORS errors and I managed to avoid them, locally by overriding some of the security features of Chrome. But once I deployed and tested a production version, CORS came back. Hungrier than ever. 😼
At that point I had to learn about the ways of implementing remote calls and what it meant in my case. In a nutshell: Writings has a share as an image on Twitter feature and I was using a third party library to publish tweets on somebody's else behalf. Those requests were rejected as part of a security mechanism called preflight request. Preflight request is a small request done by the browser before the actual one happens, to check on few security points. The solution for me was in building a Node.js Backend to which these requests were delegated. (btw, learning how to write Node.js app was yet another great experience!).
Having an own BE, gave me control over all request headers.
The Web App lifecycle
When testing a web app, If you type a URL in the address bar and press enter, the whole app will restart and your stack will get recreated. Not sure if this is how web apps work or it applies to Flutter only, but I had an 'aha' moment realizing it. Example: If you want to load an article that lives on
www.writings/some-article-url, the moment you load the URL via the address bar, the application object gets created (a new instance of the app), the root route gets triggered and, finally, the requestted page is shown. If you don't develop the nesting properly (with whatever state management library you use), you might execute business logic for pages that you didn't intend to show.
This gave me some headaches. The key here is to change the mindset, from having
Flutter for Content-full apps?
A bit later came the Tweet from Tim Sneath that one should not use Flutter for content heavy applications. I was 🙀🙀🙀.
There were some reactions to this tweet as you can read, but the point is, if Flutter is discouraged as a framework to be used for content rich apps, it'd be very difficult to draw the line between what is a content-rich and what an interactive app. In my case, for what I can say, I had my fair share of problems and my app i not THAT heavy on content. But this is good fact to know from the Flutter team because it very honest and adds speed to the decision process if Flutter should be used as a web app framework and in which cases. Reading that tweet thread might give you an impression of a flamewar happening. Don't be confused by that, it's just a few very strong opinions mixing to each other from some great developers in the community.
But Tim Sneath is correct. Flutter Web should not be used for content heavy apps. It does not have problems only with inputs and representation of data (text in particular), it does also with performance tasks as simple as scrolling. Any user intending to find issues with an app will first report that a very long text put into a
SingleChildScrollView scrolls a bit .. poorly, compared to the native ones. Or a big list of images put into a
ListView. Things can sooth by using some suggestions from the community, but not significantly. Also, I don't think any of this is a blocker. It's an issue for sure, but only if performance is very important for your app. In my case, not a big deal.
Things work (more or less)
But with all seriousness and without being very picky, things with Flutter Web can be brought to production very easily. The promise of Flutter being multi-platform is justified as it's very easy for one to port a Flutter web app, to a mobile app. That makes simple processes like creating responsive layouts and supporting different sizes, screens and devices, really an easy task to do. So it feels that Flutter gives most the basics out of the box, then it's up to the developer to proceed further with the details.
Other things that I really liked, not directly related to Flutter itself, but integrating a Flutter web app with existing deployment and building services is just as easy as any other web app out there. In my case I am using a combination of Netlify and Github Actions for the deployments (and Vercel for the Node.js deployment) and ever since i configured these things the first time, I haven't looked back, they are working.
Would I do it again?
I guess not at this phase. Moreover that I am curious for other technologies too. At the end, users are after solutions and not a technology. If you make the experience pleasant with the bare minimum covered, regardless if it's React, Flutter, Angular or whatever, no user will object. On the other hand, if you prompt a non tech user (say, a content creator or fiction Writer) with a Flutter Web app where they have to do their writing, the first things they'd notice are the simple UX problems I mentioned above. Not being able to do selection of a text and formatting or do the frequently used undo/redo operations can be really frustrating. And if this requires you to use tons of libraries to support the use-case then it's really better to look elsewhere.
But, if the need is to develop a simple prototype as a proof of concept, then for sure Flutter Web is good to go. You have more or less everything, you just have to make it work.