Mobile App

The Flutter app for Android and iOS: setup, API_BASE, rebranding, Firebase push, deep links and release builds.
Version: 1.2.0
Stack: Laravel 13 · Flutter 3.44 · MySQL

Item: MediaDesk — Multilingual AI-Powered Newsroom ERP

Before You Start: Google Firebase Dependency

The app has a hard dependency on Google Firebase. The Flutter packages firebase_core and firebase_messaging (Google, BSD-3 licensed SDK) are compiled into every build of the app; they cannot be removed without changing the source code.

  • You need your own Firebase project (free to create at console.firebase.google.com) under your own Google account if you want push notifications in the app. The author does not provide, share or operate a Firebase project for you, and the package contains no google-services.json, GoogleService-Info.plist, API key or service account.
  • You must accept Google’s terms. Creating a Firebase project means agreeing to the Firebase Terms of Service and the Google APIs Terms of Service, and Google’s data-processing terms. Mention Firebase Cloud Messaging in your app’s privacy policy and in the store data-safety forms (it processes a device token).
  • Costs: Firebase Cloud Messaging is currently free, but it is Google’s service and its pricing and terms can change; any charge is between you and Google.
  • Without Firebase the app still builds and runs: articles, accounts, podcasts, e-paper and everything else work, only push notifications stay off.
  • Who: a developer adds the Firebase files when building the app (Firebase Push); the project id and service-account JSON are then pasted into the admin panel (Common Setup → Push).

What the App Does

The MediaDesk app is a Flutter client (Android and iOS, v1.2.0, Flutter 3.44 / Dart 3.12) for your MediaDesk backend. It has no content of its own: everything comes from your site’s REST API under /api/v1, in the language the reader chooses (English, Bangla, Arabic with right-to-left layout, French). State management is provider; the sign-in token is kept in the Keychain / EncryptedSharedPreferences via flutter_secure_storage.

  • Home (breaking bar, hero, latest, trending), categories, article reader with key points, reactions and comments, search.
  • E-paper, live blogs, podcasts with an audio player, polls.
  • Accounts (register, sign in, forgot password), profile, saved articles and reading history (stored on the device), membership plans, notification settings.
  • Settings: language, light / dark / system theme, text size. Offline cache of the latest articles. Push with Firebase (optional) and deep links.

The app does not process payments and never shows a payment page inside the app: Choose plan and Buy this article open your website checkout in the device’s external browser. Read Payments & store policy before you publish.

What’s New in 1.2.0

  • Read aloud: the headphones button in article tools reads the article with the device voice. Tap it again to stop. Languages depend on the voices installed on the device.
  • Following: readers follow topics and authors, and see them in the Following feed.
  • Library: saved articles and reading history work without signing in, and readers can back up their library.
  • Account tools: account security controls and billing access under Account & Library.
  • E-paper: lists real PDF editions from your site, with protected PDF access.
  • Bottom tabs and theme: set on the website under Site → Navigation → Mobile bottom tabs and Settings → Public site → Default theme.
  • Update and maintenance control: set on the website under System → Operations.

Screens

Captured on an iPhone 16 Pro simulator against the demo database (English locale).

Home with breaking bar and hero story
Home with breaking bar and hero story
Home, scrolled: latest list
Home, scrolled: latest list
Categories
Categories
Article with key points
Article with key points
Article body and reactions
Article body and reactions
Search
Search
E-paper editions
E-paper editions
Live blogs
Live blogs
Podcasts
Podcasts
Membership plans
Membership plans
Notification settings
Notification settings
Settings: language, appearance, text size
Settings: language, appearance, text size
More menu: profile, saved articles, history, newsletter, contact
More menu: profile, saved articles, history, newsletter, contact

Requirements

ToolVersion / note
Flutter SDK3.44 or newer (Dart 3.12 or newer; pubspec.yaml has sdk: ^3.12.0)
AndroidAndroid Studio with SDK; JDK 17 (Gradle config targets Java 17); Android Gradle Plugin 9.0.1 and Kotlin 2.3.20 are declared in android/settings.gradle.kts
iOSmacOS, Xcode, CocoaPods, an Apple Developer account for devices and the store. Minimum iOS version is 15.0 (Firebase requires it).
BackendA running MediaDesk site with the Mobile / Public API v1 module enabled and at least one published article
FirebaseThe Firebase SDK is always compiled in. Your own Firebase project (Google account, Google terms) is needed only for push notifications; see Firebase dependency

Setup & Run

