Whiteboard is an irreplaceable tool in many business processes. To allow for the same kind of collaborative process using whiteboard online, live whiteboards have been created. In this blog post, we will explore a possible implementation of such a whiteboard using Flutter to create a mobile application and Firestore database as backend.
Flutter is a framework for creating cross-platform applications for mobile devices (Android and iOS), desktop (Mac and PC), and web.
Firestore is a document database offered by Google as a part of the Firebase platform. Features that make it especially well suited for use cases such as live whiteboard are low latency synchronization and the ability for clients to subscribe to notifications about data changes.
In this blog post, we will analyze the most important aspects of creating a simple whiteboard app. The app consists of two pages, the first StartPage for providing id of the whiteboard the user intends to connect to. Second WhiteboardPage displays the selected whiteboard and allows for adding and removing lines.
Widget responsible for rendering the lines to the screen is WhiteboardView. WhiteboardView consists of a CustomPaint widget with WhiteboardPainter. WhiteboardPainter in paint method iterates over all lines and draws lines joining consecutive points to the canvas.
WhiteboardView is also responsible for detecting user inputs and exposing appropriate onGestureStart, onGestureUpdate, and onGestureEnd callbacks. Gestures are detected using a GestureDetector widget and mapped to Point models after being scaled relative to whiteboard width. Using coordinated relative to screen width resolves issues related to different screen dimensions across devices. WhiteboardView has also fixed aspect ratio to 9/16 for the same reason.
The app uses a very simple data structure in Firestore. Every whiteboard is saved in a single document consisting of content id and a list of lines. Such a structure is a result of the Firestore pricing model which is based mostly on a number of documents being read and written. Not separating whiteboard into multiple documents has some drawbacks most significant of which in this case is the limit to 1 write per second to a document.
New lines are saved to the database when the drawing is finished to reduce the number of writes and Firestore costs. The new content id is also added to the list of written ids to allow for comparison explained earlier.
Adding a new line if fairly straight forward. New line is created in the onGestureStart method and stored in the _currentLine field. New points are added in the onGestureUpdate method. In onGestureEnd a line is added to the WhiteboardContent model and saved in Firestore.
Deleting lines is a bit more complex. In each onGestureStart or onGestureUpdate invocations rectangle created by current and previous pointer positions is checked for intersection with rectangles created by each two consecutive line points (or with a rectangle of size equal to double the line width centered around the point in case of only one available). If any of the rectangles created by the line section intersect line is removed from WhiteboardContent and changes are saved in the database.
Firestore by default created indexes for each array field in a document to allow for “contains” querying. In the case of large arrays, this leads to a huge number of indexes being created which translates to higher database cost and slower write speeds. In our case “contains” query is completely useless so it’s important to disable the creation of those indexes.