Every fintech product I have been close to had the same argument in its first week: what do we build the app in? Not the ledger, not the KYC vendor, not the licence. The app. In Egypt, Saudi Arabia and the UAE the app is the product; the branch is a formality and the website is a brochure.
Flutter and Dart have become the default answer for many new wallets, neobanks and investment apps in the region. That is not the same as the right answer. This piece treats the ecosystem as a subject: where Flutter fits, where it bites, how to secure it, what it costs, and how to decide. It is written for founders, CTOs and senior engineers who must defend the choice to a board, a bank partner and a regulator. It is not a tutorial.
Why Flutter matters in fintech
The phone is the branch
In most MENA markets a financial product's entire relationship with its customer lives inside a mobile app: onboarding, KYC, card controls, transfers, bills, savings, disputes. The app is the whole storefront, and the quality bar is set by the best consumer apps on the user's phone, not by other banks.
So you must ship the same experience on Android and iOS at once, because your customers are split: Egypt leans Android, the Gulf leans iOS. And you must iterate for years, because fintech apps are never finished. Flutter's pitch, one codebase, one team, compiled natively for both platforms, maps almost exactly onto those needs.
What Flutter actually is (and is not)
Flutter is a UI toolkit from Google that draws every pixel itself. It does not wrap native widgets the way React Native does; it ships its own rendering engine and compiles your Dart code ahead of time into native ARM code. Dart is statically typed, with sound null safety and, since Dart 3 in 2023, records, patterns and sealed classes.
Drawing every pixel means the app looks identical on both platforms, which makes brand consistency and accessibility audits easier. It also means that when you need Keychain, Keystore, biometrics, NFC or attestation, you cross a platform channel and write a little Kotlin or Swift. Flutter is not a way to avoid native code. It is a way to write much less of it.
Why the timing favours Flutter now
Three things changed since the first wave of Flutter fintech apps. Dart 3 gave the language the modelling tools serious domain code needs. Impeller replaced Skia and removed the shader-compilation jank that embarrassed early apps. And the plugins for secure storage, biometrics, push and crash reporting are now maintained and reasonably boring, which is what you want in a regulated product.
The right question is no longer "can Flutter do banking?" It is "does Flutter fit this team, this roadmap and this regulator?" The rest of this article is about the second question.
Where it is used
I will only name deployments the companies themselves have documented publicly; the real list is longer.
Public, documented deployments
Nubank, the Brazilian neobank and one of the largest digital banks in the world by customer count, chose Flutter for its consumer app and has explained why publicly: one team, one codebase, consistent UX, and the freedom to move engineers between features.
Google Pay is the other reference case. Google wrote publicly about rebuilding the Google Pay app in Flutter, starting with India, and about the savings from maintaining one codebase instead of two. Google also uses Flutter for Google Ads and Google Classroom, which is a longevity signal: a framework that runs Google's own payment app will not be abandoned quietly.
Alibaba's Xianyu marketplace was one of the earliest large-scale showcases, around the time of Flutter 1.0, and an early proof that Flutter screens can live inside an existing native app. Outside finance, BMW built the My BMW app in Flutter and Toyota adopted it for in-vehicle systems: large, regulated organisations, a useful answer when someone says Flutter is for startups only. The Flutter showcase keeps a curated list.
In MENA
Several wallets, neobanks, BNPL providers and brokerage apps in Egypt, Saudi Arabia and the UAE ship Flutter apps today. I am deliberately not naming them: most do not advertise their stack, and the ones that do change their minds. What the hiring market shows is that Flutter postings for fintech roles in Cairo, Riyadh and Dubai are routine now.
The regional pattern is clear: new consumer fintech products default to Flutter; incumbent banks stay native for the core app and use Flutter for satellite apps, new brands or specific journeys embedded via add-to-app.
Strengths
One codebase, one design language, two stores
The hidden cost of two native codebases is not the salaries; it is the drift. Android ships the new transfer-limit check a week before iOS, compliance asks why the apps differ, and the product manager starts writing "parity" into every ticket. Flutter removes that category of bug: every regulated behaviour, limits, consent screens, disclosures, cooling-off periods, exists exactly once.
Pixel control for regulated UI
Because Flutter paints its own UI, you control typography, Arabic shaping, numeral style, disclosure placement and button states on every device. When consumer-protection rules require a disclosure in a specific position before a user confirms a fee, you build it once, as a widget, and it is identical everywhere.
Dart 3 as a modelling language
Dart used to be the weak part of the pitch. Sealed classes with exhaustive switch expressions, records, pattern matching and sound null safety now give you most of what Kotlin or Swift offer for domain modelling. A transfer that can be in one of six states is exactly what sealed classes are for, and the compiler flagging a screen that forgot the needsOtp case is a real defect prevented. The class modifiers page on dart.dev explains the tools.
Tooling, testing and hot reload
Hot reload is the feature everyone mentions and underrates. The expensive part of fintech development is the long multi-step flows: onboarding, a transfer with OTP, a dispute. Changing a widget and seeing it in place with state preserved changes how fast a team iterates on those flows. Add golden tests for visual regressions and an analyser that catches most mistakes before they compile.
Where it sits against the alternatives
| Criterion | Flutter | React Native | Native (Kotlin + Swift) |
|---|---|---|---|
| UI consistency across platforms | Identical by construction | Close | Two implementations |
| Language and type safety | Dart 3, sealed types | TypeScript, gradual typing | Excellent |
| Access to OS security APIs | Channels or plugins | Native modules | Direct |
| Rendering model | Own engine (Impeller) | Native views via JSI | Platform toolkits |
| Over-the-air code updates | Not built in | Possible for JS | Not applicable |
| Talent pool in MENA | Deep junior, thin senior | Overlaps with web talent | Scarce and expensive |
| Custom, brand-heavy UI | Very strong | Good | Strong, twice the cost |
| OS-deep features (NFC, watch, widgets) | Workable with native code | Workable with native code | Best |
Risks and pitfalls
Over-trusting the "write once" promise
Flutter shares UI and business logic. It does not share the parts of a banking app that touch the operating system, which are exactly the parts a bank's security team asks about: Keystore-bound keys, attestation, secure window flags, SMS Retriever, NFC. Each is a small native module. A team with nobody comfortable in Kotlin and Swift will stall on them, or install the first pub.dev package that claims to do it and never read its source.
Package quality and supply chain
pub.dev is a public registry, and fintech apps have a supply-chain problem that games do not. Pin versions. Read the native side of any plugin that touches security. Prefer packages maintained by the Flutter team or a known vendor, and let no package in without checking which permissions its Android manifest merge adds; too many banking apps carry a READ_SMS-style permission through a transitive dependency.
The native gap on your team
The most common failure I see is not technical. It is a Flutter team of five with zero native depth, so every platform-specific issue, a Keystore exception on one Samsung model, a Play Integrity error code, becomes a multi-day investigation instead of an afternoon. Hire or grow at least one person per team who reads platform code fluently.
State management sprawl
Flutter does not prescribe a state management approach, and the community has produced many: Provider, Riverpod, Bloc, GetX and more. The choice matters less than consistency. Pick one, write down how a feature is structured, and enforce it in review. An app with three state styles across onboarding, payments and cards is one where a transfer bug takes a week to find.
Architecture and integration patterns
Layering a banking app
The structure that has worked best in my experience is boring and strict: presentation (widgets and view state), application (use cases, state machines, validation), domain (entities, money types, rules) and infrastructure (HTTP, storage, channels). Two rules keep it honest: money is never a double, use a Money type backed by integer minor units and a currency code; and widgets never call infrastructure directly, they dispatch events to a state holder that calls a use case.
Modelling money movement as a state machine
A transfer is not a form with a loading spinner. It is a state machine with legal and illegal transitions, and the illegal ones cost money: submitting twice, confirming without OTP, retrying a non-retryable failure.
/// Every state a transfer can be in. Exhaustive switches keep the UI honest.
sealed class TransferState { const TransferState(); }
class Idle extends TransferState { const Idle(); }
class Validating extends TransferState { const Validating(); }
class Submitting extends TransferState {
const Submitting(this.key); final String key; // idempotency key
}
class NeedsOtp extends TransferState {
const NeedsOtp(this.key, this.attemptsLeft); final String key; final int attemptsLeft;
}
class Succeeded extends TransferState { const Succeeded(this.reference); final String reference; }
class Failed extends TransferState {
const Failed(this.reason, {this.retryable = false}); final String reason; final bool retryable;
}
sealed class TransferEvent { const TransferEvent(); }
class Submit extends TransferEvent { const Submit(); }
class Validated extends TransferEvent { const Validated(this.key); final String key; }
class OtpRequired extends TransferEvent { const OtpRequired(this.attemptsLeft); final int attemptsLeft; }
class OtpEntered extends TransferEvent { const OtpEntered(); }
class Confirmed extends TransferEvent { const Confirmed(this.reference); final String reference; }
class Rejected extends TransferEvent {
const Rejected(this.reason, {this.retryable = false}); final String reason; final bool retryable;
}
class Reset extends TransferEvent { const Reset(); }
/// Pure: the same state and event always give the same next state.
/// Anything not listed is illegal and throws, so the bug is loud in tests
/// instead of double-charging a customer in production.
TransferState reduce(TransferState s, TransferEvent e) => switch ((s, e)) {
(Idle(), Submit()) => const Validating(),
(Validating(), Validated(:final key)) => Submitting(key),
(Submitting(:final key), OtpRequired(:final attemptsLeft)) => NeedsOtp(key, attemptsLeft),
(NeedsOtp(:final key), OtpEntered()) => Submitting(key),
(Submitting(), Confirmed(:final reference)) => Succeeded(reference),
(Validating() || Submitting() || NeedsOtp(), Rejected(:final reason, :final retryable)) =>
Failed(reason, retryable: retryable),
(Failed(retryable: true), Submit()) => const Validating(),
(Succeeded() || Failed(), Reset()) => const Idle(),
_ => throw StateError('Illegal transition: ${s.runtimeType} on ${e.runtimeType}'),
};
Tested with Flutter 3.x / Dart 3.
Notice what the reducer refuses: Submit while Submitting is illegal, so a double tap cannot create a second request; Confirmed while Failed is illegal. The idempotency key is generated once at validation and carried through OTP, so a retry after a timeout hits the same server-side record.
Talking to the backend: idempotency, retries and clocks
Mobile networks fail in elevators, metro tunnels and on roaming. Every write that moves money carries an idempotency key; the client retries only on network errors and 5xx responses, with capped backoff, and never on a 4xx. The client never trusts its own clock. dio is the usual HTTP layer because its interceptors are where token refresh, pinning, request signing and PII redaction live.
Platform channels, Pigeon and FFI
Platform channels are how Dart calls Kotlin and Swift. The raw MethodChannel API is stringly typed and easy to get wrong across three languages; Pigeon generates type-safe bindings from a Dart interface, which is what you want for the security-sensitive bridges. For C libraries such as a vendor's cryptographic SDK, dart:ffi calls them directly, ideally from a background isolate.
Add-to-app and kill switches
An existing native banking app does not have to be rewritten. Flutter's add-to-app mode embeds a Flutter engine inside a native app, so a new journey ships as Flutter screens while the rest stays native. The trade-offs are engine start-up on first entry and two navigation systems; but it is how most large banks that adopt Flutter start.
Whatever the shape, every money-moving feature needs a server-side kill switch. Flutter has no over-the-air Dart updates, so flexibility comes from remote configuration: limits, enabled features, forced-upgrade versions. Fetch it at startup, cache it, and fail closed for anything that moves money.
Security and compliance
The device is hostile; every control below exists because of it, and the controls are layered because each one fails in a known way.
Secure storage: a wrapper you can audit
Session tokens, refresh tokens and device keys go into the platform's secure storage: the iOS Keychain and Android's Keystore-backed encrypted preferences. The flutter_secure_storage package wraps both. Wrap it once more yourself so the rest of the app never sees the package and logout is one call.
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
/// Tokens never go into SharedPreferences: that is plain XML or a plist on
/// disk, readable on a rooted device and sometimes included in backups.
class TokenStore {
TokenStore({FlutterSecureStorage? storage})
: _storage = storage ??
const FlutterSecureStorage(
iOptions: IOSOptions(
accessibility: KeychainAccessibility.first_unlock_this_device,
),
);
final FlutterSecureStorage _storage;
final Map<String, String?> _cache = {};
static const _access = 'access_token';
static const _refresh = 'refresh_token';
Future<String?> accessToken() => _read(_access);
Future<String?> refreshToken() => _read(_refresh);
Future<void> save({required String access, required String refresh}) async {
await Future.wait([
_storage.write(key: _access, value: access),
_storage.write(key: _refresh, value: refresh),
]);
_cache[_access] = access;
_cache[_refresh] = refresh;
}
/// Call on logout, on a 401 that refresh cannot recover from, and when
/// the user removes device biometrics. Wipes memory first, then disk.
Future<void> clear() async {
_cache.clear();
await Future.wait([
_storage.delete(key: _access),
_storage.delete(key: _refresh),
]);
}
Future<String?> _read(String key) async {
if (_cache.containsKey(key)) return _cache[key];
final value = await _storage.read(key: key);
_cache[key] = value;
return value;
}
}
Tested with Flutter 3.x / Dart 3.
The in-memory cache avoids a Keychain round trip on every request, and clear() is explicit because a token that survives logout is worse than a leaked one. Storage protects data at rest, so keep sessions short and refresh tokens rotating.
Biometrics as a gate, not a lock
The local_auth package shows the system biometric prompt and tells you whether the user passed. That is a gate against a casual person holding your phone. It is not cryptographic proof, and a rooted, instrumented device can make authenticate() return true. For step-up on a high-value transfer, bind the operation to a Keystore or Secure Enclave key that requires user authentication and have the server verify a signature; that is native code behind a channel.
import 'package:flutter/services.dart';
import 'package:local_auth/local_auth.dart';
/// What the UI needs to know. Nothing else leaks out of the gate.
sealed class BiometricResult {
const BiometricResult();
}
class BiometricPassed extends BiometricResult { const BiometricPassed(); }
class BiometricUnavailable extends BiometricResult { const BiometricUnavailable(); }
class BiometricNotEnrolled extends BiometricResult { const BiometricNotEnrolled(); }
class BiometricCancelled extends BiometricResult { const BiometricCancelled(); }
class BiometricLockedOut extends BiometricResult { const BiometricLockedOut(); }
class BiometricError extends BiometricResult {
const BiometricError(this.code);
final String code;
}
Future<BiometricResult> biometricGate({
LocalAuthentication? auth,
String reason = 'Confirm it is you to continue',
}) async {
final local = auth ?? LocalAuthentication();
try {
if (!await local.isDeviceSupported()) return const BiometricUnavailable();
final enrolled = await local.getAvailableBiometrics();
if (enrolled.isEmpty) return const BiometricNotEnrolled();
final ok = await local.authenticate(
localizedReason: reason,
options: const AuthenticationOptions(
biometricOnly: true, // no PIN or pattern fallback when money moves
stickyAuth: true, // survive a brief trip to the background
useErrorDialogs: false, // we render our own dialogs, Arabic included
),
);
return ok ? const BiometricPassed() : const BiometricCancelled();
} on PlatformException catch (e) {
return switch (e.code) {
'NotAvailable' => const BiometricUnavailable(),
'NotEnrolled' => const BiometricNotEnrolled(),
'LockedOut' || 'PermanentlyLockedOut' => const BiometricLockedOut(),
_ => BiometricError(e.code),
};
}
}
Tested with Flutter 3.x / Dart 3.
The sealed result forces every screen to handle "not enrolled" and "locked out" explicitly: a user who removed their fingerprint should land on a PIN screen, not an error toast. A fresh enrolment should also invalidate the biometric-bound key natively; many regulators treat a new fingerprint as a new device.
Root and jailbreak detection, and its limits
Bank security teams and regulators expect root and jailbreak detection, and it does stop the honest majority of risky devices. Rooting tools are built to hide from it, though, and an attacker with Frida or a Magisk module will get past any pure-Dart check. Treat it as a risk signal for your fraud engine, not a wall: check in native code, more than once, and send the result to the server.
Certificate pinning trade-offs
Pinning the server's public key in the app blocks most man-in-the-middle setups. But rotate the certificate without updating the pin and every installed app dies the same morning. Pin the SPKI hash, not the leaf certificate; ship a backup pin; keep a remote-config way to relax pinning in an emergency. Pinning protects honest users on hostile networks; attestation protects you from hostile devices. You want both.
App attestation
Play Integrity on Android and App Attest on iOS let your server ask the platform whether a request came from your genuine app on a device that passes integrity checks. The app requests a token with a server-supplied nonce and the server verifies it with Google or Apple; skip the nonce or the server-side check and it is worthless. One regional caveat: devices without Google services (some Huawei models remain popular in Egypt and the Gulf) cannot produce Play Integrity verdicts, so decide what you do for them.
Screenshots, overlays and tapjacking
On Android, FLAG_SECURE on the window blocks screenshots and recording of balance and card screens. iOS cannot block screenshots; you can only detect them and blur the app in the switcher. Overlay attacks, a malicious app drawing over yours to capture a PIN, are the bigger Android threat: set filterTouchesWhenObscured on sensitive views, detect unknown accessibility services, and refuse confirmations from an obscured window.
OTP auto-read done properly
Auto-reading SMS codes is a conversion feature, and the wrong way gets your app rejected from the Play Store. On Android use the SMS Retriever API (the message carries an app hash and the OS hands you only that message, with no SMS permission) or SMS User Consent. On iOS, mark the field as a one-time-code input and the keyboard offers the code. Never request the broad SMS read permission.
Obfuscation, logging and PII
Release builds are AOT-compiled Dart, already far harder to read than a JavaScript bundle. Add --obfuscate --split-debug-info to rename symbols and keep the mapping files for crash reports. Obfuscation slows a reverse engineer down rather than stopping one; there are no secrets in the app. The fastest way to fail an audit is a log line: build a redaction layer early so no tokens, card numbers, IBANs, national IDs or phone numbers ever reach print, your logger or your crash reporter, and scrub breadcrumbs in the reporter's pre-send hook. Remember that assert is stripped in release, and decide which analytics events may carry amounts (usually none).
PCI DSS and the card data problem
For PCI DSS, keep card data out of your code. Use the payment provider's SDK or hosted fields: the card number is entered into a component the provider controls and tokenised on their side, so your app only ever sees a token and most of the PCI scope stays with the provider. If you show a full PAN, fetch it at the moment of display, over an attested session, and never cache it. SoftPOS and tap-to-phone bring their own certification regime and live in native SDKs.
Regulators: SAMA, CBUAE, CBE, PSD2 and SCA
The Saudi Central Bank, the Central Bank of the UAE and the Central Bank of Egypt each publish cybersecurity and consumer-protection frameworks for the institutions they licence. The specifics differ, but the mobile expectations rhyme: multi-factor authentication, device binding, secure storage, root detection, session controls, limits and alerts, and audit logging. PSD2's Strong Customer Authentication adds dynamic linking: the authentication for a payment is bound to the amount and payee, and the server checks them. None of it is Flutter-specific.
Arabic, RTL and the compliance of UI
A user who misreads a confirmation screen is a dispute, so Arabic support is a security topic. Flutter handles bidirectional text and Arabic shaping in the engine, and Directionality flips layouts for RTL locales provided you used directional properties (EdgeInsetsDirectional, start and end rather than left and right). Decide on numerals deliberately: most regional financial apps show Western digits even in Arabic UI; make it a locale setting. A mixed-direction string such as an IBAN inside an Arabic sentence is where layout bugs hide; test every amount, IBAN and date in both languages.
What each control actually stops
| Control | Stops | Does not stop |
|---|---|---|
| Secure storage (Keychain / Keystore) | Reading tokens from a backup or file browser | Malware inside a compromised app process |
Biometric gate (local_auth) |
Casual access on an unlocked phone | Instrumented bypass on a rooted device |
| Root / jailbreak detection | The honest majority of risky devices | Hidden root, Frida, ROMs built to hide |
| Certificate pinning | Interception on hostile networks | Attacks from a hostile device |
| App attestation | Modified apps, emulators, many bots | Devices without Google services |
| FLAG_SECURE / overlay checks | Screenshots and most overlays on Android | Screenshots on iOS |
Performance and scaling
Impeller and the end of shader jank
Flutter's old renderer compiled shaders at runtime, which caused the first-animation stutter that gave Flutter a reputation for jank. Impeller precompiles shaders at build time; it became the default on iOS in 2023 and on Android in late 2024, with a fallback on devices that lack the required graphics API. Still, test on a low-end Android device from the local market, not the flagship in your pocket.
The 16-millisecond budget
At 60 Hz each frame gets about 16 milliseconds; at 120 Hz, about 8. Everything on the UI isolate has to fit. The usual offenders in banking apps: parsing a large JSON statement on the UI thread, rebuilding the whole screen because state lives too high in the tree, non-const widgets in long lists, and shadows and blurs on every card. Profile the three slowest screens in DevTools before every release.
Isolates for the heavy lifting
Dart runs one thread of execution per isolate, and the UI has its own. Anything heavy, decoding a year of transactions, generating a PDF statement, encrypting a payload, belongs in a separate isolate; Isolate.run makes that a one-liner. Isolates do not share memory, so a worker cannot corrupt UI state.
Startup time and app size
Cold start is the first thing a bank partner's review will measure. Keep main() lean: initialise crash reporting and remote config, show the first frame, and defer everything else. On size, a Flutter app carries a few megabytes of engine that a native app does not; after that the growth comes from assets and fonts. Split per ABI, subset your Arabic fonts, and run the size analysis on every release build.
Lists, statements and virtualisation
Transaction histories are the longest lists in the app. Use ListView.builder or slivers so only visible rows are built, paginate from the server with a cursor, and never set shrinkWrap: true on a long list. Format dates and amounts once per item in the view model, not in build.
Offline-first without offline money
A fintech app should open instantly without a network and show the last known balance with a clear "as of" timestamp. It should never let the user move money offline. Cache locally (a SQLite layer such as drift), read the cache first, refresh in the background, and let the server be the only source of truth for balances. Anything that debits an account waits for a live connection and a live attestation.
Honest words about web and desktop
Flutter web renders into a canvas with a large initial download, limited SEO, and accessibility behaviour that differs from a DOM app. For a public-facing site, use React or similar. For an internal console behind login, it lets the mobile team reuse domain code, which can be a real win. Desktop is stable and works well for branch and teller tools.
Team and hiring in MENA
The talent map
Flutter has become the most common mobile skill among early-career engineers in Egypt and much of the Gulf. That gives you a deep pool at the junior and mid level. The senior pool, people who have operated a Flutter app with real security requirements through several store releases, is thinner, and the people in it know their value. Senior native engineers are rarer still and often concentrated in large banks and telcos. React Native talent overlaps with web talent: easy to find, rarely deep on mobile-specific problems.
A workable fintech mobile team is small: a handful of Flutter engineers, at least one of whom reads native code fluently, a QA engineer who owns the device lab, and a shared security engineer who reviews every platform channel and every new plugin. Pair them tightly with the backend team that owns the ledger, because most "app bugs" in money movement are contract bugs between the two.
Upskilling from Android, iOS and React Native
The best Flutter fintech engineers I have worked with came from native backgrounds. An Android engineer who knows Kotlin learns Dart in days, and Compose or SwiftUI has already taught them the declarative model. They bring what a pure Flutter team lacks: they can read a Keystore stack trace and they do not fear the platform channel. React Native engineers bring the declarative mindset and often strong product sense, and need to unlearn dynamic-typing reflexes and the expectation of over-the-air bundle updates.
Interview signals that matter
For a Flutter role in fintech I care less about trivia and more about judgement. Can the candidate explain what a rebuild is and where expensive work should not live? Can they model a transfer as a state machine without being prompted? Can they describe what secure storage protects against, and what it does not? Have they built an Arabic interface and hit the mixed-direction bugs? And the one that separates seniors: can they open the Android or iOS project and find their way around?
Hire people who have been paged for a production app. The framework is learnable in a month; the instinct that a double-submitted transfer is a career-ending bug is not.
Decision framework
When Flutter is the right call
Flutter is the right default when you are building a new consumer or SME financial app for both platforms with a team of modest size, when brand and UI consistency matter, when you ship in Arabic and English across several countries, and when you expect to iterate for years. It also suits satellite apps around an incumbent bank.
When native wins
Go native when the product is the platform: tap-to-pay and SoftPOS, deep wallet integrations, watch apps, home-screen widgets, CarPlay or Android Auto, or anything that leans on new OS features in the month they ship. Stay native when you have two strong native teams and a stable app. And consider native when a key bank partner's security assessment is built around native tooling.
When React Native wins
Choose React Native when the organisation is a React shop with a large web product, the mobile app shares substantial business logic with it, and the team's seniority is in TypeScript. Be cautious about over-the-air updates: attractive to product teams, unattractive to regulators and bank partners, and constrained by store policy.
Decision matrix
Weight each criterion for your situation from 1 to 5; the scores are an illustrative guide, not a measurement.
| Criterion | Weight (yours) | Flutter | React Native | Native |
|---|---|---|---|---|
| Time to first release | 5 | 4 | 2 | |
| Parity of regulated behaviour | 5 | 4 | 2 | |
| OS-level security features | 3 | 3 | 5 | |
| Brand-heavy UI and Arabic RTL | 5 | 3 | 4 | |
| Reuse of a React web codebase | 1 | 5 | 1 | |
| Hiring in Cairo, Riyadh, Dubai | 4 | 4 | 2 | |
| Performance on low-end Android | 4 | 3 | 5 | |
| Comfort of bank security assessors | 4 | 3 | 5 | |
| Deep platform features | 2 | 2 | 5 | |
| Back-office reuse | 4 | 3 | 1 |
Multiply, sum, then ignore the arithmetic if it disagrees with the engineers you can actually hire this quarter; the team you have is the most important column, and it is not in the table.
FAQ
Is Flutter secure enough for a banking app?
Yes, with the caveat that applies to every framework: the framework is not the security. Flutter compiles to native code, stores nothing by itself, and gives you clean access to Keychain, Keystore, biometrics and attestation through plugins and channels. The security comes from the controls you implement and the native code behind them, not from the toolkit.
Will a Flutter app pass PCI DSS and central bank audits?
A Flutter app passes or fails on the same criteria as a native one. For PCI DSS, keep card data out of your code with the provider's SDK or hosted fields so your scope stays small. For central bank and bank-partner assessments, expect questions about secure storage, multi-factor authentication, root detection, pinning, obfuscation and logging; all have standard answers in Flutter.
Should we rewrite our existing native banking app in Flutter?
Usually not all at once. If the native app is stable and the teams are strong, a rewrite costs a lot and the benefit is mostly future velocity. The common path is add-to-app: ship new journeys or a new brand in Flutter inside the existing app and expand from there. A full rewrite makes sense when the native apps have diverged badly or when a new product gives you a reason to start clean.
How well does Flutter handle Arabic and RTL layouts?
Very well, provided the team uses directional layout properties and tests both locales from the first sprint. The engine handles shaping and bidirectional text; the mistakes are in application code that hard-codes left and right, in numerals that switch style between screens, and in mixed-direction strings such as IBANs inside Arabic sentences. Golden tests in both locales catch most regressions.
Can Flutter handle real-time trading screens and charts?
Yes. Custom-drawn rendering suits charts and tickers, and a well-built screen updates many times per second without jank, especially on Impeller. The discipline is in the data path: aggregate market data in a background isolate, push only the changes the screen needs, and keep heavy repaint areas inside RepaintBoundary widgets.
Key takeaways
- In MENA the app is the bank, and Flutter's single codebase removes the parity bugs that regulated behaviour cannot afford.
- Flutter reduces native code; it does not remove it. Every team needs one engineer who reads Kotlin and Swift fluently.
- Dart 3's sealed classes and exhaustive switches are the right tool for money movement; put every transfer behind a state machine that forbids illegal transitions.
- Tokens live in Keychain and Keystore through an auditable wrapper, never in SharedPreferences, and logout clears disk and memory.
- Biometrics, root detection, pinning and obfuscation are layers with known limits; attestation and server-side verification carry the real weight.
- Keep card data out of the app with provider SDKs and hosted fields; keep PII out of logs with a redaction layer.
- Impeller, isolates, virtualised lists and a lean startup path make Flutter fast enough for banking on low-end Android, if you test on low-end Android.
- Hire native-literate Flutter engineers, pick one state management approach, and decide with a matrix weighted by the team you can actually hire.