0% found this document useful (0 votes)
27 views42 pages

Flutter Platform Channels Explained

The document provides an overview of Flutter platform channels, focusing on the communication between Flutter and native code for iOS and Android. It details three types of channels: Method Channel for one-time requests, Event Channel for continuous updates, and Pigeon for type-safe code generation. The document includes implementation steps, error handling, supported data types, and a comparison of the channels, along with a project architecture outline.

Uploaded by

Ayush Singh
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
27 views42 pages

Flutter Platform Channels Explained

The document provides an overview of Flutter platform channels, focusing on the communication between Flutter and native code for iOS and Android. It details three types of channels: Method Channel for one-time requests, Event Channel for continuous updates, and Pigeon for type-safe code generation. The document includes implementation steps, error handling, supported data types, and a comparison of the channels, along with a project architecture outline.

Uploaded by

Ayush Singh
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

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!

You might also like