Android Fundamentals for Flutter Engineers
Activity lifecycle, Intents, Services, BroadcastReceivers — why Flutter devs must know this
Open interactive version (quiz + challenge)Real-world analogy
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
- Activity Lifecycle — onCreate → onStart → onResume (visible+interactive) → onPause → onStop → onDestroy. Flutter's FlutterActivity maps to a single Activity. Understanding this explains why AppLifecycleState.paused maps to onPause/onStop in Android terms.
- Intents — Intents are Android's messaging objects for starting Activities (explicit: specific class) or Services/broadcasts (implicit: action string). Flutter uses MethodChannel to invoke Intent-based Android APIs (share sheet, camera, file picker).
- Foreground Services — A Service with a persistent notification that Android will not kill under memory pressure. Required for ongoing work like music playback, GPS tracking, or BankID sessions. Must declare FOREGROUND_SERVICE permission.
- Background Services — Regular Services run at low priority and can be killed by Android. Since Android 8 (Oreo), background execution is severely restricted — use WorkManager instead for deferred background work.
- Bound Services — Services that offer an IPC interface (AIDL or Messenger). Client components bind and call methods. Used for long-running operations clients want to interact with (e.g., media players, NFC readers).
- BroadcastReceiver — Components that receive system or app-wide events: battery low, network change, boot complete, custom events. Registered in Manifest (static) or at runtime (dynamic). Flutter plugins use receivers to detect system events.
- Content Providers — Structured data sharing between apps via URIs (content://authority/path). Android's gallery, contacts, and FileProvider use this pattern. Flutter's image_picker and file_picker work through Content Provider URIs.
- AndroidManifest.xml — Declares: package name, permissions (uses-permission), activities, services, receivers, providers, intent-filters. Flutter adds entries here for FCM, deep links, permissions, and custom services via the plugin system.
- Why Flutter Engineers Need This — Debugging MethodChannel errors requires understanding Android threading (main thread vs background). Diagnosing crash logs (ANR — Application Not Responding) requires Activity lifecycle knowledge. Writing plugins or fixing native issues demands Manifest and Intent knowledge.
- ANR — Application Not Responding — Android kills apps that block the main thread for 5+ seconds. Flutter runs on its own thread but Kotlin code in platform channels runs on the main thread by default — long operations there cause ANRs. Always use coroutines for async platform channel work.
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. uses-permission entries declare what system resources the app needs — missing permissions cause SecurityException at runtime
- 2. intent-filter with autoVerify='true' enables App Links — Android verifies your domain ownership before allowing deep links
- 3. android:foregroundServiceType='location' is required since Android 10 for location foreground services
- 4. START_STICKY in onStartCommand tells Android to recreate the service after killing it — critical for always-on trackers
- 5. MethodChannel name must match exactly on Dart and Kotlin sides — mismatch causes MissingPluginException
- 6. Intent.ACTION_SEND with createChooser shows the system share sheet — no need to handle individual app integrations
- 7. startForeground(id, notification) must be called within 5 seconds of service start or Android throws a ForegroundServiceStartNotAllowedException
- 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?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Android Activity Lifecycle (Android Docs)
- Background Execution Limits (Android 8+) (Android Docs)
- Services Overview (Android Docs)
- Flutter Platform Channels (Flutter Docs)
- AndroidManifest.xml reference (Android Docs)