Lesson 42 of 77 intermediate

Android Fundamentals for Flutter Engineers

Activity lifecycle, Intents, Services, BroadcastReceivers — why Flutter devs must know this

Open interactive version (quiz + challenge)

Real-world analogy

Flutter is like a high-end appliance with a smart control panel (Dart). Android is the electrical infrastructure of the building it runs in — circuits (ActivityManager), fuse boxes (Manifest), wiring (Intents). You can use the appliance without knowing the wiring, but when something trips a breaker or you need to add a new socket (platform channel), you need to understand the infrastructure.

What is it?

Android is the operating system layer beneath every Flutter Android app. Flutter engineers who understand Android fundamentals can write platform channels correctly, diagnose production crashes, interpret logcat output, and collaborate with Android-native teammates — all critical for senior hybrid roles.

Real-world relevance

In an NGO offline-first field survey app: a foreground service keeps GPS tracking active during a field visit. A BroadcastReceiver detects network reconnection and sends a broadcast that triggers the sync WorkManager task. The AndroidManifest declares the FOREGROUND_SERVICE and ACCESS_FINE_LOCATION permissions, plus intent-filter for the deep link scheme. The Flutter team needs to know this to debug why the sync does not start after network reconnects.

Key points

Code example

// --- ANDROID MANIFEST ENTRIES (android/app/src/main/AndroidManifest.xml) ---
/*
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
  <!-- Permissions -->
  <uses-permission android:name="android.permission.INTERNET"/>
  <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
  <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
  <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>

  <application ...>
    <activity android:name=".MainActivity" android:exported="true">
      <!-- Deep link intent filter -->
      <intent-filter android:autoVerify="true">
        <action android:name="android.intent.action.VIEW"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>
        <data android:scheme="https" android:host="app.myservice.com"/>
      </intent-filter>
    </activity>

    <!-- Foreground service for GPS tracking -->
    <service android:name=".LocationForegroundService"
             android:foregroundServiceType="location"
             android:exported="false"/>

    <!-- BroadcastReceiver for network changes -->
    <receiver android:name=".NetworkChangeReceiver"
              android:exported="false">
      <intent-filter>
        <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>
      </intent-filter>
    </receiver>
  </application>
</manifest>
*/

// --- PLATFORM CHANNEL — Calling Android Intent from Flutter ---
// Dart side
class ShareService {
  static const _channel = MethodChannel('com.myapp/share');
  static Future<void> shareText(String text) =>
      _channel.invokeMethod('shareText', {'text': text});
}

// Kotlin side (MainActivity.kt)
/*
class MainActivity : FlutterActivity() {
  override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "com.myapp/share")
      .setMethodCallHandler { call, result ->
        if (call.method == "shareText") {
          val intent = Intent(Intent.ACTION_SEND).apply {
            type = "text/plain"
            putExtra(Intent.EXTRA_TEXT, call.argument<String>("text"))
          }
          startActivity(Intent.createChooser(intent, "Share via"))
          result.success(null)
        }
      }
  }
}
*/

// --- FOREGROUND SERVICE (Kotlin) ---
/*
class LocationForegroundService : Service() {
  override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    val notification = buildNotification("Tracking location...")
    startForeground(NOTIF_ID, notification)  // Required — shows persistent notification
    startLocationUpdates()
    return START_STICKY  // Restart if killed
  }
  override fun onBind(intent: Intent?): IBinder? = null
}
*/

Line-by-line walkthrough

  1. 1. uses-permission entries declare what system resources the app needs — missing permissions cause SecurityException at runtime
  2. 2. intent-filter with autoVerify='true' enables App Links — Android verifies your domain ownership before allowing deep links
  3. 3. android:foregroundServiceType='location' is required since Android 10 for location foreground services
  4. 4. START_STICKY in onStartCommand tells Android to recreate the service after killing it — critical for always-on trackers
  5. 5. MethodChannel name must match exactly on Dart and Kotlin sides — mismatch causes MissingPluginException
  6. 6. Intent.ACTION_SEND with createChooser shows the system share sheet — no need to handle individual app integrations
  7. 7. startForeground(id, notification) must be called within 5 seconds of service start or Android throws a ForegroundServiceStartNotAllowedException
  8. 8. result.success(null) must always be called — failing to call result leaks the channel reply handle

Spot the bug

// Kotlin MethodChannel handler
MethodChannel(messenger, "com.myapp/data")
  .setMethodCallHandler { call, result ->
    if (call.method == "fetchUserData") {
      val data = networkService.fetchUserBlocking()  // Synchronous, ~2 seconds
      result.success(data)
    }
  }
Need a hint?
This code works in testing but causes production ANRs under load. What is wrong and how do you fix it?
Show answer
Bug: networkService.fetchUserBlocking() runs synchronously on the Android main thread (MethodChannel handlers execute on the main thread). A 2-second blocking call risks ANR (Android kills apps blocking main thread for 5+ seconds). Under load or slow networks it will exceed this. Fix: use Kotlin coroutines — launch(Dispatchers.IO) { val data = networkService.fetchUser(); withContext(Dispatchers.Main) { result.success(data) } }. The IO dispatcher runs on a background thread pool; withContext(Main) switches back to post the result safely.

Explain like I'm 5

Android is like a city. Your Flutter app is a building in that city. The city has rules: buildings must register with the city hall (Manifest), they can send messages to other buildings (Intents), some buildings have security guards who keep working all night (Foreground Services), and there are town criers who shout news to everyone (BroadcastReceivers). Even if you live in the Flutter building, you still follow the city's rules.

Fun fact

The Android Activity was conceived in 2007 when multitasking on phones was a novel idea. The lifecycle was designed to handle phone calls interrupting apps — a problem nobody thinks about anymore. Yet every Flutter app on Android still goes through the exact same onCreate/onPause/onDestroy dance from that original design.

Hands-on challenge

You are debugging a production crash in an NGO field app. The crash log shows an ANR in the MethodChannel handler. Walk through: (1) why the ANR is happening, (2) how to fix it using Kotlin coroutines, (3) what Manifest permission you need to add for the foreground service that was added to prevent task loss.

More resources

Open interactive version (quiz + challenge) ← Back to course: Flutter Interview Mastery