Writing Platform-Specific Code When Necessary in Flutter

Flutter, Google’s UI toolkit, is celebrated for its ability to build natively compiled applications for mobile, web, and desktop from a single codebase. However, there are times when platform-specific code is necessary to access native APIs, implement features unavailable in Flutter, or optimize performance for a particular platform. This article explores the scenarios in which platform-specific code is needed in Flutter, and how to implement it using platform channels, conditional imports, and platform directives.

When Is Platform-Specific Code Needed in Flutter?

While Flutter aims to provide a unified development experience, certain scenarios necessitate the use of platform-specific code:

  • Accessing Native APIs: When you need to use platform-specific features or APIs not exposed by Flutter plugins.
  • Performance Optimization: Optimizing performance on specific platforms by leveraging native code.
  • Integration with Native Libraries: Using existing native libraries or SDKs for particular platforms.
  • Unique Platform Features: Implementing features that are unique to a platform and have no equivalent in Flutter.

Methods for Implementing Platform-Specific Code in Flutter

Flutter provides several ways to incorporate platform-specific code into your applications:

1. Platform Channels

Platform channels are the primary mechanism for communicating between Flutter code and platform-specific code (written in languages like Kotlin/Java for Android and Swift/Objective-C for iOS). Platform channels allow you to invoke native code and receive results back in Flutter.

Step 1: Define a Method Channel in Flutter

In your Flutter code, create a MethodChannel:


import 'package:flutter/services.dart';

const platform = MethodChannel('your.package.name/channel');

Future getPlatformVersion() async {
  String version;
  try {
    final String result = await platform.invokeMethod('getPlatformVersion');
    version = 'Platform version: $result';
  } on PlatformException catch (e) {
    version = "Failed to get platform version: '${e.message}'.";
  }
  return version;
}
Step 2: Implement Native Code (Android)

In your Android code (MainActivity.kt or MainActivity.java), handle the method call:


import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity: FlutterActivity() {
    private val CHANNEL = "your.package.name/channel"

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
            call, result ->
            if (call.method == "getPlatformVersion") {
                result.success("Android ${android.os.Build.VERSION.RELEASE}")
            } else {
                result.notImplemented()
            }
        }
    }
}
Step 3: Implement Native Code (iOS)

In your iOS code (AppDelegate.swift or AppDelegate.m), handle the method call:


import Flutter
import UIKit

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    let channel = FlutterMethodChannel(name: "your.package.name/channel",
                                              binaryMessenger: controller.binaryMessenger)
    channel.setMethodCallHandler({
      (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
      guard call.method == "getPlatformVersion" else {
        result(FlutterMethodNotImplemented)
        return
      }
      result("iOS " + UIDevice.current.systemVersion)
    })

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}
Step 4: Use the Method Channel in Flutter

Call the method in your Flutter UI:


import 'package:flutter/material.dart';

class MyHomePage extends StatefulWidget {
  MyHomePage({Key? key, required this.title}) : super(key: key);

  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State {
  String _platformVersion = 'Unknown';

  @override
  void initState() {
    super.initState();
    getPlatformVersion().then((version) {
      setState(() {
        _platformVersion = version;
      });
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Text(_platformVersion),
      ),
    );
  }
}

2. Conditional Imports

Conditional imports allow you to import different files based on the platform at compile time. This is useful for using different implementations of a class or function for different platforms.

Step 1: Create Platform-Specific Implementations

Create separate files for each platform:

my_platform.dart (Generic interface):


abstract class MyPlatform {
  String getPlatformName();
}

MyPlatform getPlatform();

my_platform_android.dart (Android implementation):


import 'my_platform.dart';

class MyPlatformAndroid implements MyPlatform {
  @override
  String getPlatformName() => 'Android';
}

MyPlatform getPlatform() => MyPlatformAndroid();

my_platform_ios.dart (iOS implementation):


import 'my_platform.dart';

class MyPlatformIOS implements MyPlatform {
  @override
  String getPlatformName() => 'iOS';
}

MyPlatform getPlatform() => MyPlatformIOS();
Step 2: Use Conditional Imports

In your main file, use conditional imports to import the correct implementation based on the platform:


import 'my_platform.dart';
import 'my_platform_android.dart' if (dart.library.io) 'my_platform_ios.dart';

void main() {
  final platform = getPlatform();
  print('Running on ${platform.getPlatformName()}');
}

3. Platform Directives

Platform directives are compile-time constants that allow you to conditionally execute code based on the target platform. Common directives include kIsWeb, kIsAndroid, kIsIOS, kIsLinux, kIsMacOS, and kIsWindows.

Step 1: Import foundation.dart

Ensure you import foundation.dart from package:flutter/foundation.dart:


import 'package:flutter/foundation.dart';
Step 2: Use Platform Directives

Use the directives to conditionally execute code:


import 'package:flutter/foundation.dart';

void main() {
  if (kIsWeb) {
    print('Running on Web');
  } else if (kIsAndroid) {
    print('Running on Android');
  } else if (kIsIOS) {
    print('Running on iOS');
  } else {
    print('Running on an unknown platform');
  }
}

Best Practices

  • Minimize Platform-Specific Code: Aim to use platform-specific code only when absolutely necessary. Prefer using Flutter’s built-in features or community plugins whenever possible.
  • Abstract Platform Differences: Create an abstraction layer to hide platform-specific details. This makes your code more maintainable and testable.
  • Document Platform Dependencies: Clearly document any platform-specific code and its dependencies. This helps other developers understand and maintain the code.
  • Test on Multiple Platforms: Ensure your application is thoroughly tested on all target platforms to catch any platform-specific issues.

Conclusion

Writing platform-specific code in Flutter is sometimes necessary to access native features or optimize performance. Platform channels, conditional imports, and platform directives are powerful tools for incorporating this code while maintaining a mostly shared codebase. By following best practices, you can effectively integrate platform-specific code into your Flutter applications while minimizing complexity and maximizing code reuse.