Flutter'da Platform Channels: iOS ve Android Native Entegrasyon

Flutter, platform bağımsız UI çizer; ancak kamera, Bluetooth, biyometrik kimlik doğrulama veya özel platform API'leri gibi yerel işlevlere erişmek için Platform Channels kullanılır. Dart kodu ile native (Kotlin/Swift) kod arasında mesaj köprüsü kurar.

MethodChannel: Tek Seferlik Çağrılar

En yaygın kullanım şeklidir. Dart tarafından bir metot çağrısı yapılır, native taraf yanıt döner:

// Flutter (Dart) tarafı
class BatteryService {
  static const _channel = MethodChannel('com.example.app/battery');

  Future<int> getBatteryLevel() async {
    try {
      final level = await _channel.invokeMethod<int>('getBatteryLevel');
      return level ?? -1;
    } on PlatformException catch (e) {
      debugPrint('Pil bilgisi alınamadı: ${e.message}');
      return -1;
    }
  }

  Future<bool> isCharging() async {
    return await _channel.invokeMethod<bool>('isCharging') ?? false;
  }
}

// UI'da kullanım
class BatteryWidget extends StatefulWidget {
  const BatteryWidget({super.key});

  @override
  State<BatteryWidget> createState() => _BatteryWidgetState();
}

class _BatteryWidgetState extends State<BatteryWidget> {
  final _service = BatteryService();
  int _level = 0;
  bool _charging = false;

  @override
  void initState() {
    super.initState();
    _loadBattery();
  }

  Future<void> _loadBattery() async {
    final level = await _service.getBatteryLevel();
    final charging = await _service.isCharging();
    if (mounted) setState(() { _level = level; _charging = charging; });
  }

  @override
  Widget build(BuildContext context) => ListTile(
    leading: Icon(_charging ? Icons.battery_charging_full : Icons.battery_std),
    title: Text('Pil: $_level%'),
    subtitle: Text(_charging ? 'Şarj oluyor' : 'Pilde'),
  );
}

Android (Kotlin) Implementasyonu

android/app/src/main/kotlin/.../MainActivity.kt dosyasını düzenleyin:

import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity : FlutterActivity() {
    private val CHANNEL = "com.example.app/battery"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        MethodChannel(
            flutterEngine.dartExecutor.binaryMessenger,
            CHANNEL
        ).setMethodCallHandler { call, result ->
            when (call.method) {
                "getBatteryLevel" -> {
                    val level = getBatteryLevel()
                    if (level != -1) result.success(level)
                    else result.error("UNAVAILABLE", "Pil bilgisi alınamadı", null)
                }
                "isCharging" -> result.success(isCharging())
                else -> result.notImplemented()
            }
        }
    }

    private fun getBatteryLevel(): Int {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            val manager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
            manager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
        } else {
            val intent = registerReceiver(null,
                IntentFilter(Intent.ACTION_BATTERY_CHANGED))
            val level = intent?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
            val scale = intent?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1
            if (level == -1 || scale == -1) -1 else level * 100 / scale
        }
    }

    private fun isCharging(): Boolean {
        val intent = registerReceiver(null,
            IntentFilter(Intent.ACTION_BATTERY_CHANGED))
        val status = intent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1
        return status == BatteryManager.BATTERY_STATUS_CHARGING ||
               status == BatteryManager.BATTERY_STATUS_FULL
    }
}

iOS (Swift) Implementasyonu

ios/Runner/AppDelegate.swift dosyasını düzenleyin:

import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        let controller = window?.rootViewController as! FlutterViewController
        let batteryChannel = FlutterMethodChannel(
            name: "com.example.app/battery",
            binaryMessenger: controller.binaryMessenger
        )

        batteryChannel.setMethodCallHandler { [weak self] call, result in
            switch call.method {
            case "getBatteryLevel":
                UIDevice.current.isBatteryMonitoringEnabled = true
                let level = UIDevice.current.batteryLevel
                if level == -1 {
                    result(FlutterError(code: "UNAVAILABLE",
                                       message: "Pil bilgisi alınamadı",
                                       details: nil))
                } else {
                    result(Int(level * 100))
                }
            case "isCharging":
                UIDevice.current.isBatteryMonitoringEnabled = true
                let state = UIDevice.current.batteryState
                result(state == .charging || state == .full)
            default:
                result(FlutterMethodNotImplemented)
            }
        }

        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
    }
}

EventChannel: Sürekli Veri Akışı

Sensör verileri veya gerçek zamanlı güncellemeler için EventChannel kullanın:

// Dart tarafı — Stream olarak dinle
class AccelerometerService {
  static const _channel = EventChannel('com.example.app/accelerometer');

  Stream<Map<String, double>> get events =>
      _channel.receiveBroadcastStream().map((dynamic event) {
        final map = Map<String, dynamic>.from(event as Map);
        return {
          'x': (map['x'] as num).toDouble(),
          'y': (map['y'] as num).toDouble(),
          'z': (map['z'] as num).toDouble(),
        };
      });
}

// Widget'ta kullanım
StreamBuilder<Map<String, double>>(
  stream: AccelerometerService().events,
  builder: (context, snapshot) {
    if (!snapshot.hasData) return const CircularProgressIndicator();
    final data = snapshot.data!;
    return Text(
      'X: ${data['x']!.toStringAsFixed(2)}\n'
      'Y: ${data['y']!.toStringAsFixed(2)}\n'
      'Z: ${data['z']!.toStringAsFixed(2)}',
    );
  },
)

// Android Kotlin — EventChannel handler
EventChannel(flutterEngine.dartExecutor.binaryMessenger,
    "com.example.app/accelerometer")
    .setStreamHandler(object : EventChannel.StreamHandler {
        private var sensorManager: SensorManager? = null
        private var eventSink: EventChannel.EventSink? = null
        private val listener = object : SensorEventListener {
            override fun onSensorChanged(event: SensorEvent) {
                eventSink?.success(mapOf(
                    "x" to event.values[0],
                    "y" to event.values[1],
                    "z" to event.values[2]
                ))
            }
            override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {}
        }
        override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
            eventSink = events
            sensorManager = getSystemService(SENSOR_SERVICE) as SensorManager
            sensorManager?.registerListener(listener,
                sensorManager!!.getDefaultSensor(Sensor.TYPE_ACCELEROMETER),
                SensorManager.SENSOR_DELAY_NORMAL)
        }
        override fun onCancel(arguments: Any?) {
            sensorManager?.unregisterListener(listener)
            eventSink = null
        }
    })

Platform channels, Flutter'ın ulaşamadığı her native API'ye kapı açar. Channel isimlerini ters domain formatında tutun (com.sirket.uygulama/kanal) ve metodları küçük Dart servis sınıflarında sarmalayın. Böylece platform bağımlı kod uygulama kodundan ayrı kalır.