Home About Skills Experience Projects Certifications GitHub ↗ Hire me
← Back to Portfolio
SystemPro icon
Company Project · Real Estate Marketplace · Valuerythm · Nov 2023 – May 2025

SystemPro

Overview

SystemPro is a bilingual (Arabic/English) real estate marketplace app for the Saudi market, built in Flutter. Property seekers browse listings for sale, rent, and short-term booking, filter them by category, price, size, bedrooms, bathrooms and amenities, search by district/city/region in either language, save favorites, and contact the listing agency directly by phone or WhatsApp. Listings are published by real estate companies, each with its own browsable profile page. The app ships with full RTL support, light/dark theming, offline-capable caching, and an automated Fastlane + GitHub Actions pipeline that distributes builds to testers via Firebase App Distribution.

AR / ENBilingual with full RTL support
6Advanced filters: price, size, beds, baths, amenities & more
CI/CDAutomated release pipeline via Fastlane + GitHub Actions
Core Capabilities

What the app does

🔐
Full Authentication Flow

Sign-up with email OTP verification, login, forgot-password with OTP resend, and password change — with tokens held in encrypted secure storage.

🏘️
Paginated Listing Marketplace

Cursor-based infinite scroll across buy, rent and booking listing types, with server-side sorting by newest, oldest, and price.

🎛️
Advanced Property Filters

Filter by category and property type (including lands), price range, area, floors, bedrooms, bathrooms, and amenities from a dedicated filter screen.

🔎
Bilingual Location Search

Type-ahead search over a nested region → city → district dataset that matches Arabic or English input against every field, with debouncing.

🕘
Recent Searches

Previously searched locations are persisted locally and offered as suggestions the next time the search screen opens.

❤️
Favorites With Live Sync

Saving or unsaving a listing updates the marketplace list, the details screen and the favorites tab together, so no screen shows a stale heart icon.

🖼️
Rich Listing Details

Swipeable image gallery with pinch-to-zoom full-screen viewer, plus amenities, floor count, bed/bath/area breakdown, location, and lister details.

🏢
Company Profiles

Each listing links to the publishing company's profile, showing its details alongside its own filtered set of active listings.

📞
Direct Agent Contact

One-tap phone call and WhatsApp handoff, with runtime permission requests and a fallback that sends the user to app settings when denied.

🔗
Deep-Linked Listing Sharing

Share any listing as a Branch short link that opens the app directly on that property, with a web fallback for users who don't have it installed.

🌓
Theming & Localization

Runtime light/dark theme switching and Arabic/English toggling, both persisted locally and applied with correct RTL layout direction.

🚀
Automated Release Pipeline

GitHub Actions runs code generation, builds via Fastlane, uploads Sentry symbols, ships to Firebase App Distribution, and posts a Telegram notice.

Architecture

How it's built

The app uses a feature-first modular architecture. Every feature — Authentication, Home, Search, EditProfile, CompanyProfile — is a self-contained slice with three layers: data (Freezed/json_serializable models plus a repository), logic (a Cubit and its state union), and ui (a view plus its widget folder). Shared concerns live in a core layer: dependency injection with get_it, a networking layer built on Dio and a Retrofit-generated API client, caching split between SharedPreferences and flutter_secure_storage, routing, theming, and localization.

State management is Bloc's Cubit throughout, chosen per-feature rather than uniformly: most cubits are plain Cubits, while the marketplace cubit extends HydratedCubit so listings survive a cold start. Repositories never throw — every call returns a Freezed ApiResult union of success or failure, so cubits pattern-match on the result and emit a typed state instead of handling exceptions. Errors are normalized once in a central ErrorHandler that maps network exception types and HTTP status codes to localized messages. Crash reporting runs through Sentry, with symbol upload wired into the CI pipeline.

