A Flutter e-commerce app unifying four incompatible Bangladeshi payment gateways behind one interface, with a local-first cart and connectivity-aware UX built for mid-range devices across the whole country.
eBangladesh's hardest engineering problem wasn't the shopping flow — it was payments. Four Bangladeshi gateways (bKash, Nagad, uPay, aamarPay), four incompatible integration patterns, four different failure vocabularies, and a checkout screen that couldn't be allowed to know or care which one a customer picked. On top of that: a large share of the target userbase is on mid-range Android devices with inconsistent connectivity, so the app also had to stay usable — browsing, cart, checkout warnings — when the network wasn't.
I built the Flutter mobile app at Codeboxr, designing the payment abstraction layer, the offline-tolerant cart, and the connectivity-aware UX that made the app usable across the country, not just in areas with reliable signal.
The Problem
E-commerce penetration in Bangladesh outside of Dhaka required a different approach than traditional marketplaces. Customers in semi-urban and rural areas needed local coordinators they could trust, local payment methods they already used (bKash, Nagad), and an app that worked reliably on mid-range devices with variable connectivity.
eBangladesh's model of having a physical coordinator in every square mile needed a mobile app that made both browsing and buying frictionless for users across the country.
My Role
I built the Flutter mobile app at Codeboxr — responsible for architecture, implementation, and delivery:
Designed a payment gateway abstraction layer — unified bKash, Nagad, uPay, and aamarPay behind a common PaymentGateway interface with a single pay(amount, orderId) contract. Each gateway's WebView redirects, token management, and error normalization are sealed behind that interface; the checkout screen never touches a gateway directly.
Chose Provider over Bloc or Riverpod — the app's state graph is relatively flat (cart, auth, connectivity), and the team context at Codeboxr favored Provider's simplicity. The tradeoff was accepted: less compile-time safety for faster onboarding and fewer boilerplate files across 15+ screens.
Implemented a local-first cart backed by shared_preferences — users can browse, add to cart, and manage wishlists entirely offline. Cart state persists across sessions without requiring authentication, reducing checkout friction for first-time users.
Built connectivity-aware UX using connectivity_plus — on network loss the app degrades gracefully: cached product data remains browsable, cart operations work offline, and the user is warned before attempting payment without a connection.
Optimized for mid-range Android devices (2GB RAM, Android 8+) — profiled on real devices throughout development. Used ListView.builder everywhere, kept widget trees shallow, deferred non-critical initialization with addPostFrameCallback, and standardized image loading via cached_network_image with fixed dimensions and shimmer placeholders.
Technical Decisions
Payment Gateway Abstraction
Four gateways, four integration patterns — bKash and Nagad use WebView redirects, uPay uses a custom SDK, aamarPay provides a direct card form. Each returns different response objects and error codes. Rather than branching on gateway type throughout the checkout flow, I defined a common contract:
abstract class PaymentGateway { Future<PaymentResult> pay(double amount, String orderId);}
Each gateway implementation encapsulates its own redirect flow, token lifecycle, and error normalization behind that single method. The checkout screen depends only on the interface — adding a fifth gateway means writing one new class, touching zero checkout code.
Local-First Cart Without an Account
E-commerce apps typically require login before adding to cart. I chose shared_preferences (not SQLite) for the cart and wishlist because the data is small (a list of product IDs + quantities), session-scoped, and doesn't need querying. This lets users build a cart before committing to sign-up — a deliberate product decision to reduce abandonment in a market where users are cautious about sharing credentials.
Connectivity-Aware Architecture
Rather than wrapping every network call in try-catch, connectivity_plus feeds a singleton stream into Provider. Screens subscribe to connectivity state and adjust their UI accordingly: product grids remain fully browsable from cache, the checkout button shows a warning banner when offline, and the cart badge continues to update. This decouples connectivity logic from business logic — no screen needs to know how connectivity is detected.
Technical Stack
Package
Purpose
dio
Network requests with interceptors
provider
State management
shared_preferences
Local persistence (cart, wishlist, session)
connectivity_plus
Network state monitoring
cached_network_image
Image loading and caching
carousel_slider
Product image galleries
image_picker
Profile photo selection
flutter_svg
SVG asset rendering
pin_code_fields
OTP verification UI
syncfusion_flutter_pdfviewer
Invoice and document viewing
share_plus
Share products via platform share sheet
Challenges & Solutions
Challenge: Four different payment gateways, each with a distinct integration pattern, error code vocabulary, and sandbox behaviour. Keeping the checkout flow consistent regardless of which gateway the user chose was complex.
Solution: Built a payment gateway abstraction layer — a common PaymentGateway interface with a pay(amount, orderId) method. Each gateway implementation handles its own redirect flow, token management, and error normalisation behind that interface. The checkout screen only knows about the interface, not the gateways.
Challenge: Product images from vendor systems were inconsistent in size and quality, causing layout jank as images loaded.
Solution: Standardised image dimensions via cached_network_image with fixed width and height constraints and a shimmer placeholder. Images are loaded lazily via ListView.builder so only visible items trigger network requests.
Challenge: App needed to work on budget Android devices (2GB RAM, older Android versions) that are common outside Dhaka.
Solution: Profiled on a real mid-range device throughout development. Used ListView.builder everywhere, kept widget trees shallow, and deferred initialisation of non-critical services using addPostFrameCallback.
Outcome
A single PaymentGateway interface absorbed all four gateways' quirks — adding a fifth would mean one new class and zero changes to checkout code
Local-first cart and connectivity-aware UX kept the app fully browsable and largely usable through the connectivity gaps common outside Dhaka
Rendering and initialization work tuned specifically for 2GB-RAM, Android 8+ devices, profiled on real hardware rather than emulators
Post-launch monitoring drove targeted fixes that meaningfully reduced the crash rate