Skip to main content

Overview

The MND mobile app is built with Flutter, supporting both Android and iOS platforms. This guide covers building, signing, and submitting to app stores.

Prerequisites

  • Flutter SDK 3.0+
  • Dart SDK 3.0+
  • Android Studio (for Android builds)
  • Xcode (for iOS builds, macOS only)
  • Backend API running and accessible

Initial Setup

1. Install Flutter

Follow the official Flutter installation guide for your platform. Verify installation:
flutter doctor

2. Install Dependencies

cd mnd_flutter
flutter pub get
Key dependencies:
  • provider - State management
  • http - Networking
  • shared_preferences - Local storage
  • google_maps_flutter - Map integration
  • flutter_dotenv - Environment configuration

3. Configure Environment

Create .env file:
API_BASE_URL=http://192.168.1.100:3000
GOOGLE_MAPS_API_KEY=your_mobile_maps_api_key
For production:
API_BASE_URL=https://api.yourdomain.com
GOOGLE_MAPS_API_KEY=your_production_maps_api_key
See Environment Variables for details.

Development Testing

Run on Emulator/Simulator

Android:
flutter run
iOS:
flutter run

Run on Physical Device

Android:
  1. Enable Developer Options and USB Debugging on your Android device
  2. Connect via USB
  3. Run:
flutter devices
flutter run -d <device-id>
iOS:
  1. Connect iPhone/iPad via USB
  2. Trust the computer on your device
  3. Run:
flutter run -d <device-id>

Hot Reload

While app is running:
  • Press r - Hot reload
  • Press R - Hot restart
  • Press q - Quit

Android Deployment

1. Configure App Identity

Edit android/app/build.gradle:
android {
    defaultConfig {
        applicationId "com.yourcompany.mnd_flutter"
        minSdkVersion 21
        targetSdkVersion 33
        versionCode 1
        versionName "1.0.0"
    }
}
Update:
  • applicationId - Unique app identifier (reverse domain notation)
  • versionCode - Integer version (increment for each release)
  • versionName - Display version (e.g., “1.0.0”)

2. Configure Google Maps API Key

Edit android/app/src/main/AndroidManifest.xml:
<application>
    <!-- ... -->
    <meta-data
        android:name="com.google.android.geo.API_KEY"
        android:value="YOUR_ANDROID_MAPS_API_KEY" />
</application>

3. Create App Icon

Replace android/app/src/main/res/mipmap-*/ic_launcher.png with your app icons:
  • mipmap-mdpi/ic_launcher.png (48x48)
  • mipmap-hdpi/ic_launcher.png (72x72)
  • mipmap-xhdpi/ic_launcher.png (96x96)
  • mipmap-xxhdpi/ic_launcher.png (144x144)
  • mipmap-xxxhdpi/ic_launcher.png (192x192)
Or use the flutter_launcher_icons package:
dev_dependencies:
  flutter_launcher_icons: ^0.13.1

flutter_launcher_icons:
  android: true
  ios: true
  image_path: "assets/icon/app_icon.png"
flutter pub run flutter_launcher_icons

4. Generate Signing Key

Create keystore:
keytool -genkey -v -keystore ~/mnd-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias mnd
Answer the prompts and save the password securely. Create key.properties: Create android/key.properties:
storePassword=your_store_password
keyPassword=your_key_password
keyAlias=mnd
storeFile=/Users/yourname/mnd-release-key.jks
Add key.properties to .gitignore - never commit this file!

5. Configure Signing in build.gradle

Edit android/app/build.gradle:
def keystoreProperties = new Properties()
def keystorePropertiesFile = rootProject.file('key.properties')
if (keystorePropertiesFile.exists()) {
    keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
}

android {
    // ...

    signingConfigs {
        release {
            keyAlias keystoreProperties['keyAlias']
            keyPassword keystoreProperties['keyPassword']
            storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null
            storePassword keystoreProperties['storePassword']
        }
    }

    buildTypes {
        release {
            signingConfig signingConfigs.release
            minifyEnabled true
            shrinkResources true
        }
    }
}

6. Build APK

