~/muhammad-taha_

Flutter · Android · Based in the UAE

I build small Android apps that behave like large ones.

I'm Muhammad Taha — a Flutter and Android developer based in the UAE. Fifteen of my apps are on Google Play. They're single-purpose utilities that share one toolchain, one services layer and one rule: if a feature isn't tested and translated, it isn't done.

Nine are written up below — each with the engineering decision inside it that turned out to matter more than the feature list. The rest are listed underneath.

15Apps on Google Play
1,300+Automated tests
15Locales, RTL included
1Shared toolchain

Selected work

Projects

Flutter, Android-first, offline by default. Each card notes the version, the test count, and the part that was genuinely hard. Six are live on Google Play; three are finished and waiting on a release.

JWT Decoder

v1.0.0

Decode, inspect and actually verify JSON Web Tokens — entirely on the device, with no token ever leaving the phone.

The hard part: offline signature verification across 13 algorithms (HS, RS, PS and ES families). The crypto is validated against test vectors generated with OpenSSL rather than against itself — which is how the PSS salt-length mismatch that broke six tests was found instead of shipped.
313 tests+35 on-device 10 localesFlutterDart crypto

JSON Formatter

v1.0.0

Format, validate and explore JSON — without silently rewriting the document you pasted in.

The hard part: it does not use json.decode. Dart's built-in parser changes data: 1e5 becomes 100000.0, 1.50 becomes 1.5, huge integers lose digits and duplicate keys vanish. So this ships a hand-written recursive-descent scanner that carries every number as literal text, keeps duplicate keys and reports them, and returns error codes with line and column — because the messages exist in ten languages.
266 tests10 locales Custom parserFlutter

Hash Generator

v1.3.0

Digests for text and for files, with a searchable history you can undo.

The hard part: keeping a pure, dependency-free HashEngine testable while the app around it grew a file picker, an undoable history, a GDPR consent flow and push topics — a 1,034-line main.dart split into 33 files without changing a single digest.
154 tests6 locales UMP consentFCM

Qibla Compass

v1.0.0

Qibla direction, prayer times, manual location and a map — accurate without a network connection.

The hard part: no Flutter compass plugin exposes the two values this app is built on, so it ships its own HeadingPlugin in Kotlin and Swift: Android's GeomagneticField declination (true north with no bundled magnetic model and no network) and the raw magnetometer accuracy bucket, so the app can tell you when to recalibrate instead of quietly pointing the wrong way.
121 testsKotlin SwiftSensorsOffline

Flip Text

v1.0.0

Upside-down, mirrored and scrambled Unicode text in fifteen styles.

The hard part: Unicode, not UI. Three near-duplicate flip tables disagreed on 15 of 94 shared entries; 280 of 1,158 mirror entries were unreachable dead code because astral characters are surrogate pairs; and reversal was tearing emoji in half. Rebuilt as two correct tables plus grapheme-aware reversal.
105 tests15 locales UnicodeGraphemes

TV Size Calculator

v1.1.0

Screen size against viewing distance — metric and imperial, viewing angle, real screen dimensions and a scaled room diagram.

The hard part: the pass that mattered was done on a phone, not in CI. A green test suite still shipped a clipped tab bar, a locale pinned at install time and a counter truncated to one line. Android's font scaling is non-linear above 100%, and no widget test notices.
186 tests15 locales Metric + imperiala11y

Shade Shift

v1.0.0

Spot the one tile whose shade is different, against the clock, with a daily challenge.

The hard part: difficulty is a target CIE76 ΔE per level rather than an RGB step, found by bisection to within ±0.4 ΔE — so level 40 is perceptually harder than level 20 on every panel. The daily challenge needs no server: the date seeds a PRNG that is pinned in-app so it can never drift between releases.
94 testsColour science Deterministic PRNGOffline

Text Repeater

v1.3.0

Repeat text by count or by target length, with custom separators, escape sequences and file export.

The hard part: staying responsive at scale. Anything over 50,000 characters is generated on a background isolate, so the UI never blocks — and the enum is AmountMode, not RepeatMode, because Flutter's material library already exports that name and the ambiguity is silent.
56 tests8 locales IsolatesSAF export

Quick Chat WA

v1.3.0

Open a WhatsApp chat with any number without saving it to your contacts first.

The hard part: privacy and RTL. An earlier release was logging full phone numbers and message lengths to analytics; that came out, and the events now carry outcomes only. Arabic exposed a second class of bug — bidi reordering renders +971 50 123 4567 back-to-front unless the number is isolated.
45 tests8 locales RTL / bidiPrivacy fix

How these get built

One toolchain, fifteen apps

The apps are unrelated, but they are not built differently. A single reference implementation — Income Tax Calculator — carries the services layer, the Gradle config and the release checks; each new app copies it rather than reinventing it.

Same skeleton every time

models/ services/ screens/ widgets/ theme/ utils/ — no app keeps logic in main.dart. Business rules live in pure Dart services that can be tested without a widget tree.

Tests, then a real device

1,300+ unit and widget tests across the nine apps above — and a manual pass on hardware regardless, because clipped layouts, font scaling and time zones stay green in CI right up until a user sees them.

Localised, not translated late

Up to fifteen locales including Arabic and Urdu, so RTL and bidi are design constraints from the first screen instead of a bug report after launch.

Release gates that run

tool/check.sh plus a 16 KB page-size check on every release build, so Android's alignment requirement is verified before upload rather than discovered during review.

Correct beats convenient

Three of these apps replaced a popular package with their own implementation — a JSON parser, a compass plugin, a flip table — because the convenient option was quietly wrong.

Offline by default

Hashing, JWT verification, prayer times and Qibla bearing all resolve on-device. Nothing a user types is sent anywhere to be processed.

Toolchain

Stack

Current shared baseline, kept in lockstep across the family so a fix in one app ports to the rest.

~/apps — toolchain --baseline
# framework
flutter        3.47            # dart 3.13
languages      Dart, Kotlin, Swift

# android
gradle         9.1.0           # agp 9.0.1, kotlin 2.3.20
sdk            compile/target 36, min 24
jvm            17

# platform services
firebase       core, analytics, crashlytics, messaging
local          flutter_local_notifications, shared_prefs
ads            google_mobile_ads + UMP consent

# quality
tests          unit, widget, integration, on-device AOT
lints          flutter_lints 6
checks         tool/check.sh, check_16kb.sh, check_l10n.py

Let's talk

Flutter and Android work — new apps, or rescuing one that has drifted a few toolchain versions behind. Email is the fastest way to reach me.