FLUTTER PLATFORM CHANNELS - INTERVIEW
NOTES
Project: method_channel_learning
Purpose: Native Communication (Flutter ↔ iOS/Android)
PAGE 1: INTRODUCTION & BASICS
What are Platform Channels?
Definition:
Platform Channels = Communication bridge between Flutter (Dart) and Native code
(iOS/Android)
Why do we need them?
Access platform-specific APIs (battery, sensors, camera)
Use existing native libraries/SDKs
Access hardware features not in Flutter
Call native system services
Simple Diagram to Draw:
┌──────────────────────┐
│ Flutter (Dart) │
│ Your App Code │
└──────────┬───────────┘
│
Platform Channel
(Communication)
│
┌──────────┴───────────┐
│ Native Platform │
│ iOS | Android │
└──────────────────────┘
Three Types of Channels:
1. Method Channel - Request/Response (Pull Pattern)
2. Event Channel - Stream/Continuous Updates (Push Pattern)
3. Pigeon - Type-Safe Code Generation (Pull Pattern)
PAGE 2: METHOD CHANNEL (Manual Implementation)
What is Method Channel?
Pattern: Request-Response (like HTTP)
Direction: Bidirectional (Flutter ↔ Native)
Use Case: One-time calls (get battery, open settings, etc.)
How it Works - Flow Diagram:
Flutter Native
│ │
│ 1. invokeMethod() │
├─────────────────────────>│
│ │ 2. Process
│ │ 3. Get data
│ │
│ 4. Return result │
│<─────────────────────────┤
│ │
│ 5. Use data │
Channel Name Rule:
CRITICAL: Must be EXACTLY same on both sides!
Good:
Flutter: MethodChannel('battery_method_channel')
Native: "battery_method_channel"
Bad:
Flutter: 'battery_channel'
Native: 'batteryChannel' // Won't work!
Implementation Steps:
STEP 1: Flutter Side (Dart)
// Create channel
static const MethodChannel _channel =
MethodChannel('battery_method_channel');
// Call native method
static Future<int> getBatteryLevel() async {
final int result = await _channel.invokeMethod('getBatteryLevel');
return result;
}
STEP 2: Android Side (Kotlin)
// Create channel
val batteryChannel = MethodChannel(
[Link],
"battery_method_channel"
)
// Handle method calls
[Link] { call, result ->
if ([Link] == "getBatteryLevel") {
val batteryManager = getSystemService(BATTERY_SERVICE) as BatteryManager
val level = [Link](
BatteryManager.BATTERY_PROPERTY_CAPACITY
)
[Link](level)
} else {
[Link]()
}
}
STEP 3: iOS Side (Swift)
// Create channel
let batteryChannel = FlutterMethodChannel(
name: "battery_method_channel",
binaryMessenger: [Link]
)
// Handle method calls
[Link] { (call, result) in
if [Link] == "getBatteryLevel" {
[Link] = true
let level = Int([Link] * 100)
result(level)
} else {
result(FlutterMethodNotImplemented)
}
}
Error Handling:
Flutter:
try {
final result = await [Link]('method');
} on PlatformException catch (e) {
print('Error: ${[Link]} - ${[Link]}');
}
Native (Android):
[Link]("ERROR_CODE", "Error message", null)
Native (iOS):
result(FlutterError(code: "ERROR_CODE",
message: "Error message",
details: nil))
PAGE 3: DATA TYPES & MODELS
Supported Data Types:
Dart iOS Android
null nil (NSNull) null
bool NSNumber Boolean
int NSNumber Integer/Long
double NSNumber Double
String NSString String
List NSArray ArrayList
Map NSDictionary HashMap
Important: Only these types can cross the channel!
Our Project's Model - BatteryInfo:
class BatteryInfo {
final int level; // 0-100
final DateTime timestamp; // When retrieved
final String source; // 'method_channel' or 'pigeon'
const BatteryInfo({
required [Link],
required [Link],
required [Link],
});
}
Why use models?
Better than returning just primitives (int)
Type safety
Include metadata (timestamp, source)
Cleaner code
State Management Pattern:
Our project uses sealed class pattern for states:
// Base class
abstract class BatteryState {}
// Possible states:
class BatteryInitial extends BatteryState {}
class BatteryLoading extends BatteryState {}
class BatterySuccess extends BatteryState {
final BatteryInfo batteryInfo;
}
class BatteryError extends BatteryState {
final String message;
final String? details;
}
State Flow:
Initial → Loading → Success
↓
Error
PAGE 4: EVENT CHANNEL (Stream Implementation)
What is Event Channel?
Pattern: Stream/Push (continuous updates)
Direction: Unidirectional (Native → Flutter)
Use Case: Real-time monitoring (network, sensors, location)
How it Works - Flow Diagram:
Flutter Native
│ │
│ 1. Subscribe │
├─────────────────────────>│
│ │ 2. Start monitoring
│ │ 3. Event occurs
│ 4. Send event │
│<─────────────────────────┤
│ 5. Update UI │
│ │ 6. Another event
│ 7. Send event │
│<─────────────────────────┤
│ │
│ 8. Unsubscribe │
├─────────────────────────>│
│ │ 9. Stop monitoring
Stream Lifecycle:
3 Key Methods:
1. onListen - Flutter subscribes
Initialize resources
Start monitoring
Send initial value
2. Event Emission - Send updates
[Link](data) - send data
[Link](code, msg, details) - send error
3. onCancel - Flutter unsubscribes
Stop monitoring
Release resources
Prevent memory leaks
Implementation Steps:
STEP 1: Flutter Side (Dart)
// Create channel
static const EventChannel _channel =
EventChannel('network_event_channel');
// Get stream
static Stream<NetworkInfo> getNetworkStatusStream() {
return _channel.receiveBroadcastStream().map((event) {
return NetworkInfo(
type: _parseNetworkType(event['type']),
isConnected: event['isConnected'],
timestamp: [Link](),
);
});
}
// Use in UI with StreamBuilder
StreamBuilder<NetworkInfo>(
stream: [Link](),
builder: (context, snapshot) {
if ([Link]) {
return Text('Network: ${[Link]!.type}');
}
return CircularProgressIndicator();
},
)
STEP 2: Android Side (Kotlin)
// Create stream handler
class NetworkStreamHandler(private val context: Context)
: [Link] {
private var eventSink: [Link]? = null
private var networkCallback: NetworkCallback? = null
// Called when Flutter subscribes
override fun onListen(args: Any?, events: EventSink?) {
eventSink = events
// Start monitoring
val connectivityManager = [Link](
CONNECTIVITY_SERVICE
) as ConnectivityManager
networkCallback = object : NetworkCallback() {
override fun onAvailable(network: Network) {
val data = mapOf(
"type" to "wifi",
"isConnected" to true
)
Handler([Link]()).post {
events?.success(data)
}
}
override fun onLost(network: Network) {
val data = mapOf(
"type" to "none",
"isConnected" to false
)
Handler([Link]()).post {
events?.success(data)
}
}
}
[Link](
[Link]().build(),
networkCallback!!
)
}
// Called when Flutter unsubscribes
override fun onCancel(args: Any?) {
// IMPORTANT: Cleanup to prevent memory leaks!
networkCallback = null
eventSink = null
}
}
// Register in MainActivity
val networkEventChannel = EventChannel(
[Link],
"network_event_channel"
)
[Link](NetworkStreamHandler(this))
STEP 3: iOS Side (Swift)
// Create stream handler
class NetworkStreamHandler: NSObject, FlutterStreamHandler {
private var networkMonitor: NWPathMonitor?
private var networkQueue = DispatchQueue(label: "NetworkMonitor")
private var eventSink: FlutterEventSink?
// Called when Flutter subscribes
func onListen(withArguments args: Any?,
eventSink events: @escaping FlutterEventSink) -> FlutterError? {
[Link] = events
// Start monitoring
networkMonitor = NWPathMonitor()
networkMonitor?.pathUpdateHandler = { [weak self] path in
guard let self = self else { return }
let isConnected = [Link] == .satisfied
var networkType = "unknown"
if [Link](.wifi) {
networkType = "wifi"
} else if [Link](.cellular) {
networkType = "cellular"
} else if !isConnected {
networkType = "none"
}
let networkData: [String: Any] = [
"type": networkType,
"isConnected": isConnected
]
[Link] {
[Link]?(networkData)
}
}
networkMonitor?.start(queue: networkQueue)
return nil
}
// Called when Flutter unsubscribes
func onCancel(withArguments args: Any?) -> FlutterError? {
// IMPORTANT: Cleanup to prevent memory leaks!
networkMonitor?.cancel()
networkMonitor = nil
eventSink = nil
return nil
}
}
// Register in AppDelegate
let networkEventChannel = FlutterEventChannel(
name: "network_event_channel",
binaryMessenger: [Link]
)
[Link](NetworkStreamHandler())
Network Model:
enum NetworkType {
wifi,
cellular,
ethernet,
none,
unknown,
}
class NetworkInfo {
final NetworkType type;
final bool isConnected;
final DateTime timestamp;
const NetworkInfo({
required [Link],
required [Link],
required [Link],
});
}
PAGE 5: PIGEON (Type-Safe Code Generation)
What is Pigeon?
Definition: Code generator for type-safe platform channels
Problem it solves:
// Without Pigeon (error-prone):
final result = await [Link]('getBatery'); // typo!
final battery = result as int; // runtime cast
// With Pigeon (type-safe):
final battery = await [Link](); // autocomplete!
Benefits:
1. Type Safety - Compile-time checks
2. No String Keys - No "method name" strings
3. Auto-complete - IDE support
4. Refactoring - Safe renames across platforms
5. Documentation - Self-documenting interfaces
How Pigeon Works:
What Pigeon GENERATES:
Interface/protocol definitions (the contract)
Message passing code (plumbing underneath)
Communication layer setup
What YOU IMPLEMENT:
The interface methods in MainActivity/AppDelegate
Actual business logic
Register implementation with setUp()
Setup Steps:
1. Add Dependency:
# [Link]
dev_dependencies:
pigeon: ^18.0.0
2. Define API:
// pigeons/battery_api.dart
import 'package:pigeon/[Link]';
@HostApi() // Flutter calls Native
abstract class BatteryApi {
@async
int getBatteryLevel();
}
3. Generate Code:
flutter pub run pigeon \
--input pigeons/battery_api.dart \
--dart_out lib/pigeon_generated.dart \
--objc_header_out ios/Runner/PigeonGenerated.h \
--objc_source_out ios/Runner/PigeonGenerated.m \
--kotlin_out android/.../[Link]
Implementation:
Flutter Side:
import 'pigeon_generated.dart';
final api = BatteryApi();
final batteryLevel = await [Link]();
// Type-safe! Autocomplete! No strings!
Android Side:
// MainActivity implements generated interface
class MainActivity : FlutterActivity(), BatteryApi {
override fun configureFlutterEngine(engine: FlutterEngine) {
[Link](engine)
// Register this as implementation
[Link]([Link], this)
}
// Implement interface method
override fun getBatteryLevel(callback: (Result<Long>) -> Unit) {
val batteryManager = getSystemService(BATTERY_SERVICE) as BatteryManager
val level = [Link](
BatteryManager.BATTERY_PROPERTY_CAPACITY
)
callback([Link]([Link]()))
}
}
iOS Side:
// AppDelegate conforms to generated protocol
class AppDelegate: FlutterAppDelegate, BatteryApi {
override func application(...) -> Bool {
let controller = window?.rootViewController as! FlutterViewController
// Register this as implementation
SetUpBatteryApi(binaryMessenger: [Link], api: self)
return [Link](...)
}
// Implement protocol method
func getBatteryLevelWithCompletion(_ completion: @escaping (NSNumber?,
FlutterError?) -> Void) {
[Link] = true
let level = Int([Link] * 100)
completion(NSNumber(value: level), nil)
}
}
Pigeon Annotations:
@HostApi() - Flutter calls Native (most common)
@FlutterApi() - Native calls Flutter
@async - Asynchronous method
PAGE 6: COMPARISON TABLE
Method Channel vs Event Channel vs Pigeon
Feature Method Channel Event Channel Pigeon
Pattern Pull (Request) Push (Stream) Pull (Request)
Direction Bidirectional Native→Flutter Bidirectional
Use Case One-time calls Continuous One-time calls
Type Safety Runtime Runtime Compile
Code Gen Manual Manual Automatic
Complexity Low Medium Low (setup)
Best For Simple APIs Monitoring Production
Pull vs Push Pattern:
Pull Pattern (Method Channel/Pigeon):
// User triggers action
onPressed: () async {
final data = await [Link]('getData');
setState(() => _data = data);
}
Push Pattern (Event Channel):
// Automatic updates
StreamBuilder(
stream: [Link](),
builder: (context, snapshot) {
return Text([Link]);
},
)
Decision Tree:
Need continuous updates?
├─ YES → Event Channel
│ (sensors, network, location)
│
└─ NO → Need type safety?
├─ YES → Pigeon
│ (production app)
│
└─ NO → Method Channel
(prototype/learning)
Real-World Use Cases:
Method Channel:
Get device info (once)
Open settings
Share content
Get battery level
Event Channel:
Network monitoring
Sensor data (accelerometer)
Location tracking
Bluetooth changes
Pigeon:
Complex plugin
Production APIs
Payment processing
Team projects
PAGE 7: OUR PROJECT ARCHITECTURE
File Structure:
lib/
├── models/ # Data models
│ ├── battery_info.dart # Battery data
│ ├── battery_state.dart # Battery states
│ ├── network_info.dart # Network data
│ └── network_state.dart # Network states
│
├── channels/ # Platform helpers
│ ├── method_channel_helper.dart
│ ├── pigeon_channel_helper.dart
│ └── event_channel_helper.dart
│
├── widgets/ # UI components
│ ├── battery_level_screen.dart
│ ├── battery_display_builder.dart
│ └── network_status_screen.dart
│
├── pigeon_generated.dart # Generated code
└── [Link] # Entry point
android/app/src/main/kotlin/.../
└── [Link] # Android native
ios/Runner/
├── [Link] # iOS native
└── PigeonGenerated.h/m # Generated code
App Structure:
MyApp
└─ MainTabScreen (DefaultTabController)
├─ Tab 1: Battery (Pull)
│ ├─ Method Channel section
│ └─ Pigeon section
│
└─ Tab 2: Network (Live)
└─ Event Channel section
State Management Used:
1. For Battery (Pull): ValueNotifier + ValueListenableBuilder
Lightweight
No StatefulWidget needed
Perfect for one-time requests
2. For Network (Live): StreamBuilder
Automatic stream handling
Auto cleanup on dispose
Perfect for continuous updates
Key Design Decisions:
All widgets are stateless
Uses ValueNotifier instead of setState
More testable
Cleaner code
Models over primitives
Return BatteryInfo, not just int
Include metadata
Better type safety
Sealed classes for states
Type-safe state management
Exhaustive pattern matching
Clear state transitions
Separation of concerns
Models in models/
Channel helpers in channels/
UI in widgets/
Clean architecture
PAGE 8: BEST PRACTICES
1. Channel Names
// Good: Descriptive, unique
MethodChannel('[Link]/battery')
EventChannel('[Link]/network_events')
// Bad: Generic, collision-prone
MethodChannel('channel')
EventChannel('stream')
RULE: Must be EXACTLY same on both sides!
2. Error Handling
// Always wrap in try-catch
try {
final result = await [Link]('method');
return [Link](result);
} on PlatformException catch (e) {
// Handle platform errors
throw CustomException([Link]);
} catch (e) {
// Handle other errors
throw UnknownException(e);
}
3. Stream Cleanup
// StreamBuilder handles cleanup automatically
StreamBuilder(
stream: [Link](),
builder: (context, snapshot) { ... },
)
// Manual subscription - must cancel!
late StreamSubscription subscription;
subscription = [Link]((data) { ... });
// Don't forget: [Link]() in dispose!
4. Thread Safety
iOS:
// Send events on main thread
[Link] {
[Link]?(data)
}
Android:
// Use main looper for UI updates
Handler([Link]()).post {
events?.success(data)
}
5. Memory Management
Always cleanup in onCancel:
// Android
override fun onCancel(args: Any?) {
networkCallback?.let { callback ->
connectivityManager?.unregisterNetworkCallback(callback)
}
networkCallback = null
eventSink = null
}
// iOS
func onCancel(withArguments args: Any?) -> FlutterError? {
networkMonitor?.cancel()
networkMonitor = nil
eventSink = nil
return nil
}
6. Return Models, Not Primitives
// Bad: Return primitive
Future<int> getBatteryLevel() { ... }
// Good: Return model
Future<BatteryInfo> getBatteryLevel() {
return BatteryInfo(
level: result,
timestamp: [Link](),
source: 'method_channel',
);
}
PAGE 9: COMMON PITFALLS & SOLUTIONS
Pitfall 1: Channel Name Mismatch
// Different names
// Dart: 'battery_channel'
// Native: 'batteryChannel'
// Exact match
Both: 'battery_method_channel'
Symptom: Method calls fail silently
Fix: Copy-paste channel name, don't type manually
Pitfall 2: Forgetting Cleanup
// Memory leak
func onCancel(...) -> FlutterError? {
return nil // Forgot to stop monitoring!
}
// Proper cleanup
func onCancel(...) -> FlutterError? {
networkMonitor?.cancel()
networkMonitor = nil
eventSink = nil
return nil
}
Symptom: Memory leaks, crashes
Fix: Always cleanup resources in onCancel
Pitfall 3: Wrong Thread
// Can crash
override fun onAvailable(network: Network) {
events?.success(data) // Wrong thread!
}
// Use main thread
override fun onAvailable(network: Network) {
Handler([Link]()).post {
events?.success(data)
}
}
Symptom: Crashes, UI not updating
Fix: Always send events on main/UI thread
Pitfall 4: Not Handling Errors
// Unhandled error crashes app
final data = await [Link]('method');
// Always catch errors
try {
final data = await [Link]('method');
} on PlatformException catch (e) {
print('Error: ${[Link]} - ${[Link]}');
}
Symptom: App crashes on errors
Fix: Always use try-catch with PlatformException
Pitfall 5: Casting Errors
// Runtime error
final int level = await [Link]('get');
// Safe casting
final int? level = await [Link]('get');
final int safeLevel = level ?? 0;
Symptom: Type cast errors
Fix: Use nullable types, provide defaults
Pitfall 6: Not Enabling Battery Monitoring (iOS)
// Returns -1.0 on iOS
let level = [Link]
// Enable first
[Link] = true
let level = [Link]
Symptom: Always returns -1 on iOS
Fix: Enable battery monitoring before reading
PAGE 10: INTERVIEW Q&A - PART 1
Q1: How does Flutter communicate with native code?
Answer: Flutter uses Platform Channels for bidirectional communication.
Three types:
1. Method Channels - Request-response, one-time calls
2. Event Channels - Stream-based, continuous updates
3. Basic Message Channels - Low-level (rarely used)
How it works:
Data serialized using StandardMessageCodec
Supports basic types (int, String, List, Map)
Communication through platform embedder layer
Example:
// Flutter → Native
final result = await [Link]('getData');
// Native → Flutter (Event Channel)
eventSink(data)
Q2: What's the difference between Method Channel and Event Channel?
Answer:
Aspect Method Channel Event Channel
Pattern Pull (Request) Push (Stream)
Direction Bidirectional Unidirectional
Calls One-time Continuous
Use Case Get battery once Monitor network live
Method Channel:
User triggers action
Flutter waits for response
Good for: device info, open settings, share
Event Channel:
Native sends continuous updates
Flutter listens to stream
Good for: sensors, network, location, Bluetooth
Simple rule: Need live updates? Event Channel. One-time? Method Channel.
Q3: Why use Pigeon over raw Method Channels?
Answer:
Pigeon provides:
1. Type Safety - Compile-time vs runtime
// Without Pigeon: typo → runtime crash
await [Link]('getBatery'); // typo!
// With Pigeon: typo → compile error await [Link](); // ← IDE shows error
2. **No String Keys** - No "method name" strings
```dart
// Without: error-prone strings
if ([Link] == "getBatteryLevel")
// With: type-safe interface
override fun getBatteryLevel(callback: ...)
3. Autocomplete - IDE support
4. Refactoring - Safe renames across platforms
5. Documentation - Self-documenting
6. Null Safety - Proper handling
Trade-off: Additional build step
When to use:
Production apps
Complex APIs
Team projects
Quick prototypes
Q4: How do you handle errors in platform channels?
Answer:
Flutter Side:
try {
final result = await [Link]('method');
return result;
} on PlatformException catch (e) {
// [Link] - Error code from native
// [Link] - Human-readable message
// [Link] - Additional info
print('Error: ${[Link]} - ${[Link]}');
}
Native Side (iOS):
result(FlutterError(
code: "UNAVAILABLE",
message: "Battery info unavailable",
details: nil
))
Native Side (Android):
[Link](
"UNAVAILABLE",
"Battery info unavailable",
null
)
Best practice: Always catch PlatformException!
Q5: What are thread safety considerations?
Answer:
Key Points:
1. iOS: Callbacks on main thread by default
2. Android: Use main looper for UI updates
3. Heavy work: Move to background thread
4. Event Channels: Ensure eventSink is thread-safe
iOS Example:
// Background work
[Link]().async {
let data = heavyComputation()
// Send on main thread
[Link] {
[Link]?(data)
}
}
Android Example:
// Send on main thread
Handler([Link]()).post {
events?.success(data)
}
Why?
Flutter UI runs on main thread
Cross-thread access can cause crashes
Always send events on main/UI thread
PAGE 11: INTERVIEW Q&A - PART 2
Q6: How does serialization work in platform channels?
Answer:
StandardMessageCodec - Automatic serialization
Supported types:
null, bool, int, double, String
List, Map, typed data (Uint8List)
Binary format for efficiency
Automatic Dart ↔ Native conversion
For complex objects:
class User {
final String name;
final int age;
// Convert to supported types
Map<String, dynamic> toMap() => {
'name': name,
'age': age,
};
// Convert from Map
factory [Link](Map<String, dynamic> map) => User(
name: map['name'],
age: map['age'],
);
}
// Use it
final userMap = [Link]();
await [Link]('saveUser', userMap);
Important: Custom types must be broken down to supported types!
Q7: When should you use each approach?
Answer:
Method Channel when:
One-time data fetch (device info, battery once)
Trigger actions (open settings, share)
Simple, infrequent calls
Request-response pattern
Event Channel when:
Continuous monitoring (network, sensors)
Real-time updates (location, notifications)
State change streams
High-frequency updates
Pigeon when:
Production applications
Complex APIs with many methods
Team development (prevents errors)
Need type safety and refactoring
Skip for quick prototypes
Example decision:
Battery level once → Method Channel
Battery changes live → Event Channel
Complex payment API → Pigeon
Q8: What are common pitfalls?
Answer:
Top 7 Pitfalls:
1. Channel name mismatch - Must be identical
2. Not handling errors - Always catch PlatformException
3. Memory leaks - Forget cleanup in onCancel
4. Wrong thread - UI updates on wrong thread
5. Type mismatches - Runtime casting errors
6. Over-communication - Too many channel calls
7. Not testing - Assume it works without testing
Most common: Channel name mismatch
Flutter: 'battery_channel'
Native: 'batteryChannel'
Result: Silent failure!
Fix: Always copy-paste channel names!
Q9: How do you test platform channel code?
Answer:
Mock the channel:
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/[Link]';
void main() {
[Link]();
const channel = MethodChannel('test_channel');
setUp(() {
// Set mock handler
[Link]
.defaultBinaryMessenger
.setMockMethodCallHandler(channel,
(MethodCall call) async {
if ([Link] == 'getData') {
return {'data': 'test'};
}
return null;
}
);
});
tearDown(() {
// Clear handler
[Link]
.defaultBinaryMessenger
.setMockMethodCallHandler(channel, null);
});
test('getBatteryLevel returns data', () async {
final result = await [Link]();
expect(result, 'test');
});
}
Key points:
Mock the channel in tests
Test all error cases
Test on real devices too
Q10: How do you handle platform-specific behavior?
Answer:
Check platform at runtime:
import 'dart:io';
if ([Link]) {
// iOS-specific code
print('Running on iOS');
} else if ([Link]) {
// Android-specific code
print('Running on Android');
}
In channel helper:
Future<String> getPlatformVersion() async {
try {
final version = await [Link]('getVersion');
return version;
} catch (e) {
if ([Link]) {
return 'iOS error: $e';
} else {
return 'Android error: $e';
}
}
}
Return different data per platform:
Map<String, dynamic> getPlatformData() {
if ([Link]) {
return {'platform': 'iOS', 'minVersion': '12.0'};
} else {
return {'platform': 'Android', 'minVersion': '21'};
}
}
PAGE 12: CODE FLOW IN OUR PROJECT
Method Channel Flow (Battery):
STEP 1: UI Layer
├─ User clicks "Get Battery Level" button
├─ battery_level_screen.dart
└─ _fetchBatteryLevel() called
STEP 2: Helper Layer
├─ method_channel_helper.dart
├─ [Link]()
├─ invokeMethod('getBatteryLevel')
└─ Waits for response
STEP 3: Native Layer
├─ Android: [Link]
│ ├─ setMethodCallHandler receives call
│ ├─ getBatteryLevel() gets battery
│ └─ [Link](level)
│
└─ iOS: [Link]
├─ setMethodCallHandler receives call
├─ [Link]
└─ result(level)
STEP 4: Back to Flutter
├─ Helper receives result
├─ Wraps in BatteryInfo model
├─ Returns to UI
└─ UI updates with ValueNotifier
Event Channel Flow (Network):
STEP 1: UI Layer
├─ StreamBuilder subscribes
├─ network_status_screen.dart
└─ getNetworkStatusStream() called
STEP 2: Helper Layer
├─ event_channel_helper.dart
├─ [Link]()
├─ receiveBroadcastStream()
└─ Triggers native onListen
STEP 3: Native Layer - onListen
├─ Android: NetworkStreamHandler
│ ├─ onListen() called
│ ├─ Start ConnectivityManager monitoring
│ └─ Register NetworkCallback
│
└─ iOS: NetworkStreamHandler
├─ onListen() called
├─ Start NWPathMonitor
└─ Set pathUpdateHandler
STEP 4: Continuous Updates
├─ Network changes detected
├─ Native sends event via eventSink
├─ Helper maps to NetworkInfo
├─ StreamBuilder receives update
└─ UI rebuilds automatically
STEP 5: Cleanup (when widget disposes)
├─ StreamBuilder unsubscribes
├─ Native onCancel() called
├─ Stop monitoring
└─ Release resources
Pigeon Flow (Battery):
STEP 1: UI Layer
├─ User clicks "Get Battery Level" button
├─ battery_level_screen.dart
└─ _fetchBatteryLevel() called
STEP 2: Helper Layer
├─ pigeon_channel_helper.dart
├─ [Link]()
└─ _api.getBatteryLevel() (type-safe!)
STEP 2.5: Generated Code
├─ pigeon_generated.dart (Flutter)
├─ Handles serialization
├─ Sends message via BasicMessageChannel
└─ Waits for response
STEP 3: Native Layer - Generated Code
├─ Android: [Link]
│ ├─ Receives message
│ ├─ Deserializes
│ └─ Calls our implementation
│
└─ iOS: PigeonGenerated.h/m
├─ Receives message
├─ Deserializes
└─ Calls our implementation
STEP 3.5: Native Implementation
├─ Android: [Link]()
│ ├─ Gets battery from BatteryManager
│ └─ callback([Link](level))
│
└─ iOS: [Link]()
├─ Gets battery from UIDevice
└─ completion(NSNumber(level), nil)
STEP 4: Back to Flutter
├─ Generated code serializes result
├─ Helper receives result
├─ Wraps in BatteryInfo model
├─ Returns to UI
└─ UI updates with ValueNotifier
PAGE 13: STATE MANAGEMENT IN OUR PROJECT
ValueNotifier Pattern (Battery - Pull):
// Create notifier
final stateNotifier = ValueNotifier<BatteryState>(
const BatteryInitial()
);
// Listen to changes
ValueListenableBuilder<BatteryState>(
valueListenable: stateNotifier,
builder: (context, state, child) {
// Rebuild when state changes
return BatteryDisplayBuilder(
state: state,
onRefresh: () => _fetchBatteryLevel(notifier),
);
},
)
// Update state
[Link] = const BatteryLoading();
[Link] = BatterySuccess(batteryInfo);
[Link] = BatteryError(message);
Why ValueNotifier?
No StatefulWidget needed
Simple and lightweight
Perfect for one-time requests
Each section independent
StreamBuilder Pattern (Network - Push):
// Get stream
Stream<NetworkInfo> stream =
[Link]();
// Listen to stream
StreamBuilder<NetworkInfo>(
stream: stream,
builder: (context, snapshot) {
if ([Link] == [Link]) {
return CircularProgressIndicator();
}
if ([Link]) {
return Text('Error: ${[Link]}');
}
if ([Link]) {
return NetworkDisplay(info: [Link]!);
}
return Text('No data');
},
)
Why StreamBuilder?
Automatic stream subscription
Auto cleanup on dispose
Perfect for continuous updates
Handles all stream states
State Transitions:
Battery (Sealed Class):
BatteryInitial
↓ (user clicks button)
BatteryLoading
↓ (success) ↓ (error)
BatterySuccess BatteryError
Network (Stream States):
[Link]
↓ (subscribe)
[Link]
↓ (first event)
[Link]
↓ (more events)
[Link]
↓ (dispose)
[Link]
PAGE 14: NATIVE PLATFORM DETAILS
Android Battery (BatteryManager):
// Get system service
val batteryManager = getSystemService(
Context.BATTERY_SERVICE
) as BatteryManager
// Get battery level (0-100)
val level = [Link](
BatteryManager.BATTERY_PROPERTY_CAPACITY
)
// Returns -1 if unavailable
if (level != -1) {
[Link](level)
} else {
[Link]("UNAVAILABLE", "Battery not available", null)
}
Key points:
No monitoring needed (unlike iOS)
Returns int 0-100 directly
Returns -1 if unavailable
iOS Battery (UIDevice):
// MUST enable monitoring first!
[Link] = true
// Get battery level (0.0-1.0)
let batteryLevelFloat = [Link]
// Convert to percentage (0-100)
let batteryLevel = Int(batteryLevelFloat * 100)
// Returns -1.0 if unavailable
if batteryLevelFloat >= 0 {
result(batteryLevel)
} else {
result(FlutterError(code: "UNAVAILABLE", ...))
}
Key points:
MUST enable monitoring first
Returns Float 0.0-1.0 (need to multiply by 100)
Returns -1.0 if unavailable
Android Network (ConnectivityManager):
// Get system service
val connectivityManager = [Link](
Context.CONNECTIVITY_SERVICE
) as ConnectivityManager
// Get active network
val network = [Link]
val capabilities = connectivityManager
.getNetworkCapabilities(network)
// Check connection
val isConnected = capabilities?.hasCapability(
NetworkCapabilities.NET_CAPABILITY_INTERNET
) ?: false
// Detect type
val type = when {
capabilities?.hasTransport(TRANSPORT_WIFI) == true -> "wifi"
capabilities?.hasTransport(TRANSPORT_CELLULAR) == true -> "cellular"
capabilities?.hasTransport(TRANSPORT_ETHERNET) == true -> "ethernet"
else -> "unknown"
}
Key points:
NetworkCallback for automatic updates (API 24+)
Main thread for eventSink
Unregister callback in onCancel
iOS Network (NWPathMonitor):
// Create monitor
let networkMonitor = NWPathMonitor()
let networkQueue = DispatchQueue(label: "NetworkMonitor")
// Set handler
[Link] = { [weak self] path in
// Check connection
let isConnected = [Link] == .satisfied
// Detect type
var networkType = "unknown"
if [Link](.wifi) {
networkType = "wifi"
} else if [Link](.cellular) {
networkType = "cellular"
} else if [Link](.wiredEthernet) {
networkType = "ethernet"
}
// Send on main thread
[Link] {
self?.eventSink?(networkData)
}
}
// Start monitoring
[Link](queue: networkQueue)
Key points:
Available iOS 12.0+
Background queue for monitoring
Main thread for eventSink
Cancel in onCancel
PAGE 15: PERFORMANCE & OPTIMIZATION
Performance Considerations:
1. Minimize Channel Calls
Each call has overhead
Batch data when possible
// Bad: Multiple calls
await [Link]('getName');
await [Link]('getAge');
await [Link]('getEmail');
// Good: Single call with all data final user = await
[Link]('getUserData');
**2. Use Appropriate Pattern**
```dart
// Bad: Polling with Method Channel
[Link](Duration(seconds: 1), (timer) {
getBatteryLevel(); // Expensive!
});
// Good: Event Channel for continuous
StreamBuilder(
stream: batteryStream, // Efficient!
builder: ...
);
3. Avoid UI Thread Blocking
// Bad: Heavy work on main thread
[Link] { call, result ->
val data = heavyComputation() // Blocks UI!
[Link](data)
}
// Good: Background thread
[Link] { call, result ->
CoroutineScope([Link]).launch {
val data = heavyComputation()
withContext([Link]) {
[Link](data)
}
}
}
4. Proper Cleanup
// Always cleanup in onCancel
func onCancel(withArguments args: Any?) -> FlutterError? {
networkMonitor?.cancel()
networkMonitor = nil
eventSink = nil
return nil
}
5. Cache When Possible
// Cache static data
class DeviceInfo {
static String? _cachedModel;
static Future<String> getModel() async {
if (_cachedModel != null) {
return _cachedModel!;
}
_cachedModel = await [Link]('getModel');
return _cachedModel!;
}
}
Memory Management:
Important rules:
1. Unregister callbacks in onCancel
2. Set references to null
3. Use weak references [weak self] (iOS)
4. Cancel timers/monitors
5. Clear event sinks
Memory leak example:
// Memory leak
override fun onCancel(args: Any?) {
// Forgot to unregister callback!
// NetworkCallback keeps running forever
}
// Proper cleanup
override fun onCancel(args: Any?) {
networkCallback?.let { callback ->
connectivityManager?.unregisterNetworkCallback(callback)
}
networkCallback = null
eventSink = null
}
PAGE 16: DEBUGGING & TROUBLESHOOTING
Common Issues & Solutions:
1. Method not called (Silent Failure)
Symptom: No error, no result
Possible causes:
Channel name mismatch ← Most common!
Method name typo
Handler not registered
Debug:
// Add logging
print('Calling method: getBatteryLevel');
final result = await [Link]('getBatteryLevel');
print('Result: $result');
// Add logging
[Link] { call, result ->
Log.d("TAG", "Method called: ${[Link]}")
// ...
}
2. Type Cast Error
Symptom: "type 'Null' is not a subtype of type 'int'"
Fix:
// Assumes non-null
final int level = await [Link]('get');
// Handle null
final int? level = await [Link]('get');
final int safeLevel = level ?? 0;
3. Stream Not Receiving Events
Symptom: StreamBuilder shows waiting forever
Possible causes:
Channel name mismatch
Handler not registered
onListen not called
Events sent on wrong thread
Debug:
// Add logging
override fun onListen(args: Any?, events: EventSink?) {
Log.d("TAG", "onListen called")
// Send test event
events?.success(mapOf("test" to "data"))
}
4. Memory Leak
Symptom: App gets slower, crashes
Cause: Forgot cleanup in onCancel
Fix: Always cleanup!
override fun onCancel(args: Any?) {
Log.d("TAG", "onCancel called")
// Unregister everything
networkCallback = null
eventSink = null
}
5. Platform-Specific Crashes
iOS specific:
// Forgot to enable
let level = [Link] // -1.0
// Enable first
[Link] = true
let level = [Link]
Android specific:
// Missing permission
<uses-permission android:name="[Link].ACCESS_NETWORK_STATE"/>
Debugging Tools:
1. Flutter DevTools
Check method calls
Monitor streams
View memory usage
2. Platform Logs
# Android
adb logcat | grep TAG
# iOS
xcrun simctl spawn booted log stream --level=debug
3. Add Breakpoints
Flutter: In VS Code/Android Studio
Android: In Android Studio
iOS: In Xcode
PAGE 17: QUICK REFERENCE CARD
Method Channel Cheat Sheet:
Flutter:
static const MethodChannel _channel =
MethodChannel('channel_name');
// Call
final result = await _channel.invokeMethod('methodName');
// Call with args
final result = await _channel.invokeMethod('methodName', {
'arg1': value1,
'arg2': value2,
});
Android:
val channel = MethodChannel(
[Link],
"channel_name"
)
[Link] { call, result ->
when ([Link]) {
"methodName" -> {
[Link](data)
// or [Link]("CODE", "message", details)
}
else -> [Link]()
}
}
iOS:
let channel = FlutterMethodChannel(
name: "channel_name",
binaryMessenger: [Link]
)
[Link] { (call, result) in
if [Link] == "methodName" {
result(data)
// or result(FlutterError(...))
} else {
result(FlutterMethodNotImplemented)
}
}
Event Channel Cheat Sheet:
Flutter:
static const EventChannel _channel =
EventChannel('channel_name');
// Subscribe
Stream<T> getStream() {
return _channel.receiveBroadcastStream()
.map((event) => parseEvent(event));
}
Android:
class StreamHandler : [Link] {
private var eventSink: [Link]? = null
override fun onListen(args: Any?, events: EventSink?) {
eventSink = events
// Start monitoring
// Send: events?.success(data)
}
override fun onCancel(args: Any?) {
// Stop monitoring
eventSink = null
}
}
val channel = EventChannel(
[Link],
"channel_name"
)
[Link](StreamHandler())
iOS:
class StreamHandler: NSObject, FlutterStreamHandler {
private var eventSink: FlutterEventSink?
func onListen(withArguments args: Any?,
eventSink events: @escaping FlutterEventSink)
-> FlutterError? {
[Link] = events
// Start monitoring
// Send: eventSink?(data)
return nil
}
func onCancel(withArguments args: Any?) -> FlutterError? {
// Stop monitoring
eventSink = nil
return nil
}
}
let channel = FlutterEventChannel(
name: "channel_name",
binaryMessenger: [Link]
)
[Link](StreamHandler())
PAGE 18: FINAL TIPS FOR INTERVIEW
What Interviewers Look For:
1. Understanding of Patterns
Pull vs Push
When to use which
Trade-offs
2. Error Handling
Always catch PlatformException
Proper error messages
Graceful degradation
3. Memory Management
Cleanup in onCancel
No memory leaks
Proper resource management
4. Type Safety
Use models over primitives
Null safety
Proper casting
5. Platform Knowledge
Android vs iOS differences
Platform-specific APIs
Thread safety
Key Points to Remember:
Channel Names:
Must be identical on both sides
Use reverse domain notation
Copy-paste, don't type
Error Handling:
Always use try-catch
Catch PlatformException specifically
Provide meaningful errors
Cleanup:
Always cleanup in onCancel
Set references to null
Unregister callbacks
Threading:
Send events on main thread
Heavy work on background
iOS: [Link]
Android: Handler([Link]())
Type Safety:
Use nullable types
Provide defaults
Return models, not primitives
Project Highlights to Mention:
1. Architecture:
Clean separation: models, channels, widgets
Stateless widgets with ValueNotifier
Sealed classes for type-safe states
2. Patterns:
ValueNotifier for pull (battery)
StreamBuilder for push (network)
Automatic cleanup
3. All Three Approaches:
Method Channel (manual)
Pigeon (type-safe)
Event Channel (streaming)
4. Best Practices:
Models over primitives
Proper error handling
Memory cleanup
Thread safety
Interview Preparation Checklist:
□ Can explain Platform Channels concept
□ Know differences: Method vs Event vs Pigeon
□ Understand Pull vs Push patterns
□ Can implement Method Channel from scratch
□ Can implement Event Channel from scratch
□ Know error handling best practices
□ Understand memory management
□ Know thread safety considerations
□ Can explain our project architecture
□ Know common pitfalls and solutions
□ Understand serialization (StandardMessageCodec)
□ Can explain when to use each approach
□ Know platform-specific differences (iOS vs Android)
□ Can write test cases for channels
□ Understand performance considerations
Practice Questions:
1. Explain Platform Channels in 2 minutes
2. When would you use Event Channel over Method Channel?
3. What's the biggest advantage of Pigeon?
4. Walk me through the flow of a Method Channel call
5. How do you prevent memory leaks in Event Channels?
6. What happens if channel names don't match?
7. Why do we need to enable battery monitoring on iOS?
8. How do you ensure thread safety?
9. What data types can cross the channel?
10. How would you test platform channel code?
PAGE 19: PROJECT STRUCTURE SUMMARY
Our Complete Implementation:
Three Implementations, One Goal:
1. Method Channel (Manual)
Channel: 'battery_method_channel'
Method: 'getBatteryLevel'
Returns: int (battery %)
Pattern: Pull (user clicks button)
2. Pigeon (Type-Safe)
Generated API: BatteryApi
Method: getBatteryLevel()
Returns: int (battery %)
Pattern: Pull (user clicks button)
3. Event Channel (Streaming)
Channel: 'network_event_channel'
Stream: Network status updates
Returns: Map (type, isConnected)
Pattern: Push (automatic updates)
File Checklist:
Flutter (lib/):
[Link] - Entry point, tab navigation
models/battery_info.dart - Battery data model
models/battery_state.dart - Battery states
models/network_info.dart - Network data model
channels/method_channel_helper.dart - Manual channel
channels/pigeon_channel_helper.dart - Type-safe channel
channels/event_channel_helper.dart - Streaming channel
widgets/battery_level_screen.dart - Battery UI
widgets/network_status_screen.dart - Network UI
pigeon_generated.dart - Generated Pigeon code
Android (kotlin/):
[Link] - All channel handlers
- Method Channel setup
- Pigeon interface implementation
- Event Channel setup
- NetworkStreamHandler class
[Link] - Generated Pigeon interface
iOS (swift/):
[Link] - All channel handlers
- Method Channel setup
- Pigeon protocol implementation
- Event Channel setup
- NetworkStreamHandler class
PigeonGenerated.h/m - Generated Pigeon protocol
Testing the App:
Battery Tab (Pull):
1. Click "Get Battery Level" on Method Channel
2. See battery % displayed
3. Click "Get Battery Level" on Pigeon
4. See battery % displayed
5. Both should show same level
Network Tab (Live):
1. Open Network tab
2. See current network status
3. Turn WiFi off → See update immediately
4. Turn WiFi on → See update immediately
5. No button click needed (automatic!)
PAGE 20: SUMMARY & KEY TAKEAWAYS
Platform Channels in One Page:
What: Communication bridge Flutter ↔ Native
Why: Access platform APIs, native libraries, hardware
Three Types:
1. Method Channel - Request/Response (Pull)
2. Event Channel - Stream (Push)
3. Pigeon - Type-Safe Generation
Key Concepts:
┌────────────────────────────────────┐
│ PLATFORM CHANNELS SUMMARY │
├────────────────────────────────────┤
│ │
│ METHOD CHANNEL │
│ ├─ Pull pattern │
│ ├─ One-time calls │
│ ├─ Bidirectional │
│ └─ Use: Get battery, open settings│
│ │
│ EVENT CHANNEL │
│ ├─ Push pattern │
│ ├─ Continuous stream │
│ ├─ Unidirectional (Native→Flutter)│
│ └─ Use: Network, sensors, location│
│ │
│ PIGEON │
│ ├─ Pull pattern │
│ ├─ Type-safe generation │
│ ├─ No string method names │
│ └─ Use: Production, complex APIs │
│ │
└────────────────────────────────────┘
Critical Rules:
1. Channel Names:
MUST be identical on both sides
Copy-paste, never type manually
2. Error Handling:
Always catch PlatformException
Provide meaningful errors
Handle null cases
3. Memory Management:
Always cleanup in onCancel
Unregister callbacks
Set references to null
4. Threading:
Send events on main/UI thread
iOS: [Link]
Android: Handler([Link]())
5. Type Safety:
Use models over primitives
Nullable types with defaults
Proper serialization
When to Use What:
Decision Flow:
│
├─ Need continuous updates?
│ ├─ YES → Event Channel
│ └─ NO ─┐
│ │
│ ├─ Production app with many methods?
│ │ ├─ YES → Pigeon
│ │ └─ NO → Method Channel
Interview Success Formula:
1. Understand Concepts (not just syntax)
Why Platform Channels exist
Pull vs Push patterns
When to use which
2. Know the Flow (step by step)
Flutter → Helper → Native
Native → Helper → Flutter
Stream lifecycle
3. Best Practices (what makes good code)
Error handling
Memory cleanup
Thread safety
Type safety
4. Common Pitfalls (learn from mistakes)
Channel name mismatch
Forgot cleanup
Wrong thread
Type casting errors
5. Our Project (practical example)
Three implementations
Clean architecture
Best practices applied
Real working code
Final Checklist Before Interview:
□ Can draw Platform Channel architecture
□ Can explain all three types with examples
□ Know when to use each type
□ Can implement Method Channel from memory
□ Can implement Event Channel from memory
□ Know error handling patterns
□ Understand memory management
□ Know platform differences (iOS vs Android)
□ Can explain our project architecture
□ Practiced answering all Q&A questions
Resources in This Project:
[Link] - Complete theory guide
Working code - All three implementations
These notes - Interview preparation
Comments in code - Step-by-step flow
Good luck with your interview!
Remember: Interviewers value UNDERSTANDING over memorization.
Explain the "why", not just the "how".
END OF NOTES
These notes cover everything you need for Flutter Platform Channels interview
preparation. Review these before your interview, and you'll be ready to answer any
question confidently!