In the ever-evolving landscape of mobile app development, the quest for creating seamless, efficient, and high-performing applications has led developers down the path of innovation and integration. Among the shining beacons in this journey is Flutter, Google’s UI toolkit for crafting natively compiled applications for mobile, web, and desktop from a single codebase.

Flutter stands out for its ability to deliver apps with a native feel, thanks to its rich set of fully-customizable widgets that make building native interfaces in minutes a reality. However, even with its extensive capabilities, there are scenarios where Flutter apps may need to leverage the specific functionalities offered by native SDKs (Software Development Kits).

Native SDKs, which are platform-specific tools for Android and iOS, provide access to device- or platform-specific features not directly available in Flutter, such as advanced payment processing, using device sensors beyond the basics, or integrating with platform-specific software. This integration is not only a testament to the flexibility and adaptability of Flutter but also an essential strategy for developers aiming to enrich their applications with features that enhance user experience and performance.

Imagine creating a mobile application that not only boasts a beautiful UI and smooth performance but also integrates seamlessly with platform-specific functionalities, offering your users an unparalleled experience. This is not just an aspiration; it’s a reality made possible through the integration of Flutter with native SDKs.

In this blog post, we’ll dive into how this integration can elevate your mobile applications, drawing from a practical example where we enhanced a Flutter app with native Stripe SDKs for payment processing. Although the backend intricacies, developed with Go, are beyond the scope of this discussion, they play a crucial role in the app’s functionality. Our journey will reveal how combining Flutter’s versatility with the power of native SDKs can unlock new possibilities for your mobile apps, making them more robust, versatile, and user-friendly.

Flutter Platform Integrations

Integrating native SDKs into your Flutter application is a powerful way to tap into platform-specific features and capabilities that are not natively available in Flutter. This process involves a communication bridge known as platform channels, enabling Flutter to send and receive messages to and from the native side of the app.

Understanding Platform Channels

platform channels diagram

Platform channels are the bridge for communication between Flutter and native code. They allow you to execute native code from your Flutter app. Flutter supports different types of messages for platform channels, including simple messages and method calls. The standard message codec facilitates efficient binary serialization of simple JSON-like values, which includes booleans, numbers, strings, byte buffers, and lists and maps of these items.

Define the Channel

Start by defining a platform channel in your Flutter app. Choose between the available channel types based on your needs:

MethodChannel: Used for most of the use cases, where a method call in Flutter triggers a native method. It supports asynchronous method calls.

EventChannel: Used for data streams from native to Flutter.

BasicMessageChannel: Used for sending simple messages between Flutter and native code.

The MethodChannel is most commonly used for integrating native SDKs and It’s going to be the approach used in this case.

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class CartPage extends StatelessWidget {
  static const MethodChannel _channel = MethodChannel('co.wawand/stripe');

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      // Widget...
    );
  }
}

Implement the Native Code

On the native side (both Android and iOS), we need to implement the method that gets called from Flutter. This involves editing the native code files. Considering that we’re going to use the native stripe SDK, we need to install the dependency for each platform using their dependency manager and set up the payment logic.

Android

From our experience, the recommendation is to handle the Android project by opening it with Android Studio, it will make it easier to identify the dependencies, sync them, and add the custom native code.

Stripe Android SDK. We need to add the Stripe SDK for the android platform modifying the build.gradle within android/app/ directory and sync the gradle dependencies.

dependencies {
    // ...
    implementation 'com.stripe:stripe-android:20.37.4'
}

Android Activity. We need to implement the method to call from Flutter on the MainActivity file. Depending on the Flutter project configuration could be a Java or Kotlin file within the android/app/src/main/java/<your_package_name>/ directory.

package co.wawand.stripe_payment

import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import com.stripe.android.PaymentConfiguration

class MainActivity: FlutterActivity() {
    private val CHANNEL = "co.wawand/stripe"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        
        // Setup stripe payment config
        PaymentConfiguration.init(applicationContext, "publishable stripe key")
        