cd Main_Files/AppSourceCode
flutter pub get
flutter analyze
flutter test
flutter run --dart-define=API_BASE=http://10.0.2.2:8000

Start the backend (php artisan serve in the web project, or your staging URL). Targets:

TargetAPI_BASE
Android emulator (default if you pass nothing)http://10.0.2.2:8000 (the emulator’s alias for your computer)
iOS simulator / desktop (default if you pass nothing)http://127.0.0.1:8000
Real phone on your Wi-Fihttp://192.168.x.x:8000 — start the backend with php artisan serve --host=0.0.0.0
Staging / productionhttps://your-domain.com

Plain http:// works only in debug and profile builds (Android allows cleartext traffic only in android/app/src/debug and profile manifests; the release manifest does not). A release build needs an https:// API.

API_BASE

The server address is read in lib/config.dart:

String get apiBase {
  const fromEnv = String.fromEnvironment('API_BASE');
  if (fromEnv.isNotEmpty) return fromEnv;
  if (!kIsWeb && Platform.isAndroid) return 'http://10.0.2.2:8000';
  return 'http://127.0.0.1:8000';
}

Pass it on every run and build with --dart-define=API_BASE=https://your-domain.com: no trailing slash and no /api/v1 (the app appends it). Release builds default to the placeholder https://your-domain.com; debug/profile builds use the emulator or simulator localhost defaults unless you pass an explicit value. If you prefer not to type it each time, create a dart_defines.json file and use --dart-define-from-file=dart_defines.json:

{ "API_BASE": "https://your-domain.com" }

Confirm the API answers: curl https://your-domain.com/api/v1/languages.

Customization & Rebranding

Replace the MediaDesk identity with your own before the first store build. Use only letters, digits, hyphens and dots in identifiers: Apple rejects underscores in bundle identifiers, so keep the Android and iOS ids identical and underscore-free (for example com.yourbrand.news). The application id and bundle identifier cannot change after publishing.

The table shows the current value of every identifier and where to change it.

WhatCurrent valueWhere to change it
App name (Android)MediaDeskandroid:label in android/app/src/main/AndroidManifest.xml
App name (iOS)MediaDesk / MediaDeskCFBundleDisplayName (name under the icon) / CFBundleName in ios/Runner/Info.plist
App name (inside the app)MediaDesktitle: of MaterialApp in lib/main.dart; the texts “Support MediaDesk” and “Advertise with MediaDesk” in lib/features/native_content/native_content_screens.dart. Headers show the site name your API returns
Android application idcom.mediadesk.readerapplicationId and namespace in android/app/build.gradle.kts
Android Kotlin packagecom.mediadesk.readerMove android/app/src/main/kotlin/com/mediadesk/reader/MainActivity.kt to the folder of your new id and update its package line
iOS bundle identifiercom.mediadesk.reader (tests: com.mediadesk.reader.RunnerTests)Xcode → Runner target → General (or PRODUCT_BUNDLE_IDENTIFIER in ios/Runner.xcodeproj/project.pbxproj)
Deep-link schememediadesk://android/app/src/main/AndroidManifest.xml, CFBundleURLSchemes in ios/Runner/Info.plist and lib/services/deep_links.dart (see Deep Links)
Version1.2.0+2version: in pubspec.yaml (name+build). Raise the build number for every store upload
App icon

The project includes flutter_launcher_icons (configured at the end of pubspec.yaml). Replace the two source images and regenerate every Android and iOS size:

  1. Replace the images

    assets/icon/app_icon.png — 1024×1024 PNG, square, no transparency (used for iOS and the legacy Android icon). assets/icon/app_icon_foreground.png — 1024×1024 PNG with the logo centred inside the middle ~66% and a transparent background (Android adaptive icon). Change adaptive_icon_background: "#FFFFFF" in pubspec.yaml to your background colour.

  2. Generate

    Run dart run flutter_launcher_icons. It rewrites android/app/src/main/res/mipmap-*, drawable-*/ic_launcher_foreground.png and ios/Runner/Assets.xcassets/AppIcon.appiconset.

Splash screen

The native launch screens are used (no splash package):

  • Android: android/app/src/main/res/drawable/launch_background.xml and drawable-v21/launch_background.xml. Change @android:color/white to your colour, and uncomment the <bitmap> item to show a centred logo (add the image as mipmap/launch_image or point android:src at your drawable).
  • iOS: replace the images in ios/Runner/Assets.xcassets/LaunchImage.imageset (1x, 2x, 3x) and adjust the background colour in ios/Runner/Base.lproj/LaunchScreen.storyboard with Xcode’s Interface Builder.

