When building Flutter apps, sometimes Dart alone isn't enough. You may need native functionality—like accessing sensors, battery info, Bluetooth, or a custom SDK.
That's where Platform Channels (a.k.a. Native Channels) come in. They allow Flutter to communicate with the host platform (Android/iOS, desktop, etc.) and execute native code seamlessly.
🏗 What Are Platform Channels?
Flutter runs on its own engine with Dart, but you can bridge to the host platform when needed.
Platform channels send asynchronous messages between:
- Flutter (Dart side)
- Host platform (Kotlin/Java for Android, Swift/Objective-C for iOS, etc.)
👉 Think of it as a two-way communication bridge.
⚡ Types of Platform Channels
Flutter provides three types of channels:
1. MethodChannel
✅ Best for one-time operations.
- Purpose: Invoke a native method and get a response back.
- Pattern: Request → Response (like a function call).
Use Cases:
- Fetching device info (battery, OS version, etc.)
- Triggering a single action (open camera, share a file)
2. EventChannel
✅ Best for continuous streams of data.
- Purpose: Subscribe once and keep receiving updates.
- Pattern: Stream of events over time.
Use Cases:
- Sensors (accelerometer, gyroscope, pedometer)
- System updates (battery state, connectivity changes)
3. BasicMessageChannel
✅ Best for flexible, bi-directional messaging.
- Purpose: Send raw, asynchronous messages (text, JSON, binary) back and forth.
- Pattern: Free-form communication (no fixed method names).
Use Cases:
- Continuous two-way messaging
- Custom protocols (status updates, commands)
💻 Platforms & Native Languages
Flutter supports multiple platforms and integrates with their native languages:
- Android → Kotlin, Java
- iOS → Swift, Objective-C
- Windows → C++
- macOS → Objective-C
- Linux → C
🧑💻 Quick Examples
Battery Level (MethodChannel)
// Flutter side
static const platform = MethodChannel('battery');
Future<void> getBatteryLevel() async {
final int level = await platform.invokeMethod('getBatteryLevel');
print("Battery level: $level%");
}
// Android side
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "battery")
.setMethodCallHandler { call, result ->
if (call.method == "getBatteryLevel") {
val batteryLevel = getBatteryPercentage()
result.success(batteryLevel)
}
}
Live Chat (BasicMessageChannel)
static const messageChannel = BasicMessageChannel<String>('chat', StringCodec());
void sendMessage(String msg) {
messageChannel.send(msg);
}
void listenMessages() {
messageChannel.setMessageHandler((msg) async {
print("New message: $msg");
});
}
🚶♂️ Real-World Implementation: Background Step Counter
Let's build a production-ready step counter that demonstrates MethodChannel, EventChannel, and security validation working together. This app features:
- Start/stop a native foreground service (MethodChannel)
- Stream live step updates to Flutter (EventChannel)
- Input validation and data sanitization (Security)
- Handle permissions and sensor fallbacks
- Work in the background even when app is minimized
Architecture Overview
Flutter (Dart) Android (Kotlin)
┌─────────────────┐ ┌──────────────────────┐
│ Start/Stop UI │────▶│ MainActivity │
│ │ │ - MethodChannel │
├─────────────────┤ │ - EventChannel │
│ Step Display │◀────│ - SecurityValidator │
│ (Real-time) │ ├──────────────────────┤
└─────────────────┘ │ StepCounterService │
│ - Foreground Service │
│ - Sensor Manager │
│ - Step Detection │
└──────────────────────┘
Flutter Implementation
Data Model
First, let's define our step data structure:
// lib/features/counter_steps/models/step_data.dart
class StepData {
final int steps;
final DateTime timestamp;
final String sensorType;
final String accuracy;
StepData({
required this.steps,
required this.timestamp,
required this.sensorType,
required this.accuracy,
});
factory StepData.fromMap(Map<String, dynamic> map) {
return StepData(
steps: map['steps'] as int? ?? 0,
timestamp: DateTime.fromMillisecondsSinceEpoch(
map['timestamp'] as int? ?? DateTime.now().millisecondsSinceEpoch
),
sensorType: map['sensor_type'] as String? ?? 'unknown',
accuracy: map['accuracy'] as String? ?? 'medium',
);
}
Map<String, dynamic> toMap() {
return {
'steps': steps,
'timestamp': timestamp.millisecondsSinceEpoch,
'sensor_type': sensorType,
'accuracy': accuracy,
};
}
}
Error Handling
Create proper error types for better UX:
// lib/features/counter_steps/services/platform_channel_error.dart
enum PlatformChannelErrorType {
permissionDenied,
sensorUnavailable,
serviceUnavailable,
invalidData,
unknownError
}
class PlatformChannelError extends Error {
final PlatformChannelErrorType type;
final String message;
final String? code;
final dynamic details;
PlatformChannelError({
required this.type,
required this.message,
this.code,
this.details
});
factory PlatformChannelError.fromPlatformException(PlatformException e) {
PlatformChannelErrorType type;
switch (e.code) {
case 'PERMISSION_DENIED':
type = PlatformChannelErrorType.permissionDenied;
break;
case 'SENSOR_UNAVAILABLE':
type = PlatformChannelErrorType.sensorUnavailable;
break;
case 'SERVICE_UNAVAILABLE':
type = PlatformChannelErrorType.serviceUnavailable;
break;
default:
type = PlatformChannelErrorType.unknownError;
}
return PlatformChannelError(
type: type,
message: e.message ?? 'Unknown platform error',
code: e.code,
details: e.details
);
}
String get userFriendlyMessage {
switch (type) {
case PlatformChannelErrorType.permissionDenied:
return 'Please grant permission to access motion sensors in your device settings.';
case PlatformChannelErrorType.sensorUnavailable:
return 'Motion sensors are not available on this device.';
case PlatformChannelErrorType.serviceUnavailable:
return 'Step counting service is temporarily unavailable. Please try again.';
default:
return 'An unexpected error occurred. Please contact support if this persists.';
}
}
}
Platform Channel Manager
This class handles all communication with native Android:
// lib/features/counter_steps/services/platform_channel_manager.dart
class PlatformChannelManager {
static const String _channelName = 'com.example.step_counter';
static const MethodChannel _methodChannel = MethodChannel(
'$_channelName/method',
);
static const EventChannel _eventChannel = EventChannel('$_channelName/event');
static StreamSubscription<dynamic>? _eventSubscription;
static final StreamController<StepData> _stepDataController =
StreamController<StepData>.broadcast();
static Stream<StepData> get stepDataStream => _stepDataController.stream;
static Future<void> initialize() async {
try {
_eventSubscription = _eventChannel
.receiveBroadcastStream()
.handleError(_handleEventChannelError)
.listen(_handleStepDataEvent);
if (kDebugMode) {
print('Platform channels initialized successfully');
}
} catch (e) {
throw PlatformChannelError(
type: PlatformChannelErrorType.unknownError,
message: 'Failed to initialize platform channels: $e',
);
}
}
static Future<Map<String, dynamic>> startStepCountingService() async {
try {
final result = await _methodChannel.invokeMethod('startService');
return Map<String, dynamic>.from(result ?? {});
} on PlatformException catch (e) {
throw PlatformChannelError.fromPlatformException(e);
}
}
static Future<Map<String, bool>> checkPermissions() async {
try {
final result = await _methodChannel.invokeMethod('checkPermissions');
return Map<String, bool>.from(result ?? {});
} on PlatformException catch (e) {
throw PlatformChannelError.fromPlatformException(e);
}
}
static Future<Map<String, dynamic>> stopStepCountingService() async {
try {
final result = await _methodChannel.invokeMethod('stopService');
return Map<String, dynamic>.from(result ?? {});
} on PlatformException catch (e) {
throw PlatformChannelError.fromPlatformException(e);
}
}
static void _handleStepDataEvent(dynamic event) {
try {
if (event is Map) {
final stepData = StepData.fromMap(Map<String, dynamic>.from(event));
_stepDataController.add(stepData);
}
} catch (e) {
_stepDataController.addError(
PlatformChannelError(
type: PlatformChannelErrorType.invalidData,
message: 'Invalid step data received: $e',
),
);
}
}
static void _handleEventChannelError(dynamic error) {
if (error is PlatformException) {
final channelError = PlatformChannelError.fromPlatformException(error);
_stepDataController.addError(channelError);
}
}
static Future<void> dispose() async {
await _eventSubscription?.cancel();
await _stepDataController.close();
}
}
UI Implementation
Simple, clean interface for controlling the step counter:
// lib/features/counter_steps/views/step_counter_home_page.dart
class StepCounterHomePage extends StatefulWidget {
static const String routeName = "/StepCounterHomePage";
const StepCounterHomePage({super.key});
@override
_StepCounterHomePageState createState() => _StepCounterHomePageState();
}
class _StepCounterHomePageState extends State<StepCounterHomePage> {
StepData? currentStepData;
bool isServiceRunning = false;
bool isInitialized = false;
PlatformChannelError? lastError;
StreamSubscription<StepData>? stepDataSubscription;
@override
void initState() {
super.initState();
_initializeApp();
}
Future<void> _initializeApp() async {
try {
await PlatformChannelManager.initialize();
_setupStepDataListener();
setState(() {
isInitialized = true;
lastError = null;
});
} catch (e) {
setState(() {
lastError = e is PlatformChannelError
? e
: PlatformChannelError(
type: PlatformChannelErrorType.unknownError,
message: 'Initialization failed: $e',
);
});
}
}
void _setupStepDataListener() {
stepDataSubscription = PlatformChannelManager.stepDataStream
.handleError((error) {
setState(() {
lastError = error is PlatformChannelError
? error
: PlatformChannelError(
type: PlatformChannelErrorType.unknownError,
message: 'Stream error: $error',
);
});
})
.listen((stepData) {
setState(() {
currentStepData = stepData;
lastError = null;
});
});
}
Future<void> _startService() async {
if (!isInitialized) return;
try {
final result = await PlatformChannelManager.startStepCountingService();
if (result['success'] == true) {
setState(() {
isServiceRunning = true;
lastError = null;
});
_showSnackBar('Step counting started successfully');
}
} catch (e) {
final error = e is PlatformChannelError
? e
: PlatformChannelError(
type: PlatformChannelErrorType.serviceUnavailable,
message: 'Failed to start service: $e',
);
setState(() {
lastError = error;
});
_showSnackBar(error.userFriendlyMessage, isError: true);
}
}
Future<void> _stopService() async {
if (!isInitialized) return;
try {
final result = await PlatformChannelManager.stopStepCountingService();
if (result['success'] == true) {
setState(() {
isServiceRunning = false;
lastError = null;
});
_showSnackBar('Step counting stopped successfully');
}
} catch (e) {
final error = e is PlatformChannelError
? e
: PlatformChannelError(
type: PlatformChannelErrorType.serviceUnavailable,
message: 'Failed to stop service: $e',
);
setState(() {
lastError = error;
});
_showSnackBar(error.userFriendlyMessage, isError: true);
}
}
void _showSnackBar(String message, {bool isError = false}) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: isError ? Colors.red : Colors.green,
duration: const Duration(seconds: 3),
),
);
}
@override
void dispose() {
stepDataSubscription?.cancel();
PlatformChannelManager.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Step Counter')),
body: !isInitialized
? const Center(child: CircularProgressIndicator())
: Column(
children: [
if (lastError != null)
Container(
padding: const EdgeInsets.all(16),
color: Colors.red.shade100,
child: Text(lastError!.userFriendlyMessage),
),
Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Steps Today',
style: TextStyle(fontSize: 24),
),
Text(
'${currentStepData?.steps ?? 0}',
style: const TextStyle(
fontSize: 48,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 40),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: isServiceRunning
? null
: _startService,
child: const Text('Start Counting'),
),
ElevatedButton(
onPressed: !isServiceRunning
? null
: _stopService,
child: const Text('Stop Counting'),
),
],
),
],
),
),
),
],
),
);
}
}
Android Implementation
Manifest Configuration
Add the required permissions and service declaration:
<!-- android/app/src/main/AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Permissions for step counting -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
<uses-permission android:name="android.permission.BODY_SENSORS" />
<!-- Sensor features -->
<uses-feature android:name="android.hardware.sensor.stepcounter" android:required="false" />
<uses-feature android:name="android.hardware.sensor.accelerometer" android:required="true" />
<application android:label="Step Counter App">
<activity android:name=".MainActivity" android:exported="true">
<!-- Activity configuration -->
</activity>
<!-- Background step counting service -->
<service
android:name=".StepCounterService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="health" />
</application>
</manifest>
MainActivity: Channel Setup
Handle Flutter's method calls and set up the event stream:
// android/app/src/main/kotlin/com/example/native_channel_app/MainActivity.kt
class MainActivity : FlutterActivity() {
// Step counter
private val CHANNEL_NAME = "com.example.step_counter"
private val METHOD_CHANNEL = "$CHANNEL_NAME/method"
private val EVENT_CHANNEL = "$CHANNEL_NAME/event"
private var stepMethodChannel: MethodChannel? = null
private var stepEventChannel: EventChannel? = null
private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.ACTIVITY_RECOGNITION)
private val PERMISSION_REQUEST_CODE = 1001
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
// 🔹 Step counter channels
setupStepCounterChannels(flutterEngine)
}
private fun setupStepCounterChannels(engine: FlutterEngine) {
stepMethodChannel = MethodChannel(engine.dartExecutor.binaryMessenger, METHOD_CHANNEL)
stepMethodChannel?.setMethodCallHandler { call: MethodCall, result: MethodChannel.Result ->
handleStepMethodCall(call, result)
}
stepEventChannel = EventChannel(engine.dartExecutor.binaryMessenger, EVENT_CHANNEL)
stepEventChannel?.setStreamHandler(object : EventChannel.StreamHandler {
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
StepCounterService.setEventSink(events)
}
override fun onCancel(arguments: Any?) {
StepCounterService.setEventSink(null)
}
})
}
private fun handleStepMethodCall(call: MethodCall, result: MethodChannel.Result) {
try {
when (call.method) {
"startService" -> startStepCounterService(result)
"stopService" -> stopStepCounterService(result)
"checkPermissions" -> checkPermissions(result)
"requestPermissions" -> requestStepPermissions(result)
"validateData" -> validateInputData(call, result)
else -> result.notImplemented()
}
} catch (e: Exception) {
Log.e("MainActivity", "Error handling method call: ${call.method}", e)
result.error("PLATFORM_ERROR", e.message, null)
}
}
private fun startStepCounterService(result: MethodChannel.Result) {
if (!hasRequiredPermissions()) {
result.error("PERMISSION_DENIED", "Required permissions not granted", null)
return
}
try {
val intent = Intent(this, StepCounterService::class.java)
if (VERSION.SDK_INT >= VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}
result.success(mapOf("success" to true, "message" to "Service started"))
} catch (e: Exception) {
result.error("SERVICE_ERROR", "Failed to start service: ${e.message}", null)
}
}
private fun stopStepCounterService(result: MethodChannel.Result) {
try {
val intent = Intent(this, StepCounterService::class.java)
stopService(intent)
result.success(mapOf("success" to true, "message" to "Service stopped"))
} catch (e: Exception) {
result.error("SERVICE_ERROR", "Failed to stop service: ${e.message}", null)
}
}
private fun hasRequiredPermissions(): Boolean =
REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
}
private fun checkPermissions(result: MethodChannel.Result) {
result.success(hasRequiredPermissions())
}
private fun requestStepPermissions(result: MethodChannel.Result) {
if (hasRequiredPermissions()) {
result.success(true)
return
}
ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, PERMISSION_REQUEST_CODE)
result.success(true)
}
private fun validateInputData(call: MethodCall, result: MethodChannel.Result) {
@Suppress("UNCHECKED_CAST")
val data = call.arguments as? Map<String, Any?>
if (data == null) {
result.error("INVALID_DATA", "Data cannot be null", null)
return
}
val isValid = SecurityValidator.validateSensorData(data)
result.success(mapOf("isValid" to isValid))
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == PERMISSION_REQUEST_CODE) {
// Handle permission results if needed
}
}
}
Security Validator: Input Validation & Sanitization
Critical security component for validating platform channel data:
// android/app/src/main/kotlin/com/example/native_channel_app/SecurityValidator.kt
class SecurityValidator {
companion object {
// Validate step count values are within reasonable bounds
fun validateStepCount(value: Any?): Boolean {
if (value !is Number) return false
val intValue = value.toInt()
return intValue >= 0 && intValue <= 1000000 // Max 1M steps per session
}
// Validate sensor data structure and freshness
fun validateSensorData(data: Map<String, Any?>?): Boolean {
if (data == null) return false
// Check required fields exist
if (!data.containsKey("timestamp") || !data.containsKey("value")) {
return false
}
// Validate timestamp freshness (within last hour)
val timestamp = data["timestamp"] as? Long ?: return false
val now = System.currentTimeMillis()
if (kotlin.math.abs(now - timestamp) > 3600000) return false // 1 hour
return true
}
// Sanitize string inputs to prevent injection attacks
fun sanitizeInput(input: String?): String {
return input?.replace(Regex("[<>\"'\${}]"), "") ?: ""
}
}
}
StepCounterService: The Heart of Step Detection
This foreground service handles sensor data and streams it to Flutter:
// android/app/src/main/kotlin/com/example/native_channel_app/StepCounterService.kt
class StepCounterService : Service(), SensorEventListener {
private lateinit var sensorManager: SensorManager
private var stepCounterSensor: Sensor? = null
private var accelerometerSensor: Sensor? = null
private var initialStepCount: Float = -1f
private var currentSessionSteps: Int = 0
private var isUsingAccelerometer = false
// Accelerometer step detection variables
private var lastAcceleration = 0f
private var currentAcceleration = 0f
private var lastUpdate = 0L
private val stepThreshold = 12f
companion object {
private const val NOTIFICATION_ID = 1001
private const val CHANNEL_ID = "step_counter_channel"
private var eventSink: EventChannel.EventSink? = null
fun setEventSink(sink: EventChannel.EventSink?) {
eventSink = sink
}
}
override fun onCreate() {
super.onCreate()
initializeSensors()
createNotificationChannel()
startForegroundService()
}
private fun initializeSensors() {
sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
// Try hardware step counter first (more accurate, less battery drain)
stepCounterSensor = sensorManager.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)
if (stepCounterSensor != null) {
sensorManager.registerListener(this, stepCounterSensor, SensorManager.SENSOR_DELAY_NORMAL)
Log.d("StepService", "Using hardware step counter")
} else {
// Fallback to accelerometer-based detection
accelerometerSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
if (accelerometerSensor != null) {
isUsingAccelerometer = true
sensorManager.registerListener(this, accelerometerSensor, SensorManager.SENSOR_DELAY_UI)
lastUpdate = System.currentTimeMillis()
Log.d("StepService", "Using accelerometer fallback")
} else {
Log.e("StepService", "No suitable sensors available")
}
}
}
override fun onSensorChanged(event: SensorEvent?) {
if (event == null) return
when (event.sensor.type) {
Sensor.TYPE_STEP_COUNTER -> handleStepCounterData(event)
Sensor.TYPE_ACCELEROMETER -> handleAccelerometerData(event)
}
}
private fun handleStepCounterData(event: SensorEvent) {
val totalSteps = event.values[0]
// Initialize baseline on first reading
if (initialStepCount < 0) {
initialStepCount = totalSteps
}
// Calculate steps for this session
val sessionSteps = (totalSteps - initialStepCount).toInt()
// Validate step count before updating
if (SecurityValidator.validateStepCount(sessionSteps)) {
updateStepCount(sessionSteps)
}
}
private fun handleAccelerometerData(event: SensorEvent) {
val currentTime = System.currentTimeMillis()
// Throttle processing to avoid overwhelming the CPU
if (currentTime - lastUpdate < 100) return
val x = event.values[0]
val y = event.values[1]
val z = event.values[2]
// Calculate acceleration magnitude
lastAcceleration = currentAcceleration
currentAcceleration = sqrt(x * x + y * y + z * z)
// Simple step detection: significant acceleration change
val delta = currentAcceleration - lastAcceleration
if (delta > stepThreshold) {
currentSessionSteps++
// Validate before updating
if (SecurityValidator.validateStepCount(currentSessionSteps)) {
updateStepCount(currentSessionSteps)
}
}
lastUpdate = currentTime
}
private fun updateStepCount(steps: Int) {
currentSessionSteps = steps
// Create validated data map
val data = mapOf(
"steps" to steps,
"timestamp" to System.currentTimeMillis(),
"sensor_type" to if (isUsingAccelerometer) "accelerometer" else "step_counter",
"accuracy" to if (isUsingAccelerometer) "medium" else "high"
)
// Validate data before sending to Flutter
if (SecurityValidator.validateSensorData(data)) {
eventSink?.success(data)
updateNotification("Steps: $steps")
} else {
Log.w("StepService", "Invalid sensor data blocked")
}
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"Step Counter Service",
NotificationManager.IMPORTANCE_LOW
).apply {
description = "Tracks your steps in the background"
}
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.createNotificationChannel(channel)
}
}
private fun startForegroundService() {
val notification = createNotification("Starting step counter...")
startForeground(NOTIFICATION_ID, notification)
}
private fun createNotification(content: String): Notification =
NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Step Counter Active")
.setContentText(content)
.setSmallIcon(android.R.drawable.ic_menu_directions)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
.build()
private fun updateNotification(content: String) {
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.notify(NOTIFICATION_ID, createNotification(content))
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
// Handle sensor accuracy changes if needed
}
override fun onDestroy() {
super.onDestroy()
sensorManager.unregisterListener(this)
eventSink = null
}
override fun onBind(intent: Intent?): IBinder? = null
}
Key Features Explained
Security First Approach
The SecurityValidator ensures all data is validated before processing:
- Step Count Validation: Prevents unrealistic values (0-1M steps)
- Timestamp Freshness: Rejects stale data older than 1 hour
- Input Sanitization: Removes dangerous characters from strings
- Data Structure Validation: Ensures required fields are present
Dual Sensor Strategy
The service tries hardware step counter first (TYPE_STEP_COUNTER) for accuracy and battery efficiency. If unavailable, it falls back to accelerometer-based detection with a simple threshold algorithm.
Foreground Service
Android requires foreground services for background sensor access. The persistent notification ensures the system won't kill our service and keeps users informed.
Stream Communication
EventChannel creates a continuous data pipeline from Android to Flutter. Unlike MethodChannel's request-response pattern, EventChannel pushes data whenever new steps are detected.
Permission Handling
Modern Android requires explicit permission for activity recognition. The app checks and requests permissions before starting the service.
🚀 Running the App
git clone https://github.com/AhmedTarek-f/native-channel-app.git
cd native-channel-app
flutter pub get
flutter run
- Grant Activity Recognition permission when prompted
- Tap "Start" to begin step counting
- Walk around to see real-time step updates
- The service continues counting even when the app is backgrounded
- Tap "Stop" to end the session
Key Take away: By combining MethodChannel for control operations, EventChannel for streaming data, and proper security validation, you can build sophisticated native integrations that are both powerful and secure while leveraging the full capabilities of the host platform.
📁 Complete Project
Repository: https://github.com/AhmedTarek-f/native-channel-app.git
This repository contains three complete examples:
- 🔋 Battery Level (MethodChannel)
- 💬 Live Chat (BasicMessageChannel)
- 👟 Step Counter (MethodChannel + EventChannel)
Features: All 3 channel types • Foreground services • Sensor integration • Permission handling • Error handling • Security validation • Input sanitization • Data validation
Top comments (0)