        //Catching method channel call invoke
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
            call, result ->
            when (call.method) {
                "processPayment" -> {
                    val paymentDetails = call.arguments<Map<String, Any>>()
                    try {
                        val paymentResult = processPayment(paymentDetails)
                        result.success(paymentResult)
                    } catch (e: Exception) {
                        result.error("ERROR", "Payment processing failed: ${e.localizedMessage}", null)
                    }
                }
                else -> result.notImplemented()
            }
        }
    }

    private fun processPayment(paymentDetails: Map<String, Any>): String {
        // payment processing logic here.
        return "Payment processed for ${paymentDetails["amount"].toString()}"
    }
}

Android Considerations

In regular Android projects is common to see that the Activity class extends from the base classes such as ComponentActivity, FragmentActivity, or Activity. Flutter recreated their activity by making a custom class named FlutterActivity that extends from the Activity base class allowing it to add custom methods to communicate the Flutter app with the native code.

There is a Groovy file named flutter.groovy which contains some configurations for the Android builds to manage source and target compatibilities, gradle tasks, minimum, compile, and target SDK versions, and so on. Normally we can evidence those configurations in the gradle file at the app level in regular Android projects.

There are several gradle files generated by Flutter to handle Flutter plugins to publish the app, others to load the Flutter configurations or load the native dependencies, and more. Could be a little tricky to understand and make some adjustments for the native dependencies or the custom native code if they demand it.

iOS

For iOS projects XCode (Apple’s official integrated development environment) will always be the best option. We need to take into account that we are going to edit Swift code, change or create view controllers, and touch the Main.storyboard.

Stripe iOS SDK: We need to import the Stripe SDK via CocoaPods (there are other methods. Check the official documentation for more information) modifying the Podfile.

target 'Runner' do
  use_frameworks!
  use_modular_headers!

  flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
  target 'RunnerTests' do
    inherit! :search_paths
  end
  #Stripe Pod
  pod 'StripePaymentSheet'
end

Then, run the command pod install within the ios folder to install the Stripe pod.

iOS AppDelegate: We need to adjust AppDelegate.swift file within the ios/Runner/ directory to set the method to call from Flutter.

import UIKit
import Flutter
import StripePaymentSheet

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  private let channelName = "co.wawand/stripe"

  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)
    guard let controller = window?.rootViewController as? FlutterViewController else {
      fatalError("rootViewController is not type FlutterViewController")
    }
      
    StripeAPI.defaultPublishableKey = "publishable stripe key"
      
    let methodChannel = FlutterMethodChannel(name: channelName,
                                             binaryMessenger: controller.binaryMessenger)
    methodChannel.setMethodCallHandler({
      (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
      // Check if method call is processPayment
      if call.method == "processPayment" {
        self.processPayment(call: call, result: result)
      } else {
        result(FlutterMethodNotImplemented)
      }
    })

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

  private func processPayment(call: FlutterMethodCall, result: @escaping FlutterResult) {
    // Assume call.arguments is a dictionary with payment details
    guard let args = call.arguments as? [String: Any],
          let amount = args["amount"] as? String else {
      result(FlutterError(code: "INVALID_ARGUMENTS", message: "Invalid iOS arguments received for processing payment", details: nil))
      return
    }
    // your payment processing logic here
    result("Payment processed for \(amount)")
  }
}

iOS Considerations

The AppDelegate class extending from FlutterAppDelegate in a Flutter project is a design choice that facilitates the integration of the Flutter engine with the native iOS application lifecycle and system. This setup ensures that Flutter apps can leverage native platform capabilities while providing a smooth and native user experience.

Some SDKs require additional configurations or keys to be added to the Info.plist file. This could include API keys, configuration options, or SDK-specific settings necessary for the SDK to function correctly.

Xcode’s build settings and project configurations can be complex, with many options for configuring your app’s build process. When integrating native SDKs, you might need to adjust settings like the deployment target, architecture settings, or compile flags. Adding native SDKs often involves linking against additional frameworks and libraries.