Standard APK:
flutter build apk --release
Output: build/app/outputs/flutter-apk/app-release.apk Split APK by ABI (smaller size):
flutter build apk --split-per-abi --release
Creates separate APKs for:
  • app-armeabi-v7a-release.apk (32-bit ARM)
  • app-arm64-v8a-release.apk (64-bit ARM)
  • app-x86_64-release.apk (64-bit Intel)

7. Build App Bundle (for Play Store)

Recommended format for Google Play:
flutter build appbundle --release
Output: build/app/outputs/bundle/release/app-release.aab Benefits:
  • Smaller download size (Google Play generates optimized APKs)
  • Required for apps over 150MB
  • Supports Dynamic Delivery

8. Test Release Build

Install APK on device:
flutter install build/app/outputs/flutter-apk/app-release.apk
Or:
adb install build/app/outputs/flutter-apk/app-release.apk

9. Submit to Google Play Store

  1. Create account at Google Play Console
  2. Pay one-time $25 registration fee
  3. Create new app
  4. Fill in app details:
    • App name
    • Short description
    • Full description
    • Screenshots (required: at least 2)
    • Feature graphic (1024x500)
    • App icon (512x512)
    • Privacy policy URL
  5. Upload AAB file
  6. Complete Content Rating questionnaire
  7. Set pricing (free/paid)
  8. Select countries
  9. Submit for review
Review time: 1-7 days typically

iOS Deployment

1. Configure App Identity

Edit ios/Runner/Info.plist:
<key>CFBundleIdentifier</key>
<string>com.yourcompany.mndFlutter</string>
<key>CFBundleName</key>
<string>MND</string>
<key>CFBundleShortVersionString</key>
<string>1.0.0</string>
<key>CFBundleVersion</key>
<string>1</string>

2. Configure Google Maps API Key

Edit ios/Runner/AppDelegate.swift:
import GoogleMaps

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GMSServices.provideAPIKey("YOUR_IOS_MAPS_API_KEY")
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

3. Create App Icon

Replace icons in ios/Runner/Assets.xcassets/AppIcon.appiconset/ Or use flutter_launcher_icons (see Android section).

4. Open in Xcode

open ios/Runner.xcworkspace
Always open .xcworkspace, not .xcodeproj

5. Configure Signing

In Xcode:
  1. Select Runner project
  2. Select Runner target
  3. Go to Signing & Capabilities
  4. Check Automatically manage signing
  5. Select your Team (requires Apple Developer account)
  6. Set Bundle Identifier (must match App ID)

6. Register App ID

  1. Go to Apple Developer Portal
  2. Certificates, Identifiers & Profiles
  3. Identifiers → Click +
  4. Select App IDs → Continue
  5. Select App → Continue
  6. Enter:
    • Description: MND Bus Routing
    • Bundle ID: com.yourcompany.mndFlutter
    • Capabilities: (select as needed)
  7. Register

7. Build Release

Command line:
flutter build ios --release
Or in Xcode:
  1. Select Any iOS Device (arm64) as target
  2. Product → Archive
  3. Wait for archive to complete
  4. Organizer window opens

8. Distribute via Xcode

In Xcode Organizer:
  1. Select your archive
  2. Click Distribute App
  3. Choose distribution method:
    • App Store Connect - for App Store submission
    • Ad Hoc - for testing on specific devices
    • Enterprise - for internal distribution
    • Development - for development testing
  4. Follow the wizard
  5. Upload to App Store Connect

9. Submit to App Store

  1. Go to App Store Connect
  2. Create new app:
    • Platform: iOS
    • Name: MND Bus Routing
    • Primary Language: English
    • Bundle ID: (select registered ID)
    • SKU: unique identifier
  3. Fill in app information:
    • Subtitle
    • Description
    • Keywords
    • Support URL
    • Privacy Policy URL
  4. Upload screenshots:
    • 6.5” display (required)
    • 5.5” display (optional)
    • iPad Pro (if supporting iPad)
  5. Select build uploaded from Xcode
  6. Set pricing
  7. Submit for review
Review time: 1-3 days typically

App Store Requirements

Screenshots

Android (Google Play):
  • Minimum 2 screenshots
  • Format: JPEG or 24-bit PNG
  • Minimum dimension: 320px
  • Maximum dimension: 3840px
  • Recommended: 1080x1920 (portrait) or 1920x1080 (landscape)
