DEV Community

Omar Elsadany
Omar Elsadany

Posted on • Edited on

🔌 Native Channels in Flutter — A Complete Guide

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%");
}
Enter fullscreen mode Exit fullscreen mode
// Android side
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "battery")
  .setMethodCallHandler { call, result ->
    if (call.method == "getBatteryLevel") {
      val batteryLevel = getBatteryPercentage()
      result.success(batteryLevel)
    }
  }
Enter fullscreen mode Exit fullscreen mode

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");
  });
}
Enter fullscreen mode Exit fullscreen mode

🚶‍♂️ 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     │
                        └──────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

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.';
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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'),
                            ),
                          ],
                        ),
                      ],
                    ),
                  ),
                ),
              ],
           ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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("[<>\"'\${}]"), "") ?: ""
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
  1. Grant Activity Recognition permission when prompted
  2. Tap "Start" to begin step counting
  3. Walk around to see real-time step updates
  4. The service continues counting even when the app is backgrounded
  5. 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)