How to use native APIs in Flutter: iOS and Android Biometric Authentication [Part 2]

Mais Alheraki
Invertase
Published in
9 min readJul 13, 2022

How often do you find yourself in a situation where you need to integrate a new native iOS/Android API, or a third-party SDK, and you think for a second it would have been better if I’m using native instead of Flutter? This does not happen very often, but when it does, it could be critical to the experience of your app.

This is the second part in our series on using platform channels in Flutter to communicate with the host platform. If you have not read the first part, it is recommended that you read it first.

In this part, you will get hands-on using native code from Swift and Kotlin in Dart.

Get the full code

Before diving in, you can get the full project and code used in this tutorial from this repository. The code is runnable, you can clone and play around with it on Android and iOS devices.

Preparing the project

Let’s start with creating a new Flutter project.

flutter create flutter_biometrics_authentication

By default, the newly created project will use Kotlin for Android and Swift for iOS.

💡 You can create a new project that is using Java/ObjC for the Android/iOS side, if you have experience with them.

To do that, run the command with additional parameters:
flutter create -a java -i objc flutter_biometrics_authentication

Once the project is created, and if you are on the stable channel, as of the time of writing, it is Flutter 3.0.2. You can notice you got 6 folders with the following platform names: ios, android, web, macos, windows, and linux. In this tutorial, we are only interested in ios and android folders.

If you flutter run, you can see the usual Flutter counter application. You won’t learn how to increment the counter with a platform channel, instead, you will look into a real use-case.

What are you building?

Before going further into code, let’s break down the sample you’re going to build in simple english.

The use-case

Some apps hold sensitive information about a user, such as financial, and health apps. It is recommended that you use biometrics as an option to protect the user’s privacy.

About Biometrics Native APIs

Both iOS and Android exposes an easy-to-use API that allows developers to authenticate users locally using their configured biometrics such as Face and Fingerprint, or fallback to passcode.

On iOS, you can communicate with the system to ask the user to authenticate with biometrics using LocalAuthentication framework. On Android, this can be achieved using BiometricPrompt package.

iOS and Android sample preview

The Dart and Flutter side

You will start from main.dart. Open the file and delete the MyHomePage widget.

UI part

The app has a single view only, let’s name it AuthView.

The AuthStatus type is not yet defined, so let’s create it.

Lastly, add the new widget AuthView as the home in MaterialApp.

MethodChannel part

The very first step in defining a MethodChannel to use to communicate with the native side.

You will need flutter/services package, go to the very top of your file and add this import:

import 'package:flutter/services.dart';

Next, under _AuthViewState, add the following.

Next, you will implement authenticateWithBiometrics.

To break down what’s happening inside this function:

  1. You’re invoking a method named authenticateWithBiometrics on the samples.invertase.io/biometricschannel.
  2. Then, you’re setting up a handler that will listen to any method calls on this channel that is coming from the native side of the app.
  3. Finally, you’re catching any exception of type PlatformException that might be thrown from the native side.

Results and data validation

After invoking a method on a channel, you might get a response back from the native side. The type returned by invokeMethod is Future<dynamic>, meaning you need to await it, and for the fact that the value of the future is dynamic, you will need to validate the result before going any further in your code.

In this example, you won’t store the result, rather just await for the invocation to finish then proceed, since there’s no expected data to be returned from authenticateWithBiometrics as you will see in the following sections.

The native side

In this section, you will add the native implementation for authenticateWithBiometrics method. Even if you don’t have any native experience, don’t let this part scare you. The goal is to walk you through writing native code and debugging it step by step.

Writing the iOS implementation

The iOS part will be written in Swift language using Xcode, to make use of the native debugging, analyzing and formatting capabilities. If you’re using VS Code, right-click on the iOS folder, you can see Open in Xcode, click on it. If you’re using Android Studio, you should see a similar option, right-click on iOS folder, then Flutter > Open iOS module in Xcode.

Once Xcode window is open, expand Runner folder, and open AppDelegate.

A screenshot from the AppDelegate.swift in the source code.
A screenshot from the AppDelegate.swift in the source code.

The AppDelegate class is the entry point of the Flutter app on iOS. You can override some of its methods to prepare Swift to handle calls from Dart.

First step is to setup the channel. We need 1 constant channelName and 1 variable biometricsChannel, add them inside the AppDelegate class.

Next, paste the following code inside application method.

Xcode will complain that authenticateWithBiometrics does not exist yet, so let’s create it. inside the AppDelegateclass, paste this new method.

The last important part is to import LocalAuthentication framework at the top of the file.

If you got lost, you can refer to the AppDelegat.swift file from the source code.

Sending results to Flutter

To send a result to Flutter from Swift, you will pass in result that you got from setMethodCallHandler to authenticateWithBiometrics method. This object holds all the necessary information needed to send back a response to Flutter. You can send 2 types of responses:

Success with data:

result("Some value")

Error with message, which is received by Dart as PlatformException:

result(FlutterError(code: "ERR_CODE", 
meessage: "Error message",
details: "More details"))

Run and debug

The reason it’s easier to write Swift using Xcode is because it allows us to debug it. In the following snippet, you can see how to run and start a debugging session of the app.

Debugging swift code in Xcode

Writing the Android implementation

The Android part will be written in Kotlin using Android Studio.

Before writing the implementation, you need to add biometric package in app/build.gradle.

implementation 'androidx.biometric:biometric:1.1.0'
Adding biometric dependency inside gradle.build

Then you need to tell Android Studio to fetch the package. Go to File > Sync with Gradle Files, then wait for it to finish syncing.

You also need to add the permission to access biometric fingerprint on the user’s phone. Open android/app/src/main/AndroidManifest.xml, and add the following under application tag:

<uses-permission android:name="android.permission.USE_FINGERPRINT"/>

If you’re using VS Code, right-click on the Android folder, then click on Open in Android Studio. If you’re using Android Studio, you should see a similar option, right-click on Android folder, then Flutter > Open Android module in Android Studio.

Once Android Studio window is open, expand app/java folder, then open the folder named after your application ID, usually this would start with com.example. Finally, the Android native code will be written inside MainActivity.

A screenshot from the MainActivity.kt in the source code.

In your case, the MainActivity class would look like this:

class MainActivity : FlutterActivity () {}

It’s empty, you will start adding code inside of it.

First, add necessary imports.

Next, add the channel name constant and method channel variable inside MainActivity class.

Next, override configureFlutterEngine.

This method is overridden from the inherited class FlutterActivity, this method is the counterpart of applicationmethod in AppDelegate in Swift. You can notice that the implementation of the MethodChannel here has a very similar API to Swift.

Next, paste the implementation of the method authenticateWithBiometrics under configureFlutterEngine.

At this point, Android Studio should show a similar warning about casting this to FragmentActivity.

The reason is that this class is a FlutterActivity, not a FlutterFragmentctivity. To fix it, go up to the class definition, and change FlutterActivity to FlutterFragmentActivity. You will also need to update the imports.

// Remove this
import io.flutter.embedding.android.FlutterActivity
// Add this
import io.flutter.embedding.android.FlutterFragmentActivity

Sending results to Flutter

To send a result to Flutter from Kotlin, you will pass in result that you got from setMethodCallHandler to authenticateWithBiometrics method. This object holds all the necessary information needed to send back a response to Flutter. You can send 2 types of responses:

Success with data:

result.success("Any value")

Error with message, which is received by Dart as PlatformException:

result.error("ERR_CODE", "Error message", "More details")

Run and debug

For the same reason Xcode was used to write the iOS code, you can run and debug the Android side of your Flutter app in Android Studio.

Putting it all together

Let’s break up the actual steps you need when creating any platform channel regardless of your use-case.

  1. Pick a String name for your channel, make sure it’s unique enough not to collide with other plugins in your project. In our example, the channel name was samples.invertase.io/biometrics.
  2. Setup a MethodChannel in both sides, Dart and native, using the same name.
  3. From the native side, call setMethodCallHandler on the newly created MethodChannel.
  4. Inside the handler, you can read the name of the method once it’s triggered from Flutter, if it matches the expected name, put your implementation, if the method name isn’t expected, throw a Not Implemented exception.
  5. From Dart, invoke the method by calling platform.invokeMethod("methodName"). The method name should match exactly the one which the native side is expecting.
  6. To call Dart methods from native, on your Dart code, set a similar handler to the one in native by calling platform.setMethodCallHandler.
  7. From the native side, invoke any method by calling methodChannel.invokeMethod(”methodName”).

Final remarks

It is highly important as a Flutter developer to learn how to implement platform channels. In fact, you’re not going to use it on a daily basis, you might actually never use it. However, it is important to understand how it works, since most of the plugins in the ecosystem are built over native APIs and third-party SDKs.

If you ever came across a case where you had to implement your own platform channels, it is advised that you take your implementation out of you project to a separated plugin project, check this guide from the official docs on how to start your own plugin. Moreover, you should consider publishing your plugin to share it with the community, which is good since more people can try it out and help you improve it!

Finally, this series is still going on! in part 3, we will see how platform channels enable using native UI elements as Flutter widgets.

Follow us on Twitter, LinkedIn, and Youtube, and subscribe to our monthly newsletter to always stay up-to-date.

Acknowledgment

Special thanks to our interns in the period between August and June 2022, Çağla Loğoğlu and Fatimah Dagriri, for helping in polishing this tutorial.

--

--

Mais Alheraki
Invertase

Software engineer, Geek, Designer, Reader & always a Lifetime Learner!