iOS (App Store):
  • Required for 6.5” display (iPhone 14 Pro Max)
  • Optional for 5.5” and iPad Pro
  • Format: JPEG or PNG
  • Sizes:
    • 6.5”: 1284x2778 or 2778x1284
    • 5.5”: 1242x2208 or 2208x1242
    • iPad Pro: 2048x2732 or 2732x2048
Generate screenshots:
# Run app on device/emulator
flutter run --release

# Take screenshots from device
# Android: Power + Volume Down
# iOS: Side Button + Volume Up
Or use automated screenshot tools:

Privacy Policy

Both stores require a privacy policy URL. Include:
  • What data you collect
  • How you use it
  • Third-party services (Google Maps, Analytics)
  • Data retention
  • User rights
Example privacy policy generators:

Environment Configuration

Production vs Development

Development (.env):
API_BASE_URL=http://192.168.1.100:3000
GOOGLE_MAPS_API_KEY=dev_key
Production (.env.production):
API_BASE_URL=https://api.yourdomain.com
GOOGLE_MAPS_API_KEY=production_key
Load based on build mode:
import 'package:flutter_dotenv/flutter_dotenv.dart';

Future<void> main() async {
  // Load environment based on build mode
  String envFile = const String.fromEnvironment('ENV', defaultValue: '.env');
  await dotenv.load(fileName: envFile);
  
  runApp(MyApp());
}
Build with specific environment:
flutter build apk --dart-define=ENV=.env.production

Version Management

Semantic Versioning

Use format: MAJOR.MINOR.PATCH
  • MAJOR: Breaking changes
  • MINOR: New features
  • PATCH: Bug fixes
Update in pubspec.yaml:
version: 1.2.3+4
  • 1.2.3 = versionName (Android) / CFBundleShortVersionString (iOS)
  • 4 = versionCode (Android) / CFBundleVersion (iOS)

Incrementing Versions

Before each release:
  1. Update pubspec.yaml:
    version: 1.0.1+2  # was 1.0.0+1
    
  2. Android auto-updates from pubspec
  3. iOS: Update in Xcode or Info.plist

App Signing Best Practices

Android Keystore

Losing your keystore means you can never update your app again!
Backup your keystore:
cp ~/mnd-release-key.jks ~/Dropbox/backups/
Store passwords securely:
  • Use a password manager
  • Don’t commit to git
  • Share securely with team (encrypted)

iOS Certificates

  • Certificates expire after 1 year (auto-renewed by Xcode)
  • Provisioning profiles expire after 1 year
  • Distribution certificates limited to 3 per team

Testing Before Release

Beta Testing

Android (Google Play Console):
  1. Create Internal/Closed/Open testing track
  2. Upload AAB
  3. Add testers by email
  4. Share opt-in URL
iOS (TestFlight):
  1. Upload build via Xcode
  2. Go to App Store Connect → TestFlight
  3. Add internal testers (up to 100)
  4. Add external testers (up to 10,000)
  5. Testers install TestFlight app and opt-in

Pre-Launch Checklist

  • App icon configured
  • Splash screen configured
  • All API endpoints use production URL
  • Google Maps API key is restricted
  • App permissions justified (location, etc.)
  • Tested on physical devices (Android + iOS)
  • Tested on different screen sizes
  • Tested offline behavior
  • Privacy policy created
  • Screenshots captured
  • Store listings written
  • App signed with release key
  • Version numbers updated
  • Changelog prepared

Troubleshooting

Android Build Fails

# Clean build
flutter clean
cd android && ./gradlew clean && cd ..
flutter pub get
flutter build apk

iOS Build Fails

# Clean build
flutter clean
cd ios
rm -rf Pods Podfile.lock
pod install
cd ..
flutter build ios

App Crashes on Launch

Check logs:
# Android
adb logcat | grep flutter

# iOS
flutter logs
Common issues:
  • Missing .env file
  • Invalid API URL
  • Missing permissions

Google Maps Not Showing

  • Verify API key is correct
  • Check API key restrictions (bundle ID/package name)
  • Ensure Maps SDK enabled in Google Cloud Console
  • Check device has internet connection

Next Steps