Call the Native Code from Flutter

Back in your Flutter app, use the MethodChannel to invoke the native method. This is done by specifying the channel name (which must match on both Flutter and native sides) and calling the method using the invokeMethod function. It’s crucial to handle exceptions and errors that may occur during this communication.

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class CartPage extends StatelessWidget {
  static const MethodChannel _channel = MethodChannel('co.wawand/stripe');

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Cart'),
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            // Example paymentDetails.
            Map paymentDetails = {
              'amount': 1000, // Example amount
              'itemName': 'banana', // Example item
            };
            initialize(paymentDetails);
          },
          child: Text('Pay with Stripe'),
        ),
      ),
    );
  }

  Future<void> initialize(Map paymentDetails) async {
    try {
      await _channel.invokeMethod('processPayment', paymentDetails);
    } on PlatformException catch (e) {
      if (kDebugMode) {
        print(e.message);
      }
    }
  }
}

Technical Aspects

After completing the integration of the Stripe SDK with a Flutter application, several key insights and conclusions can be drawn about the process, its impact on app development, and its implications for app performance and user experience. This integration highlights the versatility of Flutter in accommodating native SDKs and showcases the potential for creating robust, feature-rich mobile applications. Here are some considerations on debugging, performance, interoperability, and common use cases for this kind of solution.

Debugging

Integrating native SDKs into a Flutter app introduces a layer of complexity when it comes to debugging. Issues may arise from the Dart side, the native Swift/Kotlin side, or the communication channel between them. The use of platform channels for this integration necessitates a careful approach to debugging that covers both native and Flutter realms. Tools like Flutter DevTools and native debuggers (Xcode for iOS, Android Studio for Android) become indispensable in identifying and resolving issues. The process underscores the importance of having a solid understanding of both Flutter and the native platform’s debugging tools and techniques.

Performance

Performance is a crucial aspect of any mobile application, and the integration of a native SDK like Stripe into a Flutter app can have implications for app speed and responsiveness. However, if implemented correctly, this integration can achieve performance that is on par with native applications. Efficient use of platform channels and optimizing the native code can minimize any potential overhead, ensuring that the integration features do not degrade the user experience. It’s essential to profile the app’s performance post-integration to identify any bottlenecks or latency issues in the integration flow.

Interoperability with the Flutter App

The success of integrating SDKs into a Flutter app heavily relies on the seamless interoperability between the Flutter code and the native modules. This integration demonstrates Flutter’s capability to work hand-in-hand with native code, allowing developers to leverage the full spectrum of features offered by third-party native SDKs. By following best practices for platform channel usage, such as defining clear method call signatures and ensuring data type compatibility, developers can achieve a high level of interoperability that feels natural within the Flutter environment.

Common Use Cases

integrating native SDKs with Flutter extends the functionality of mobile apps beyond what’s available through Dart packages, allowing developers to utilize platform-specific features, enhance performance, and meet various operational needs. While the previous discussion highlighted the integration of the Stripe SDK as a specific example, the approach can be applied to a wide range of solutions beyond payment processing. Here are some general use cases for integrating native SDKs with Flutter, illustrating the versatility and potential of such integrations:

Enhanced Device Hardware Access

Native SDKs can offer more direct or advanced access to device hardware than what’s available through Flutter plugins. This includes features like advanced camera controls, sensor data management (gyroscope, accelerometer), and Bluetooth communication for IoT devices.

Customized Map and Geolocation Services

While Flutter offers plugins for map and geolocation services, native SDKs might provide more detailed control over maps, advanced geolocation tracking, custom markers, and interactive features.

Conclusion

The integration of native SDKs with Flutter is a powerful approach that opens up a broad spectrum of possibilities for mobile app development. By leveraging the strengths of both Flutter and native platforms, developers can create apps that are not only visually appealing and performant but also deeply integrated with the platform’s core capabilities and services. This hybrid approach allows for the development of feature-rich, platform-optimized applications that can meet diverse user needs and stand out in the competitive app marketplace.

References