Todo App
Check out the app at: https://todo-app.ulmschneider.ch
Code: https://gitlab.com/leah_u/todo_app
Introduction
After writing my first small flutter app, I had a few ideas for more ambitious apps. Before tackling a big project however, I wanted to gain some more experience in state management and with building and connecting to a backend. The initial idea was to create a simple todo app with the tasks stored on a remote server.
Creating a prototype
The first step was to decide on a technology stack for the backend. I went with Rust, using Poem-OpenApi to build a REST-Api and Sqlx to connect to a Postgresql database. For deployment I used Docker on a Raspberry-Pi.
Getting everything running for the first time was challenging, both due to my own lack of experience as well as the limited documentation available for Poem-OpenApi. But once I got everything right, the result was almost magical. Poem creates a UI for the API using Swagger automatically, and thanks to the compile time checks of Rust and Sqlx, no unexpected runtime errors showed up once everything compiled successfully. I then created a small prototype app in Flutter to finish the proof of concept.
Expanding the app
While getting a basic app up and running was very simple, I realised that if I wanted to add more features, I would have to do some more sophisticated state management. After a bit of research, I landed on Flutter-Bloc, which conveniently also has a todo app as part of its examples. Once I was finished rewriting my app, I was ready to add more features. My first idea was to add support to categories, which also required expanding the backend.
At this point, the app always required an internet connection to work. On startup, the current todos and categories would be downloaded from the server and each update was instantly pushed. This was not really how I wanted it to work, but changing it seemed daunting.
After fiddling around with various small UI improvements, I finally decided to tackle offline storage and synchronisation. To store data locally, I used the Drift package, which uses a Sqlite database under the hood. The SQL abstraction took some getting used to, but was thankfully well documented. The tricky part however was synchronising local data with the server.
For this, I added an additional field to every object in the local database to track whether any local changes had been made or if the object had been deleted. When a synchronisation is triggered, all data is downloaded from the server and compared with the local state. Objects that only exist on the server get saved locally. Objects that don’t exist on the server anymore and haven’t been updated locally get deleted, while objects marked as deleted are requested to be deleted from the server and then also discarded locally.
The biggest challenge was handling cases where a local change happened, since the object could also have been edited on the server. For todos, the latest edit time can be compared and the newest version used. This is still limited though, as one version could have an updated description while the other could have the pinned status changed. With my simple algorithm, one of the changes will be lost.
After I got offline storage and synchronisation to a state where I was happy with it, I decided to polish the app and make it useable for other people. Up until this point, there was no concept of different users on the backend, so I had to implement authentication and extend the database accordingly. The whole authentication process as it stands is probably not very secure, but since this is a small personal project I didn’t want to spend too much time on it.
Polishing up the app took way longer than expected. I added a settings menu and added support for multiple languages (English and German for now). I also added a bit of responsivity by changing the layout on big screens, though the app is still best on a smartphone.
Conclusion
Creating this app was a lot of fun and I learned a lot. I was also using the app during the development process, both as a general todo list and to keep track of development tasks. This allowed me to quickly find issues and get ideas for useability improvements.
One of the biggest difficulties was that I didn’t have a clear vision at the beginning. This meant that I had to frequently change large amounts of code. It also means that the way synchronisation works right now has some issues. Ideally, I would have started over once I understood the requirements, but instead I decided to try and make the most of the decisions I had made earlier.