Prefer a generator? Add flutter_native_splash to dev_dependencies, configure your colour and image in pubspec.yaml, and run dart run flutter_native_splash:create; it writes the same files.

Colours, fonts and texts
  • Colours: the app mirrors the website’s reader skins. Per-skin colours, including the brand colour, are in _build in lib/theme/md_tokens.dart; the Material theme is built in lib/theme/md_theme.dart. The backend’s app-config endpoint can override background, surface, text and line colours and the corner radius at runtime.
  • Fonts: bundled in assets/fonts/ and declared under fonts: in pubspec.yaml (Inter, Source Serif 4, Noto Sans Bengali, Noto Naskh Arabic); the family names are kSansFamily / kSerifFamily in lib/theme/md_tokens.dart.
  • Texts: shared labels in lib/strings.dart, feature labels in each lib/features/*/strings.dart (en, bn, ar, fr). The server can override them.
Signing team (iOS)

The project ships without an Apple development team. Open ios/Runner.xcworkspace in Xcode → Runner → Signing & Capabilities and choose your own team before running on a device or archiving.

Run flutter clean && flutter pub get after renaming, then rebuild. flutter pub get also creates the generated plugin files (GeneratedPluginRegistrant.*, ios/Flutter/Generated.xcconfig), which are not shipped.

Firebase Push

Push needs your own Firebase project (see Firebase dependency). PushService calls Firebase.initializeApp(); when no Firebase configuration is present it stays inert, so the app runs fine without it. To enable it:

  1. Create the Firebase project

    Add an Android app with your application id and an iOS app with your bundle id.

  2. Android

    Download google-services.json into android/app/. Add the Google services Gradle plugin: id("com.google.gms.google-services") version "…" apply false in the plugins block of android/settings.gradle.kts, and id("com.google.gms.google-services") in android/app/build.gradle.kts. POST_NOTIFICATIONS is already declared for Android 13+.

  3. iOS

    Add GoogleService-Info.plist to ios/Runner in Xcode (tick the Runner target). Enable Push Notifications and Background Modes → Remote notifications capabilities, and upload an APNs authentication key in Firebase → Project settings → Cloud Messaging.

  4. Shortcut

    dart pub global activate flutterfire_cli && flutterfire configure generates the platform files for you.

  5. Connect the server

    In the admin panel add the Firebase project id and service-account JSON under Settings → Integrations → Firebase (FCM) (Common Setup). The app registers its FCM token with POST /api/v1/devices (with language and topics); the server sends through FCM HTTP v1 when you send a push campaign or a breaking alert.

  6. Test

    On a real device: php artisan fcm:test <token> --title="Hello" --body="Test" --url=/en/news/some-slug in the web project. Tapping a notification whose data.url is an article opens that article.

Server-side FCM sending was tested with faked HTTP only. Test on a real Android device and a real iPhone (iOS push does not work in the simulator for APNs tokens) with your own Firebase project.

Release Builds

Android
  1. Create an upload keystore

    keytool -genkeypair -v -keystore ~/mediadesk-upload-keystore.jks -keyalg RSA -keysize 2048 -validity 10000 -alias mediadesk. Keep it and its passwords safe and out of git.

  2. Create android/key.properties

    Use exactly: storePassword=your_keystore_password, keyPassword=your_key_password, keyAlias=mediadesk, storeFile=/absolute/path/to/mediadesk-upload-keystore.jks.

  3. Use the shipped signing setup

    The distributed android/app/build.gradle.kts already reads android/key.properties and uses its keystore for release builds. It does not use the debug key.

  4. Build

    flutter build appbundle --release --dart-define=API_BASE=https://your-domain.com (Play Store, .aab) or flutter build apk --release --dart-define=API_BASE=https://your-domain.com. Output in build/app/outputs/.

iOS
  1. Signing

    Open ios/Runner.xcworkspace in Xcode, select your team, set the bundle id, enable automatic signing (or use a distribution certificate and profile).

  2. Build

    flutter build ipa --release --dart-define=API_BASE=https://your-domain.com — creates an archive in build/ios/archive and an .ipa in build/ios/ipa.

  3. Upload

    Use Xcode Organizer or Transporter to upload to App Store Connect, then submit via TestFlight / review.

Always pass the same --dart-define=API_BASE=… to every build command when targeting a different server. A release build without it uses the placeholder https://your-domain.com; debug/profile builds use local defaults. Release builds enable R8 shrinking and resource shrinking. For stronger Dart obfuscation, use --obfuscate --split-debug-info=build/symbols and archive the symbols for crash de-obfuscation.

Payments & Store Policy

The app contains no in-app purchase and no payment code. Choose plan (membership screen) and Buy this article (premium articles) open your website checkout — /{lang}/membership/checkout/{plan} and /{lang}/membership/buy/{article} — in the device’s external browser, never in a WebView inside the app. The reader pays on your website with the gateways you enabled; when they come back to the app it re-loads their plan and purchased articles from the server, and the access is shared with the website account.

Apple (App Store Review Guideline 3.1.1) and Google Play (Payments policy) restrict links to external payment for digital content, and the rules differ by country and by the programmes or entitlements of your developer account. Where your store listing may not link to external purchases, build with --dart-define=CHECKOUT_LINKS=false: the purchase buttons are hidden, the app shows “Plans and purchases are available on our website.”, and members who bought on the website keep their access after signing in. You, as the publisher of the app, are responsible for complying with the store rules in your regions. Native store billing (StoreKit / Play Billing) is not implemented.

# Store build without purchase buttons
flutter build ipa --release --dart-define=API_BASE=https://your-domain.com --dart-define=CHECKOUT_LINKS=false
flutter build appbundle --release --dart-define=API_BASE=https://your-domain.com --dart-define=CHECKOUT_LINKS=false
Store checklist
  • Final app name, unique application id / bundle id, version and build number
  • App icon and splash replaced with your own (see Customization & Rebranding)
  • Release signing in place (Android upload key with Play App Signing; iOS distribution certificate and profile)
  • Release build uses https:// and has no debug URLs (cleartext is debug/profile only)
  • Privacy policy URL (the site’s static pages are reachable from the app) and data-safety / App Privacy answers: email, name, device id, push token; reading history stored locally
  • Account deletion: the Profile screen includes a delete-account action that calls DELETE /api/v1/me. For the store listing, use https://your-site/{locale}/account/delete as the web link for account deletion: readers can use it without the app, and Google sign-in accounts can confirm by e-mail. The API answers 409 while an author payout is in progress. Keep the privacy-policy and account-deletion links available in the published app and store listing
  • Paid plans: decide between external checkout links (default) and CHECKOUT_LINKS=false for each store and region, as described above
  • Your own Apple team selected in Xcode (none is set in the project)
  • Screenshots (phone and tablet), short and full descriptions per language, category, content rating, contact e-mail
  • Notification permission text and Android 13 POST_NOTIFICATIONS (declared); Firebase configured if you advertise push
  • Test on real devices (Android 8+ and a recent iPhone), Arabic RTL, large text, dark mode, offline start
  • Decide on crash reporting / analytics and disclose it

Troubleshooting

SymptomCause / fix
Blank home / “could not load” on a real phoneAPI_BASE still points at 10.0.2.2 or 127.0.0.1. Pass your computer’s LAN IP or your https domain; start the backend with --host=0.0.0.0 for LAN testing
Works in debug, fails in release on AndroidRelease builds block cleartext HTTP: use an https:// API_BASE with a valid certificate
iOS: “The package … requires a minimum iOS deployment target of 15.0” / pod install failsThe project targets iOS 15.0 (IPHONEOS_DEPLOYMENT_TARGET = 15.0). Firebase pods need 15.0 or newer. If a generated ios/Podfile says an older version, set platform :ios, '15.0', then cd ios && pod repo update && pod install
iOS build error after changing the bundle idOpen ios/Runner.xcworkspace (not .xcodeproj), re-select the signing team, then flutter clean && flutter pub get
Android: Gradle / Java version errorUse JDK 17; run flutter doctor -v; make sure android/local.properties has flutter.sdk
No push notificationsFirebase files missing (the app logs “Push disabled”), FCM credentials not saved in Integrations, no queue worker running, notification permission denied, or testing on the iOS simulator
Push arrives but tap does not open the articlePayload data.url must be an article URL (/en/news/slug or mediadesk://article/slug)
401 after some time / signed out unexpectedlyReader tokens expire after 180 days or when revoked (password change, account deletion); sign in again
429 Too Many RequestsServer rate limits: 120/min/IP for public API, 10/min for sign-in routes, 60/min for /me and devices
Language list empty or wrong content languageOnly active languages appear in /api/v1/languages; missing translations fall back to the default language
Premium article shows only an introBy design for non-members (server-side truncation)
Arabic layout not mirroredPick Arabic in Settings; the app flips to RTL for that language
Images do not loadCheck the web public/storage link or your S3/CDN URL, and that APP_URL matches the real https origin