App initializes Firebase, HydratedBloc storage, and the caching layer, then reads the saved token, locale, and theme before the first frame is built→Dependencies are registered in get_it, and theming/localization cubits are provided above the app→A UI event calls a method on the feature's Cubit, which emits a loading state→The Cubit calls its repository, which invokes the Retrofit-generated API client over the shared Dio client with the auth header attached→The repository wraps the response in an ApiResult union — success, or failure carrying a localized error→The Cubit pattern-matches on that union and emits a typed success or error state; the marketplace cubit also writes its listings to hydrated storage→BlocBuilder/BlocConsumer widgets rebuild against the state union, rendering loading, error, empty, and success cases explicitly
Walkthrough

See it in action

RealEstate Home
RealEstate Home
Favorites
Favorites
Filter
Filter
RealEstate Details
RealEstate Details
Company Profile
Company Profile
User Profile
User Profile
Problems Solved

Real engineering challenges

Pagination
Sorting a cursor-paginated list correctly

Listings load through cursor-based pagination, so the client only ever holds the pages fetched so far. The original implementation sorted that in-memory list, which meant 'lowest price' only reordered what had already been scrolled past rather than the full result set. Sorting was moved to the server by mapping the sort option to proper query parameters, and the client-side sort was removed entirely. That exposed a second issue: the listings cache was keyed by filter alone, so switching sort order returned the previous ordering from cache. Cache keys became a composite of filter and sort type, and changing either resets the cursor and refetches from the first page.

State Persistence
Deciding what should and shouldn't be hydrated

To get listings on screen instantly at launch, the marketplace cubit persists both its visible listings and its per-filter cache to disk. Applying the same approach to theme and language turned out to be the wrong fit: those need to be resolved before the widget tree is built, not restored asynchronously afterwards, so they were reverted to plain state read from local storage at startup instead. The persisted listing cache also introduced a freshness trade-off — entering a tab could serve indefinitely stale results — so tab initialization now forces a network refresh while the cached data paints the first frame, and the page size was reduced to keep each request light.

Search
Bilingual location search against a single-language API

The location dataset is a nested region → city → district tree where every node has both Arabic and English names, but the backend expects a single English 'District, City' string. A user typing in either language has to resolve to that canonical form. Search matches the query against all six name fields across both languages, and a resolver maps the selection back to its English pair before the request is built. Free text that matches nothing is deliberately rejected rather than forwarded to the API, since an unrecognized location silently returned zero results. The dataset is parsed off the UI thread so opening the search screen doesn't drop frames.

Error Handling
Localizing API errors from below the widget layer

Network and timeout errors are produced in the networking layer, which has no access to the localization system — so every connection failure surfaced in English regardless of the app language. Rather than pushing UI context into the data layer, the resolved language code is threaded through the call chain: cubits read it once and pass it down to the repository, and a central error handler uses it to select the right message when mapping exception types and status codes into a typed, localized error.

Deep Linking
Handling shared listing links at cold start

Sharing a property had to open the app directly on that listing, including when the app isn't running and no screen exists yet. Deep linking was migrated to the Branch SDK, attaching the listing ID as link metadata with a web URL fallback for users without the app installed. The session listener is initialized before the app itself launches and navigates through a global navigator key, so a link that arrives during a cold launch is still routed once the app is ready.

CI/CD
Getting readable release crash reports

Release builds are obfuscated, so crash reports were unusable without the correct symbol mapping — and the upload step in the release pipeline kept failing because it pointed at the wrong generated file. The path was corrected to the one Gradle actually produces, with an explicit check added that fails the build with a clear message if it's missing, so a broken symbol upload surfaces immediately in CI instead of quietly shipping a build whose crash reports can't be read.

Technologies Employed

Built with

FlutterBloc / CubitHydratedBlocDioRetrofitFreezedGetItflutter_secure_storageFirebase CoreSentryBranch SDKflutter_localizationsflutter_screenutilcached_network_imagepermission_handlerurl_launcherFastlane + GitHub Actions
Screenshot preview