Back to portfolio
Ataa Education Authority · Saudi Arabia

Two Flutter apps where the same screen tells different people different truths.

QMS and OPS ship as separate products on one platform. What a member sees, and what they are allowed to approve, is resolved per membership rather than per screen — so the module map below is not a menu, it is a permission model.

29 feature modules 31 role codes 2 production apps Arabic-first, RTL
The signature problem

Pick a membership. The app rearranges itself.

Every screen asks the backend who is looking before it decides what to render. Approval state is the sharpest case: it is computed as a SQL CASE over the viewer’s role, so one plan legitimately reads “approved” to a director and “awaiting” to the officer who filed it.

can create or approve read only not visible
Two products

One design system, two identities.

OPS reuses the QMS widget library, tokens and typography exactly — then ships under its own package id, its own Firebase project and its own name. Sharing the surface without sharing the identity was a deliberate constraint, and the reason the second app took weeks instead of months.

QMS Ataa

quality management · 18 modules

School visits, standards, indicators, evaluations and corrective plans, over a membership matrix that spans stage officers, domain owners, visit teams and central quality management. Comment threads carry photos, video and voice notes.

visitsstandardsindicatorscorrective plansevaluationsschools

OPS Ataa

operational planning · 11 modules

The full planning chain — strategic goals, sub-goals, work plans, indicators — plus quarterly and annual evaluations with server-derived targets, PDF export, and approval gates that refuse to pass an incomplete plan.

plansgoal chainwork plansquarterly evalPDF exportstatistics
What actually took the time

Four problems that were not about UI.

A response envelope that refuses to hold still

The data field arrives as an object, a list, an integer id, or a plain string carrying a failure message — and sometimes not at all. A request missing a header returns raw text under HTTP 200, and a 401 returns a null body, so the session decision has to come from the status code alone. Every model is parsed defensively rather than through a generic deserializer.

Approval is relative to the viewer

Two users open the same plan and see opposite states. That is the contract, not a defect: the field is derived per requester. Caching it, or trusting a value fetched under a different account, produces bugs that only appear when a second person signs in.

The web client and the app get different payloads

The backend branches on a Set-From header — eight endpoints change shape for the app, one of them renaming its keys outright. Every model had to be built against a real response captured as the app, never as the admin panel.

Arabic type had to ship as real weights

Cairo is bundled at 400, 500, 600 and 700 as four separate files. With Regular alone, Flutter synthesises the heavier weights by thickening strokes, which closes the counters of ص ض ه ع until they render as solid blobs. On an Arabic-first product that is a legibility bug, not a polish item.

Built with

Flutter, held to a clean architecture.

Flutter 3.22DartBLoC / CubitGetIt Dio + RetrofitFreezedDartzFirebase FCM local notificationseasy_localizationSkeletonizerget_storage