从0开始:Flutter生物识别插件开发

239 阅读19分钟

bio_plugin.png

前言

生物识别技术(如指纹识别和面部识别)在移动应用中的使用越来越广泛,为用户提供了便捷且安全的身份验证方式。本文将介绍如何开发一个Flutter生物识别插件,使您能够在应用中轻松集成生物识别功能。

创建插件

可以使用终端命令或者Android Studio来创建插件项目:

终端命令创建

flutter create --template=plugin --platforms=android,ios biometric_authorization

参数说明:

  • --template=plugin:指定项目模板为插件类型
  • --platforms=android,ios:指定支持的平台
  • biometric_authorization:项目名称

执行该命令会生成一个支持 Android 和 iOS 的 Flutter 插件结构。

Android Studio创建

image.png

在新建Flutter项目时,填写好项目名称,然后要选择Project Type为Plugin,包名可以根据自己的来进行修改,Platforms选择要指定的平台,这里我们要指定的是Android和iOS,选择好后点击创建即可。

目录结构

创建好插件项目后,默认的项目代码目录结构如下:

biometric_authorization/
├── android/               // Android平台特定实现
├── ios/                   // iOS平台特定实现 
├── lib/                   // Dart接口和实现
│   ├── biometric_authorization.dart             // 主API类
│   ├── biometric_authorization_method_channel.dart    // 方法通道实现
│   └──biometric_authorization_platform_interface.dart // 平台接口定义
└── example/               // 示例应用

我们在lib文件夹里面新建一个biometric_type的dart文件,用来定义生物识别的枚举类型,得到新的目录结构:

biometric_authorization/
├── android/               // Android平台特定实现
├── ios/                   // iOS平台特定实现 
├── lib/                   // Dart接口和实现
│   ├── biometric_authorization.dart             // 主API类
│   ├── biometric_authorization_method_channel.dart    // 方法通道实现
│   ├── biometric_authorization_platform_interface.dart // 平台接口定义
│   └── biometric_type.dart                     // 生物识别类型枚举
└── example/               // 示例应用

Dart接口实现

定义生物识别类型

biometric_type.dart文件中,定义了一个枚举表示支持的生物识别方法:面部识别(face)和指纹识别(finngerprint),如果不支持则使用none类型。

/// Biometric Type
///
/// - face: Face ID
/// - fingerprint: Touch ID
/// - none: None
enum BiometricType { face, fingerprint, none }

定义平台接口

平台接口定义了插件的核心功能,所有平台特定实现必须实现这些方法,在biometric_authorization_platform_interface.dart文件中定义。

import 'package:plugin_platform_interface/plugin_platform_interface.dart';

import 'biometric_authorization_method_channel.dart';
import 'biometric_type.dart';

abstract class BiometricAuthorizationPlatform extends PlatformInterface {
  /// Constructs a BiometricAuthorizationPlatform.
  BiometricAuthorizationPlatform() : super(token: _token);

  static final Object _token = Object();

  static BiometricAuthorizationPlatform _instance =
      MethodChannelBiometricAuthorization();

  /// The default instance of [BiometricAuthorizationPlatform] to use.
  ///
  /// Defaults to [MethodChannelBiometricAuthorization].
  static BiometricAuthorizationPlatform get instance => _instance;

  /// Platform-specific implementations should set this with their own
  /// platform-specific class that extends [BiometricAuthorizationPlatform] when
  /// they register themselves.
  static set instance(BiometricAuthorizationPlatform instance) {
    PlatformInterface.verifyToken(instance, _token);
    _instance = instance;
  }

  Future<String?> getPlatformVersion() {
    throw UnimplementedError('platformVersion() has not been implemented.');
  }

  /// Check if biometric is available on the device.
  Future<bool> isBiometricAvailable() async {
    throw UnimplementedError(
      'isBiometricAvailable() has not been implemented.',
    );
  }

  /// Check if biometric is enrolled on the device.
  Future<bool> isBiometricEnrolled() async {
    throw UnimplementedError('isBiometricEnrolled() has not been implemented.');
  }

  /// Get available biometric types on the device.
  Future<List<BiometricType>> getAvailableBiometricTypes() async {
    throw UnimplementedError(
      'getAvailableBiometricTypes() has not been implemented.',
    );
  }

  /// Authenticate with biometric.
  Future<bool> authenticate({
    BiometricType biometricType = BiometricType.none,
    String reason = "Authenticate",
    String? title,
    String? confirmText,
    bool useCustomUI = false,
    bool useDialogUI = false,
    String? cancelText,
  }) async {
    throw UnimplementedError('authenticate() has not been implemented.');
  }
}

这里我们定义了四个生物识别的平台接口方法:

  • isBiometricAvailable:

这个方法用来检系统是否支持生物识别的功能,放回一个bool类型的值。

  • isBiometricEnrolled:

这个方法用来检测系统是否录入了生物识别的信息(录入指纹、面部信息)。

  • getAvailableBiometricTypes:

这个方法用来放回系统支持的所有的生物识别的类型(面部识别、指纹识别)。

  • authenticate:

这个方法用来进行生物识别的认证,接收了一些参数,参数具体的说明在后面接口实现中进行说明。

定义平台接口实现

平台接口实现定义了具体的平台方法逻辑,使用 MethodChannel 与原生系统进行通信。在biometric_authorization_method_channel.dart文件中定义MethodChannelBiometricAuthorization类并继承自 BiometricAuthorizationPlatform,并通过MethodChannel实现与原生平台的通信。

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

import 'biometric_authorization_platform_interface.dart';
import 'biometric_type.dart';

/// An implementation of [BiometricAuthorizationPlatform] that uses method channels.
class MethodChannelBiometricAuthorization
    extends BiometricAuthorizationPlatform {
  /// The method channel used to interact with the native platform.
  @visibleForTesting
  final methodChannel = const MethodChannel('biometric_authorization');

  @override
  Future<String?> getPlatformVersion() async {
    final version = await methodChannel.invokeMethod<String>(
      'getPlatformVersion',
    );
    return version;
  }

  /// Check if biometric is available on the device.
  @override
  Future<bool> isBiometricAvailable() async {
    final result = await methodChannel.invokeMethod<bool>(
      'isBiometricAvailable',
    );
    return result ?? false;
  }

  /// Check if biometric is enrolled on the device.
  @override
  Future<bool> isBiometricEnrolled() async {
    final result = await methodChannel.invokeMethod<bool>(
      'isBiometricEnrolled',
    );
    return result ?? false;
  }

  /// Get available biometric types on the device.
  ///
  /// Returns a list of biometric types that are available on the device.
  @override
  Future<List<BiometricType>> getAvailableBiometricTypes() async {
    final result = await methodChannel.invokeMethod<List<dynamic>>(
      'getAvailableBiometricTypes',
    );

    if (result == null) {
      return [];
    }

    // Convert the string list to a BiometricType enum list
    return result.map<BiometricType>((item) {
      final String type = item.toString();
      switch (type) {
        case 'face':
          return BiometricType.face;
        case 'fingerprint':
          return BiometricType.fingerprint;
        case 'none':
        default:
          return BiometricType.none;
      }
    }).toList();
  }

  /// Initiates biometric authentication using the device's biometric sensors.
  ///
  /// This method triggers the biometric authentication flow, which can use fingerprint,
  /// face recognition, or other biometric methods available on the device.
  ///
  /// Parameters:
  /// - [biometricType]: Specifies the type of biometric authentication to use.
  ///   Required on iOS, optional on Android (Android will automatically use available methods).
  ///   Defaults to [BiometricType.none].
  /// - [reason]: The reason for requesting authentication, displayed to the user.
  ///   Defaults to "Authenticate".
  /// - [title]: The title of the authentication dialog. If null, a default title will be used.
  /// - [confirmText]: The text for the confirmation button in the authentication dialog.
  ///   If null, a default text will be used.
  /// - [useCustomUI]: Whether to use a custom UI for authentication (true) or the system default UI (false).
  ///   Defaults to false.
  /// - [useDialogUI]: Whether to use the Dialog UI for authentication (true) or the new UI (false) in Android.
  ///   Defaults to false.
  /// - [cancelText]: The text for the cancel button in the authentication dialog.
  ///   Only used on Android. If null, a default text ("Cancel") will be used.
  ///
  /// Returns a [Future<bool>] that completes with:
  /// - true: If authentication was successful
  /// - false: If authentication failed or was canceled by the user
  @override
  Future<bool> authenticate({
    BiometricType biometricType = BiometricType.none,
    String reason = "Authenticate",
    String? title,
    String? confirmText,
    bool useCustomUI = false,
    bool useDialogUI = false,
    String? cancelText,
  }) async {
    final Map<String, dynamic> arguments = {
      'biometricType': biometricType.name,
      'reason': reason,
      'title': title,
      'confirmText': confirmText,
      'useCustomUI': useCustomUI,
      'useDialogUI': useDialogUI,
      'cancelText': cancelText,
    };

    final result = await methodChannel.invokeMethod<bool>(
      'authenticate',
      arguments,
    );
    return result ?? false;
  }
}

在使用getAvailableBiometricTypes方法时,对返回的类型字符列表进行解析映射成BiometricType列表。

对于authenticate方法中的各参数说明如下:

  • biometricType:指定要使用的生物识别类型,在iOS上必须传递

  • reason:认证原因说明,用于显示在系统弹窗上

  • title:认证对话框的标题

  • confirmText:确认按钮文字

  • cancelText:取消按钮文字(Android专用)

  • useCustomUI:是否使用自定义 UI(默认 false)

  • useDialogUI:Android 中是否使用旧的对话框 UI(默认 false)

这些参数会被打包成 Map,传递给原生方法:

定义API给Flutter使用

biometric_authorization.dart文件中定义插件API,供Flutter进行调用使用。

import 'biometric_authorization_platform_interface.dart';
import 'biometric_type.dart';

class BiometricAuthorization {
  Future<String?> getPlatformVersion() {
    return BiometricAuthorizationPlatform.instance.getPlatformVersion();
  }

  /// Check if biometric is available on the device.
  Future<bool> isBiometricAvailable() {
    return BiometricAuthorizationPlatform.instance.isBiometricAvailable();
  }

  /// Check if biometric is enrolled on the device.
  Future<bool> isBiometricEnrolled() {
    return BiometricAuthorizationPlatform.instance.isBiometricEnrolled();
  }

  /// Get available biometric types on the device.
  Future<List<BiometricType>> getAvailableBiometricTypes() {
    return BiometricAuthorizationPlatform.instance.getAvailableBiometricTypes();
  }

  /// Initiates biometric authentication using the device's biometric sensors.
  ///
  /// This method triggers the biometric authentication flow, which can use fingerprint,
  /// face recognition, or other biometric methods available on the device.
  ///
  /// Parameters:
  /// - [biometricType]: Specifies the type of biometric authentication to use.
  ///   Required on iOS, optional on Android (Android will automatically use available methods).
  ///   Defaults to [BiometricType.none].
  /// - [reason]: The reason for requesting authentication, displayed to the user.
  ///   Defaults to "Authenticate".
  /// - [title]: The title of the authentication dialog. If null, a default title will be used.
  /// - [confirmText]: The text for the confirmation button in the authentication dialog.
  ///   If null, a default text will be used.
  /// - [useCustomUI]: Whether to use a custom UI for authentication (true) or the system default UI (false).
  ///   Defaults to false.
  /// - [cancelText]: The text for the cancel button in the authentication dialog.
  ///   Only used on Android. If null, a default text ("Cancel") will be used.
  ///
  /// Returns a [Future<bool>] that completes with:
  /// - true: If authentication was successful
  /// - false: If authentication failed or was canceled by the user
  Future<bool> authenticate({
    BiometricType biometricType = BiometricType.none,
    String reason = "Authenticate",
    String? title,
    String? confirmText,
    bool useCustomUI = false,
    bool useDialogUI = false,
    String? cancelText,
  }) {
    return BiometricAuthorizationPlatform.instance.authenticate(
      biometricType: biometricType,
      reason: reason,
      title: title,
      confirmText: confirmText,
      useCustomUI: useCustomUI,
      useDialogUI: useDialogUI,
      cancelText: cancelText,
    );
  }
}

平台特定实现

平台特定实现是指在各特定平台上使用原生代码和方法实现插件中的方法,在iOS上一般使用Object-C/Swift,在Android上一般使用 Java/Kotlin。这里我们使用Swift和Kotlin来实现。

iOS

在ios文件夹中的Classes里面定义了三个文件,ios文件夹的主要目录结构如下:

ios/
├── Assets
├── Classes/
│   ├── BiometricAuthorizationPlugin.swift  // 插件入口文件
│   ├── BiometricAuthorization.swift // 生物识别功能实现文件
│   └── BiometricAuthView.swift // 自定义生物识别的UI
└── Resources

BiometricAuthorizationPlugin

这个文件中定义了BiometricAuthorizationPlugin类,它实现了FlutterPlugin 协议,并负责处理从 Dart 端通过 MethodChannel 发起的调用请求。在这里的handle方法中,call参数包括了Flutter侧调用的方法和参数,result参数则是返回参数。在这里的call.method的名字必须与Dart平台接口实现中的名字一致,否则会进入默认处理逻辑(即 FlutterMethodNotImplemented)。

import Flutter
import UIKit

/**
 * Flutter plugin that provides biometric authentication functionality for iOS.
 * This plugin serves as the bridge between Flutter and native iOS authentication APIs.
 *
 * It implements the FlutterPlugin protocol to handle method calls from Flutter
 * and delegates the actual biometric operations to the BiometricAuthorization class.
 */
public class BiometricAuthorizationPlugin: NSObject, FlutterPlugin {
    /**
     * Registers this plugin with the Flutter engine.
     * Sets up the method channel and plugin instance.
     *
     * @param registrar The Flutter plugin registrar to register with.
     */
    public static func register(with registrar: FlutterPluginRegistrar) {
        let channel = FlutterMethodChannel(name: "biometric_authorization", binaryMessenger: registrar.messenger())
        let instance = BiometricAuthorizationPlugin()
        registrar.addMethodCallDelegate(instance, channel: channel)
    }
    
    /**
     * Handles method calls from Flutter.
     * Routes each method to the appropriate BiometricAuthorization function.
     *
     * @param call The method call from Flutter with method name and arguments.
     * @param result The result callback to send the response back to Flutter.
     */
    public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
        switch call.method {
        case "getPlatformVersion":
            // Returns the iOS version for plugin verification
            result("iOS " + UIDevice.current.systemVersion)
            
        case "isBiometricAvailable":
            // Checks if the device has biometric hardware
            result(BiometricAuthorization.isBiometricAvailable())
            
        case "isBiometricEnrolled":
            // Checks if biometrics are enrolled on the device
            result(BiometricAuthorization.isBiometricEnrolled())
            
        case "getAvailableBiometricTypes":
            // Gets a list of available biometric types on the device
            result(BiometricAuthorization.getAvailableBiometricTypes())
            
        case "authenticate":
            // Performs biometric authentication with parameters from Flutter
            BiometricAuthorization.authenticate(call: call, result: result)
            
        default:
            // Returns not implemented for unknown methods
            result(FlutterMethodNotImplemented)
        }
    }
}

BiometricAuthorization

这个文件定义了iOS侧生物识别的具体实现,基于LocalAuthentication实现,通过LAContext创建上下文对象,使用canEvaluatePolicy来检查是否支持生物识别。

import Foundation
import LocalAuthentication
import SwiftUI
import Flutter

/**
 * Represents the supported biometric authentication types.
 * Maps to the corresponding types defined in the Dart code.
 */
enum BiometricType: String {
    case face = "face"          // Face ID on supported devices
    case fingerprint = "fingerprint"  // Touch ID on supported devices
    case none = "none"          // Fallback when no biometric is available
}

/**
 * Main class that handles biometric authentication functionality.
 * Provides methods to check availability, enrollment, and perform authentication.
 */
class BiometricAuthorization {
    
    /**
     * Creates and configures a new LAContext instance.
     * 
     * @return A configured LAContext instance ready for biometric operations.
     */
    private static func createContext() -> LAContext {
        let context = LAContext()
        return context
    }
    
    /**
     * Checks if biometric authentication is available on the device.
     * This checks if the hardware supports biometrics (Face ID or Touch ID).
     * 
     * @return Boolean indicating if biometric authentication is available.
     */
    static func isBiometricAvailable() -> Bool {
        let context = createContext()
        var error:  NSError?
        let available = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
        return available
    }
    
    /**
     * Checks if biometric authentication is enrolled on the device.
     * This verifies if the user has registered their biometrics (face or fingerprint).
     * 
     * @return Boolean indicating if biometrics are enrolled.
     */
    static func isBiometricEnrolled() -> Bool {
        let context = createContext()
        var error: NSError?
        _ = context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error)
        if let err = error as? LAError {
            return err.code != .biometryNotEnrolled
        }
        return true
    }
    
    /**
     * Determines which biometric types are available on the device.
     * 
     * @return Array of strings representing available biometric types.
     *         Returns ["none"] if no biometrics are available.
     */
    static func getAvailableBiometricTypes() -> [String] {
        let context = createContext()
        var error: NSError?
        var biometricTypes: [String] = []
        
        if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
            switch context.biometryType {
            case .faceID:
                biometricTypes.append(BiometricType.face.rawValue)
            case .touchID:
                biometricTypes.append(BiometricType.fingerprint.rawValue)
            default:
                break
            }
        }
        
        return biometricTypes.isEmpty ? [BiometricType.none.rawValue] : biometricTypes
    }
    
    /**
     * Main authentication method called from Flutter through the method channel.
     * Determines whether to use standard system authentication or custom UI.
     * 
     * @param call The Flutter method call containing authentication parameters.
     * @param result The Flutter result callback to return the authentication outcome.
     */
    static func authenticate(call: FlutterMethodCall, result: @escaping FlutterResult) {
        let context = createContext()
        
        // Extract parameters from the method call
        guard let args = call.arguments as? [String: Any],
              let biometricType = args["biometricType"] as? String,
              let reason = args["reason"] as? String
        else {
            result(false)
            return
        }
        let title = args["title"] as? String
        let confirmText = args["confirmText"] as? String
        let useCustomUI = args["useCustomUI"] as? Bool ?? false
        
        let policy = LAPolicy.deviceOwnerAuthenticationWithBiometrics
        
        if #available(iOS 13.0, *) {
            if useCustomUI &&
                (biometricType == BiometricType.face.rawValue ||
                biometricType == BiometricType.fingerprint.rawValue) {
                // Use custom SwiftUI-based authentication UI
                showCustomUI(
                    biometricType: biometricType,
                    title: title,
                    confirmText: confirmText,
                    context: context,
                    policy: policy,
                    reason: reason,
                    result: result
                )
            } else {
                // Use standard system authentication dialog
                authenticateStandard(
                    context: context,
                    policy: policy,
                    reason: reason,
                    result: result
                )
            }
        }
    }
    
    /**
     * Initiates the custom UI authentication flow.
     * This is a wrapper method that calls the async presentation method.
     * 
     * @param biometricType The type of biometric to authenticate with.
     * @param title Optional title for the authentication dialog.
     * @param confirmText Optional text for the confirm button.
     * @param context The LAContext instance for biometric operations.
     * @param policy The authentication policy to use.
     * @param reason The reason for authentication to display to user.
     * @param result The Flutter result callback.
     */
    @available(iOS 13.0, *)
    private static func showCustomUI(
        biometricType: String,
        title: String?,
        confirmText: String?,
        context: LAContext,
        policy: LAPolicy,
        reason: String,
        result: @escaping FlutterResult
    ) {
        Task {
            await presentBiometricSheet(
                context: context,
                policy: policy,
                reason: reason,
                title: title,
                confirmText: confirmText,
                biometricType: biometricType,
                result: result
            )
        }
    }
    
    /**
     * Presents the custom biometric authentication sheet using SwiftUI.
     * Handles different iOS versions with appropriate UI adaptations.
     * 
     * @param context The LAContext instance for biometric operations.
     * @param policy The authentication policy to use.
     * @param reason The reason for authentication to display to user.
     * @param title Optional title for the authentication dialog.
     * @param confirmText Optional text for the confirm button.
     * @param biometricType The type of biometric to authenticate with.
     * @param result The Flutter result callback.
     */
    @available(iOS 13.0, *)
    @MainActor
    private static func presentBiometricSheet(
        context: LAContext,
        policy: LAPolicy,
        reason: String,
        title: String?,
        confirmText: String?,
        biometricType: String,
        result: @escaping FlutterResult
    ) {
        // Get the key window and root view controller
        guard let keyWindow = getKeyWindow(),
              let rootViewController = keyWindow.rootViewController else {
            authenticateStandard(
                context: context,
                policy: policy,
                reason: reason,
                result: result
            )
            return
        }
        
        // Create the SwiftUI view for biometric authentication
        let contentView = BiometricAuthView(
            title: title ?? getBiometricTitle(type: biometricType),
            reason: reason,
            buttonText: confirmText ?? "Authenticate",
            biometricType: biometricType,
            onAuthenticate: result
        )
        
        let hostingController = UIHostingController(rootView: contentView)
        
        if #available(iOS 15.0, *) {
            // iOS 15+ uses the new sheet presentation API
            hostingController.modalPresentationStyle = .pageSheet
            
            if let sheet = hostingController.sheetPresentationController {
                if #available(iOS 16.0, *) {
                    // iOS 16+ supports custom height using fraction
                    sheet.detents = [
                        .custom { context in
                            context.maximumDetentValue * 0.35
                        }
                    ]
                } else {
                    // iOS 15 only supports predefined detents
                    sheet.detents = [.medium()]
                    hostingController.view.heightAnchor.constraint(
                        equalToConstant: UIScreen.main.bounds.height * 0.35
                    ).isActive = true
                }
                
                sheet.preferredCornerRadius = 25
                sheet.prefersGrabberVisible = true
            }
        } else {
            // iOS 13-14 uses the older form sheet presentation
            hostingController.modalPresentationStyle = .formSheet
            hostingController.preferredContentSize = CGSize(
                width: UIScreen.main.bounds.width,
                height: UIScreen.main.bounds.height * 0.35
            )
            hostingController.view.backgroundColor = .systemBackground
            hostingController.view.layer.cornerRadius = 20
            hostingController.view.clipsToBounds = true
        }
        
        // Present the authentication sheet
        rootViewController.present(hostingController, animated: true)
    }
    
    /**
     * Helper method to get the key window in iOS 13+.
     * 
     * @return The key UIWindow instance or nil if not found.
     */
    @available(iOS 13.0, *)
    @MainActor
    private static func getKeyWindow() -> UIWindow? {
        return UIApplication
            .shared
            .connectedScenes
            .compactMap { $0 as? UIWindowScene }
            .flatMap { $0.windows }
            .first { $0.isKeyWindow }
    }
    
    /**
     * Returns the appropriate title for the authentication dialog based on biometric type.
     * 
     * @param type The biometric type string.
     * @return A user-friendly title for the authentication dialog.
     */
    private static func getBiometricTitle(type: String) -> String {
        switch type {
        case BiometricType.face.rawValue:
            return "Face ID Authentication"
        case BiometricType.fingerprint.rawValue:
            return "Touch ID Authentication"
        default:
            return "Biometric Authentication"
        }
    }
    
    /**
     * Performs standard system biometric authentication without custom UI.
     * 
     * @param context The LAContext instance for biometric operations.
     * @param policy The authentication policy to use.
     * @param reason The reason for authentication to display to user.
     * @param result The Flutter result callback.
     */
    @available(iOS 13.0, *)
    private static func authenticateStandard(
        context: LAContext,
        policy: LAPolicy,
        reason: String,
        result: @escaping FlutterResult
    ) {
        context.evaluatePolicy(policy, localizedReason: reason) { success, error in
            Task {
                if success {
                    result(true)
                } else {
                    result(false)
                }
            }
        }
    }
}

BiometricAuthView

这个文件定义了自定义生物识别的UI样式,就是自定义了一个底部的弹出,使用了SwiftUI。

import SwiftUI
import LocalAuthentication

/**
 * A SwiftUI view that provides a custom UI for biometric authentication.
 * This view displays a stylish interface with an animated biometric icon,
 * title, and authentication button.
 *
 * The view adapts to different screen sizes through responsive design.
 * Compatible with iOS 13.0 and later.
 */
@available(iOS 13.0, *)
struct BiometricAuthView: View {
    // Animation state variables
    @State private var isAnimating = false
    @State private var isAuthenticating = false
    
    // Environment access to dismiss the view
    @Environment(\.presentationMode) var presentationMode
    
    // View configuration properties
    var title: String
    var reason: String
    var buttonText: String
    var biometricType: String
    var onAuthenticate: (Bool) -> Void
    
    var body: some View {
        // GeometryReader allows the view to adapt to different screen sizes
        GeometryReader { geometry in
            let width = geometry.size.width
            let height = geometry.size.height
            let iconSize = min(width, height) * 0.35
            let fontSize = min(width, height) * 0.15
            
            VStack(spacing: height * 0.03) {
                // Title text
                Text(title)
                    .font(.headline)
                    .fontWeight(.semibold)
                    .lineLimit(2)
                    .multilineTextAlignment(.center)
                
                Spacer()
                    .frame(height: height * 0.02)
                
                // Animated biometric icon
                ZStack {
                    // Background circle
                    Circle()
                        .fill(Color.blue.opacity(0.1))
                        .frame(width: iconSize, height: iconSize)
                    
                    // Biometric icon (Face ID or Touch ID)
                    Image(systemName: getSystemImageName())
                        .font(.system(size: fontSize))
                        .foregroundColor(.blue)
                        .scaleEffect(isAnimating ? 1.1 : 1.0)
                        .onAppear {
                            // Create a continuous pulse animation
                            withAnimation(
                                Animation.easeInOut(duration: 1.0)
                                    .repeatForever(autoreverses: true)
                            ) {
                                isAnimating = true
                            }
                        }
                }
                
                Spacer()
                    .frame(height: height * 0.03)
                
                // Authentication button
                Button {
                    authorizationWithBiometric()
                } label: {
                    HStack {
                        Text(buttonText)
                            .font(.system(size: fontSize * 0.5))
                            .fontWeight(.semibold)
                        
                        Image(systemName: "arrow.right")
                            .font(.system(size: fontSize * 0.5))
                    }
                    .frame(maxWidth: .infinity)
                    .padding(EdgeInsets(
                        top: height * 0.05,
                        leading: width * 0.05,
                        bottom: height * 0.05,
                        trailing: width * 0.05
                    ))
                    .background(
                        ZStack {
                            LinearGradient(
                                gradient: Gradient(colors: [
                                    Color.blue,
                                    Color.blue.opacity(0.8)
                                ]),
                                startPoint: .leading,
                                endPoint: .trailing
                            )
                        }
                    )
                    .foregroundColor(.white)
                    .cornerRadius(15)
                    .shadow(color: Color.blue.opacity(0.3), radius: 5, x: 0, y: 3)
                }
                .padding(.horizontal, width * 0.05)
                .disabled(isAuthenticating)
            }
            .padding(width * 0.05)
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
            .padding(.top, height * 0.03)
        }
        .onDisappear {
            isAnimating = false
        }
    }
    
    /**
     * Determines the system icon name based on the biometric type.
     *
     * @return The SF Symbol name for the appropriate biometric type.
     */
    func getSystemImageName() -> String {
        switch biometricType {
        case "face":
            return "faceid"
        case "fingerprint":
            return "touchid"
        default:
            return "person.circle"
        }
    }
    
    /**
     * Initiates the biometric authentication process when the button is tapped.
     * Uses LocalAuthentication framework to authenticate with the device biometrics.
     * The result is communicated back via the onAuthenticate callback.
     */
    func authorizationWithBiometric() {
        isAuthenticating = true
        let context = LAContext()
        var error: NSError?
        
        if context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) {
            context.evaluatePolicy(
                .deviceOwnerAuthenticationWithBiometrics,
                localizedReason: reason
            ) { success, error in
                Task {
                    isAuthenticating = false
                    if success {
                        // Authentication successful
                        onAuthenticate(true)
                        presentationMode.wrappedValue.dismiss()
                    } else {
                        // Authentication failed
                        onAuthenticate(false)
                    }
                }
            }
        } else {
            // Device cannot use biometric authentication
            isAuthenticating = false
            onAuthenticate(false)
        }
    }
}

Android

在android文件夹中的mian里面定义了5个文件,android文件夹的主要目录结构如下:

android/src/main/kotlin/com/maojiu/biometric_authorization/
│   ├── BiometricAuthBottomSheet.kt
│   ├── BiometricAuthorizationManager.kt
│   ├── BiometricAuthorizationPlugin
│   ├── FingerprintAuthDialog.kt
│   ├── FingerprintDialogFragment.kt
└── AndroidMandiest.xml

BiometricAuthorizationPlugin

这个是Android侧的插件入口文件。

/**
 * BiometricAuthorizationPlugin.kt
 *
 * Main entry point for the Flutter plugin that provides biometric authentication capabilities.
 * This plugin serves as a bridge between Flutter code and native Android biometric APIs.
 * It implements necessary Flutter plugin interfaces and handles method calls from the Flutter side.
 */
package com.maojiu.biometric_authorization

import android.app.Activity
import android.content.Context
import androidx.fragment.app.FragmentActivity
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.embedding.engine.plugins.activity.ActivityAware
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result

/**
 * Main plugin class that implements Flutter plugin interfaces.
 *
 * This class handles the plugin lifecycle and method calls from Flutter,
 * and delegates the actual biometric operations to the BiometricAuthorizationManager.
 * It implements:
 * - FlutterPlugin: For plugin registration and lifecycle management
 * - MethodCallHandler: For handling method calls from Flutter
 * - ActivityAware: For accessing the current activity which is required for UI operations
 */
class BiometricAuthorizationPlugin : FlutterPlugin, MethodCallHandler, ActivityAware {
  /**
   * The MethodChannel that will be used to communicate with the Flutter side
   */
  private lateinit var channel: MethodChannel
  
  /**
   * Application context provided by the Flutter engine
   */
  private lateinit var context: Context
  
  /**
   * Current activity, needed for UI operations like showing biometric prompts
   */
  private var activity: FragmentActivity? = null

  /**
   * Called when the plugin is attached to the Flutter engine.
   * 
   * Sets up the MethodChannel and initializes the context.
   *
   * @param flutterPluginBinding Provides access to the Flutter engine's resources
   */
  override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
    channel = MethodChannel(flutterPluginBinding.binaryMessenger, "biometric_authorization")
    channel.setMethodCallHandler(this)
    context = flutterPluginBinding.applicationContext
  }

  /**
   * Called when the plugin is attached to an activity.
   * 
   * Stores the current activity for later use.
   *
   * @param binding Provides access to the current activity
   */
  override fun onAttachedToActivity(binding: ActivityPluginBinding) {
    activity = binding.activity as? FragmentActivity
  }

  /**
   * Called when the plugin is detached from the activity for configuration changes.
   * 
   * Clears the stored activity reference.
   */
  override fun onDetachedFromActivityForConfigChanges() {
    activity = null
  }

  /**
   * Called when the plugin is reattached to the activity after configuration changes.
   * 
   * Updates the stored activity reference.
   *
   * @param binding Provides access to the current activity
   */
  override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
    activity = binding.activity as? FragmentActivity
  }

  /**
   * Called when the plugin is detached from the activity.
   * 
   * Clears the stored activity reference.
   */
  override fun onDetachedFromActivity() {
    activity = null
  }

  /**
   * Handles method calls from the Flutter side.
   * 
   * This method routes incoming calls to appropriate methods in the BiometricAuthorizationManager.
   * It first checks if the activity is available before proceeding with the method call.
   *
   * @param call The method call from Flutter containing the method name and arguments
   * @param result The result callback to send the result back to Flutter
   */
  override fun onMethodCall(call: MethodCall, result: Result) {
    val currentActivity = activity
    if (currentActivity == null) {
      result.error("ACTIVITY_NOT_AVAILABLE", "Activity is not available", null)
      return
    }

    // Create a manager instance to handle the biometric operations
    val biometricAuthorizationManager = BiometricAuthorizationManager(context, currentActivity)

    // Route the method call to the appropriate handler
    when (call.method) {
      "getPlatformVersion" -> {
        // Return the Android version
        result.success("Android ${android.os.Build.VERSION.RELEASE}")
      }
      "isBiometricAvailable" -> {
        // Check if biometric authentication is available on the device
        result.success(biometricAuthorizationManager.isBiometricAvailable())
      }
      "isBiometricEnrolled" -> {
        // Check if biometric credentials are enrolled on the device
        result.success(biometricAuthorizationManager.isBiometricEnrolled())
      }
      "getAvailableBiometricTypes" -> {
        // Get the list of available biometric types on the device
        result.success(biometricAuthorizationManager.getAvailableBiometricTypes())
      }
      "authenticate" -> {
        // Initiate the biometric authentication process
        biometricAuthorizationManager.authenticate(call, result)
      }
      else -> {
        // Method not implemented
        result.notImplemented()
      }
    }
  }

  /**
   * Called when the plugin is detached from the Flutter engine.
   * 
   * Cleans up resources by removing the method call handler.
   *
   * @param binding The binding that was providing access to the Flutter engine's resources
   */
  override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
    channel.setMethodCallHandler(null)
  }
}

BiometricAuthorizationManager

这个文件是生物识别的具体实现。

/**
 * BiometricAuthorizationManager.kt
 *
 * Core manager for biometric authentication operations in the Flutter plugin.
 * This class handles biometric availability checks, enrollment status, and authentication processes.
 * It supports both standard system UI and custom bottom sheet UI for biometric authentication.
 */
@file:Suppress("DEPRECATION")

package com.maojiu.biometric_authorization

import android.annotation.SuppressLint
import android.content.Context
import androidx.biometric.BiometricManager
import android.content.pm.PackageManager
import android.os.Build
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel.Result
import androidx.core.content.ContextCompat
import androidx.biometric.BiometricPrompt
import androidx.core.hardware.fingerprint.FingerprintManagerCompat
import androidx.fragment.app.FragmentActivity
import java.security.KeyStore
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import android.util.Log
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import androidx.fragment.app.DialogFragment
import java.util.concurrent.atomic.AtomicBoolean

/**
 * Enum class representing the supported biometric authentication types.
 *
 * @property rawValue String value representation of the biometric type
 */
enum class BiometricType(val rawValue: String) {
    face("face"),
    fingerprint("fingerprint"),
    none("none")
}

/**
 * Constants for fingerprint error codes from FingerprintManager
 * These are not available in FingerprintManagerCompat but are needed for error handling
 */
object FingerprintConstants {
    const val FINGERPRINT_ERROR_CANCELED = 5
    const val FINGERPRINT_ERROR_USER_CANCELED = 10
    const val FINGERPRINT_ERROR_LOCKOUT = 7
    const val FINGERPRINT_ERROR_LOCKOUT_PERMANENT = 9
}

/**
 * Manager class that handles all biometric authentication operations.
 *
 * This class provides methods to check biometric availability, enrollment status,
 * and handles the authentication flow using either the system UI or a custom UI.
 *
 * @param context The application context
 * @param activity The current activity, needed for UI operations
 */
@Suppress("DEPRECATION")
class BiometricAuthorizationManager(
    private val context: Context,
    private val activity: FragmentActivity
) {
    /**
     * use biometricManager to used new UI for biometric authentication
     * use fingerprintManager to used deprecated UI for fingerprint authentication 
     */
    private val biometricManager = BiometricManager.from(context)
    @SuppressLint("RestrictedApi")
    private val fingerprintManager = FingerprintManagerCompat.from(context)

    private val packageManager: PackageManager = context.packageManager

    private lateinit var biometricPrompt: BiometricPrompt
    private lateinit var promptInfo: BiometricPrompt.PromptInfo

    /**
     * Checks if biometric authentication is available on the device.
     *
     * This method verifies that the device has the necessary hardware and
     * system support for strong biometric authentication.
     *
     * @return true if biometric authentication is available, false otherwise
     */
    fun isBiometricAvailable(): Boolean {
        val canAuthenticateResult = biometricManager.canAuthenticate(
            BiometricManager.Authenticators.BIOMETRIC_STRONG
        )
        return canAuthenticateResult == BiometricManager.BIOMETRIC_SUCCESS
    }

    /**
     * Checks if biometric credentials are enrolled on the device.
     *
     * This verifies that the user has set up at least one biometric credential
     * (fingerprint, face, etc.) that can be used for authentication.
     *
     * @return true if biometric credentials are enrolled, false otherwise
     */
    fun isBiometricEnrolled(): Boolean {
        val canAuthenticateResult = biometricManager.canAuthenticate(
            BiometricManager.Authenticators.BIOMETRIC_STRONG
        )
        return canAuthenticateResult == BiometricManager.BIOMETRIC_SUCCESS
    }

    /**
     * Gets a list of available biometric authentication types on the device.
     *
     * This method checks which biometric features are supported by the device hardware.
     * If no biometric features are available, it returns a list containing only "none".
     *
     * @return List of string values representing available biometric types
     */
    fun getAvailableBiometricTypes(): List<String> {
        val availableTypes = mutableListOf<String>()

        // Check for fingerprint hardware support
        if (packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
            availableTypes.add(BiometricType.fingerprint.rawValue)
        }

        // Check for face authentication hardware support (Android 10+)
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            if (packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)) {
                availableTypes.add(BiometricType.face.rawValue)
            }
        }

        // If no biometric types are available, add "none"
        if (availableTypes.isEmpty()) {
            availableTypes.add(BiometricType.none.rawValue)
        }

        return availableTypes
    }

    /**
     * Initiates the biometric authentication process based on Flutter method call parameters.
     *
     * This method handles the authentication flow using either the standard system UI
     * or a custom bottom sheet UI based on the useCustomUI parameter.
     *
     * @param call The method call from Flutter containing authentication parameters
     * @param result The result callback to send the authentication result back to Flutter
     */
    fun authenticate(call: MethodCall, result: Result) {
        val args = call.arguments as? Map<*, *>
        if (args == null) {
            result.error("INVALID_ARGS", "Arguments cannot be null", null)
            return
        }
        val reason = args["reason"] as? String ?: "Authenticate required"
        val title = args["title"] as? String ?: "Biometric Authentication"
        val confirmText = args["confirmText"] as? String ?: "Authenticate"
        val useCustomUI = args["useCustomUI"] as? Boolean ?: false
        val useDialogUI = args["useDialogUI"] as? Boolean ?: false
        val cancelText = args["cancelText"] as? String ?: "Cancel"

        try {
            // Check if biometric authentication is available
            if (!isBiometricAvailable()) {
                result.error(
                    "BIOMETRIC_UNAVAILABLE",
                    "Biometric authentication is not available on this device.",
                    null
                )
                return
            }
            // Check if biometric credentials are enrolled
            if (!isBiometricEnrolled()) {
                result.error(
                    "BIOMETRIC_NOT_ENROLLED",
                    "No biometric features are enrolled on this device.",
                    null
                )
                return
            }

            if (useCustomUI) {
                try {
                    // Set up biometric authentication with custom UI
                    setupBiometricAuth(title, reason, cancelText, activity) { success ->
                        try {
                            result.success(success)
                        } catch (e: Exception) {
                            // Ignore exceptions during result callback
                        }
                    }
                    // Show custom bottom sheet UI
                    BiometricAuthBottomSheet(title, confirmText) {
                        try {
                            // Set up biometric authentication with dialog UI
                            if (useDialogUI) {
                                startFingerprintAuth(result, title, cancelText)
                            } else {
                                startBiometricAuth()
                            }
                        } catch (e: Exception) {
                            result.error("BIOMETRIC_ERROR", e.message, null)
                        }
                    }.show(activity.supportFragmentManager, "biometric_auth_bottom_sheet")
                } catch (e: Exception) {
                    result.error("BIOMETRIC_ERROR", "Failed to start biometric authentication: ${e.message}", null)
                }
            } else {
                try {
                    // Set up biometric authentication with dialog UI
                    if (useDialogUI) {
                        startFingerprintAuth(result, title, cancelText)
                        return
                    }

                    // Set up biometric authentication with standard system UI
                    setupBiometricAuth(title, reason, cancelText, activity) { success ->
                        try {
                            result.success(success)
                        } catch (e: Exception) {
                            // Ignore exceptions during result callback
                        }
                    }
                    // Start authentication with standard UI
                    startBiometricAuth()
                } catch (e: Exception) {
                    result.error("BIOMETRIC_ERROR", "Failed to start biometric authentication: ${e.message}", null)
                }
            }
        } catch (e: Exception) {
            result.error("UNEXPECTED_ERROR", "Error during biometric authentication: ${e.message}", null)
        }
    }

    /**
     * Starts the fingerprint authentication process used with the deprecated UI.
     *
     * This method is used when the useDeprecatedUI parameter is set to true.
     *
     * Android 10 and above only support fingerprint authentication.
     * 
     * @param result The result callback to send the authentication result back to Flutter
     */
    @SuppressLint("RestrictedApi", "MissingPermission")
    private fun startFingerprintAuth(result: Result, title: String, cancelText: String) {
        // Flag to ensure result is called only once
        val resultSent = AtomicBoolean(false)

        // Wrapper for result callback to prevent multiple calls
        val safeResult = object {
            fun success(value: Any?) {
                if (resultSent.compareAndSet(false, true)) {
                    activity.runOnUiThread {
                        try { result.success(value) } catch (e: Exception) { Log.w("BiometricAuth", "Result success error: ${e.message}") }
                    }
                }
            }
            fun error(code: String, message: String?, details: Any?) {
                if (resultSent.compareAndSet(false, true)) {
                    activity.runOnUiThread {
                        try { result.error(code, message, details) } catch (e: Exception) { Log.w("BiometricAuth", "Result error error: ${e.message}") }
                    }
                }
            }
        }

        // Check if the device supports fingerprint authentication
        if (!fingerprintManager.isHardwareDetected) {
            Log.d("BiometricAuth", "Device does not support fingerprint authentication")
            safeResult.error(
                "FINGERPRINT_UNAVAILABLE",
                "Device does not support fingerprint authentication",
                null
            )
            return
        }

        // Check if there are any enrolled fingerprints
        if (!fingerprintManager.hasEnrolledFingerprints()) {
            Log.d("BiometricAuth", "No fingerprints are enrolled on this device")
            safeResult.error(
                "FINGERPRINT_NOT_ENROLLED",
                "No fingerprints are enrolled on this device",
                null
            )
            return
        }

        // Create a crypto object as an authentication token
        val cryptoObject = createCryptoObject()
        if (cryptoObject == null) {
            Log.d("BiometricAuth", "Failed to create CryptoObject")
            safeResult.error(
                "FINGERPRINT_CRYPTO_ERROR",
                "Failed to create cryptographic object for fingerprint authentication",
                null
            )
            return
        }

        // Create a cancellation signal for the authentication
        val cancellationSignal = androidx.core.os.CancellationSignal()

        // Create authentication callback
        val callback = object : FingerprintManagerCompat.AuthenticationCallback() {
            override fun onAuthenticationSucceeded(authResult: FingerprintManagerCompat.AuthenticationResult) {
                // Authentication succeeded
                Log.d("BiometricAuth", "Fingerprint authentication succeeded")
                // Dismiss the dialog if it's still showing
                activity.supportFragmentManager.findFragmentByTag("FingerprintDialogFragment")?.let {
                    (it as? DialogFragment)?.dismissAllowingStateLoss()
                }
                safeResult.success(true)
            }

            override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                // Authentication error
                Log.d("BiometricAuth", "Fingerprint authentication error: $errString ($errorCode)")
                // Dismiss the dialog if it's still showing
                activity.supportFragmentManager.findFragmentByTag("FingerprintDialogFragment")?.let {
                    (it as? DialogFragment)?.dismissAllowingStateLoss()
                }
                when (errorCode) {
                    FingerprintConstants.FINGERPRINT_ERROR_CANCELED,
                    FingerprintConstants.FINGERPRINT_ERROR_USER_CANCELED -> {
                        // User canceled authentication via system prompt or custom dialog cancel
                        safeResult.success(false)
                    }
                    FingerprintConstants.FINGERPRINT_ERROR_LOCKOUT,
                    FingerprintConstants.FINGERPRINT_ERROR_LOCKOUT_PERMANENT -> {
                        // Device is locked out
                        safeResult.success(false) // Reporting false, could be a specific error too
                    }
                    else -> {
                        // Other errors
                        safeResult.error(
                            "FINGERPRINT_ERROR",
                            errString.toString(),
                            errorCode // Include error code in details
                        )
                    }
                }
            }

            override fun onAuthenticationFailed() {
                // Authentication failed but can be retried
                Log.d("BiometricAuth", "Fingerprint authentication failed but can be retried")
            }
        }

        /**
         * Start fingerprint authentication
         *
         * Parameters:
         * @param crypto: the crypto object
         * @param flags: optional flags, usually 0  
         * @param cancel: a cancellation signal object to cancel the authentication
         * @param callback: the callback that receives authentication results
         * @param handler: handler for delivering messages, or null for default handler
         */
        fingerprintManager.authenticate(
            cryptoObject,
            0,
            cancellationSignal,
            callback,
            null
        )

        // --- Display the Custom Dialog --- 
        val fragmentManager = activity.supportFragmentManager
        // Ensure previous dialog is dismissed if any (e.g., rapid calls)
        fragmentManager.findFragmentByTag("FingerprintDialogFragment")?.let {
            (it as? DialogFragment)?.dismissAllowingStateLoss()
        }
        val dialogFragment = FingerprintDialogFragment.newInstance(title, cancelText) {
            // onCancel lambda from custom dialog
            Log.d("BiometricAuth", "Custom dialog cancelled by user.")
            if (!cancellationSignal.isCanceled) {
                 // IMPORTANT: Calling cancel here will trigger the onAuthenticationError callback
                 // with FINGERPRINT_ERROR_CANCELED. The callback will handle sending the result.
                cancellationSignal.cancel()
            }
        }
        // Show the dialog. It will appear over the activity while the fingerprint manager attempts auth.
        dialogFragment.show(fragmentManager, "FingerprintDialogFragment")
    }

    /**
     * Creates a cryptographic object for fingerprint authentication
     * 
     * @return Crypto object, or null if creation fails
     */
    @SuppressLint("RestrictedApi")
    private fun createCryptoObject(): FingerprintManagerCompat.CryptoObject? {
        // Early return for devices below Marshmallow
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            Log.e("BiometricAuth", "Fingerprint authentication requires Android 6.0 or above")
            return null
        }
        
        try {
            // Create and get Android KeyStore instance
            val keyStore = KeyStore.getInstance("AndroidKeyStore")
            keyStore.load(null)
            
            // Key alias
            val keyName = "com.maojiu.biometric_authorization.key"
            
            // Check if the key already exists, create it if not
            if (!keyStore.containsAlias(keyName)) {
                val keyGenerator = KeyGenerator.getInstance(
                    KeyProperties.KEY_ALGORITHM_AES, 
                    "AndroidKeyStore"
                )
                
                val builder = KeyGenParameterSpec.Builder(
                    keyName,
                    KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
                )
                    .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
                    .setUserAuthenticationRequired(true)
                    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
                
                // Set authentication validity period if API level supports it
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
                    builder.setInvalidatedByBiometricEnrollment(true)
                }
                
                keyGenerator.init(builder.build())
                keyGenerator.generateKey()
            }
            
            // Get the key and initialize Cipher
            val key = keyStore.getKey(keyName, null) as SecretKey
            val cipher = Cipher.getInstance(
                KeyProperties.KEY_ALGORITHM_AES + "/" +
                KeyProperties.BLOCK_MODE_CBC + "/" +
                KeyProperties.ENCRYPTION_PADDING_PKCS7
            )
            
            // Initialize the Cipher for encryption mode
            cipher.init(Cipher.ENCRYPT_MODE, key)
            
            // Create and return CryptoObject
            return FingerprintManagerCompat.CryptoObject(cipher)
        } catch (e: Exception) {
            // Log the error but don't throw an exception, return null to indicate crypto object creation failed
            Log.e("BiometricAuth", "Failed to create CryptoObject: ${e.message}", e)
            return null
        }
    }

    /**
     * Starts the biometric authentication process using the configured prompt.
     *
     * This method triggers the system biometric authentication dialog.
     */
    private fun startBiometricAuth() {
        biometricPrompt.authenticate(promptInfo)
    }

    /**
     * Sets up the biometric authentication components.
     *
     * This method configures the BiometricPrompt with appropriate callbacks and 
     * builds the prompt information with the specified parameters.
     *
     * @param title The title to display in the authentication dialog
     * @param reason The description/reason for requesting authentication
     * @param cancelText The text for the cancel button
     * @param activity The activity context for the authentication UI
     * @param onResult Callback function that receives the authentication result (true for success, false for failure)
     */
    private fun setupBiometricAuth(
        title: String,
        reason: String,
        cancelText: String,
        activity: FragmentActivity,
        onResult: (Boolean) -> Unit
    ) {
        val executor = ContextCompat.getMainExecutor(activity)
        biometricPrompt = BiometricPrompt(activity, executor,
            object : BiometricPrompt.AuthenticationCallback() {
                /**
                 * Called when authentication is successful.
                 */
                override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
                    onResult.invoke(true)
                }

                /**
                 * Called when authentication fails but can be retried.
                 * This doesn't count as a final failure, so we don't invoke the result callback
                 * to allow the user to retry.
                 */
                override fun onAuthenticationFailed() {
                    // Authentication failed but can be retried
                    // Don't call onResult here to allow the user to continue trying
                }

                /**
                 * Called when an authentication error occurs.
                 *
                 * Handles different error codes and determines appropriate responses:
                 * - User cancellation: Returns false without an exception
                 * - Device lockout: Returns false without an exception
                 * - Other errors: Returns false without an exception
                 *
                 * @param errorCode The error code from BiometricPrompt
                 * @param errString The error message
                 */
                override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
                    // Handle different types of errors
                    when (errorCode) {
                        BiometricPrompt.ERROR_CANCELED,
                        BiometricPrompt.ERROR_USER_CANCELED,
                        BiometricPrompt.ERROR_NEGATIVE_BUTTON -> {
                            // User canceled authentication, return false without raising an exception
                            onResult.invoke(false)
                        }
                        BiometricPrompt.ERROR_LOCKOUT,
                        BiometricPrompt.ERROR_LOCKOUT_PERMANENT -> {
                            // Device is locked out, return false without raising an exception
                            onResult.invoke(false)
                        }
                        else -> {
                            // Other errors, return false without raising an exception
                            onResult.invoke(false)
                        }
                    }
                }
            })

        // Build the prompt information with the specified parameters
        promptInfo = BiometricPrompt.PromptInfo.Builder()
            .setTitle(title)
            .setSubtitle(reason)
            .setNegativeButtonText(cancelText)
            .setAllowedAuthenticators(
                BiometricManager.Authenticators.BIOMETRIC_STRONG
            )
            .build()
    }
}

BiometricAuthBottomSheet

这个文件自定义了底部的弹出UI。

/**
 * BiometricAuthBottomSheet.kt
 * This file implements a custom bottom sheet dialog for biometric authentication using Jetpack Compose.
 * It provides a user-friendly UI for biometric authentication with support for both light and dark themes.
 */
package com.maojiu.biometric_authorization

import android.app.Dialog
import android.os.Bundle
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.Face
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.fragment.app.DialogFragment
import androidx.lifecycle.setViewTreeLifecycleOwner
import com.google.android.material.bottomsheet.BottomSheetDialog

/**
 * A bottom sheet dialog fragment that displays a biometric authentication UI.
 *
 * This class creates a custom bottom sheet dialog with Jetpack Compose UI that
 * shows a face icon animation and a confirmation button for biometric authentication.
 *
 * @param title The title text to display in the bottom sheet
 * @param confirmText The text for the confirmation button
 * @param onConfirmClick Callback function that is triggered when the user clicks the confirm button
 */
class BiometricAuthBottomSheet(
    private val title: String,
    private val confirmText: String,
    private val onConfirmClick: () -> Unit
) : DialogFragment() {
    /**
     * Creates and configures the bottom sheet dialog.
     *
     * This method initializes the dialog with a Compose UI that automatically adapts
     * to the system's theme (light or dark mode).
     *
     * @param savedInstanceState The saved instance state bundle
     * @return A configured BottomSheetDialog instance
     */
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        return BottomSheetDialog(requireContext()).apply {
            setContentView(ComposeView(requireContext()).apply {
                setViewTreeLifecycleOwner(this@BiometricAuthBottomSheet)

                // Set up the Compose content with theme support
                setContent {
                    // Detect if system is in dark mode
                    val isDarkTheme = isSystemInDarkTheme()
                    // Apply the appropriate Material theme based on system settings
                    MaterialTheme(
                        colorScheme = if (isDarkTheme) darkColorScheme() else lightColorScheme()
                    ) {
                        BiometricAuthContent(
                            title = title,
                            confirmText = confirmText,
                            onConfirmClick = {
                                onConfirmClick()
                                dismiss()
                            }
                        )
                    }
                }
            })
        }
    }
}

/**
 * Composable function that defines the content of the biometric authentication bottom sheet.
 *
 * This composable creates a UI with a title, an animated face icon, and a confirmation button.
 * The face icon pulses with a scale animation to draw user attention.
 *
 * @param title The title text to display
 * @param confirmText The text for the confirmation button
 * @param onConfirmClick Callback function that is triggered when the confirm button is clicked
 */
@Composable
fun BiometricAuthContent(
    title: String,
    confirmText: String,
    onConfirmClick: () -> Unit
) {
    // Create an infinite transition for the pulsing animation of the face icon
    val infiniteTransition = rememberInfiniteTransition(label = "infiniteTransition")
    val scale by infiniteTransition.animateFloat(
        initialValue = 1.0f,
        targetValue = 1.1f,
        animationSpec = infiniteRepeatable(
            animation = tween(durationMillis = 1200, easing = LinearEasing),
            repeatMode = RepeatMode.Reverse
        ),
        label = "iconScale"
    )

    // Main container box with background color that adapts to the theme
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .background(MaterialTheme.colorScheme.background)
    ) {
        // Content column with centered alignment
        Column(
            modifier = Modifier
                .fillMaxWidth()
                .padding(horizontal = 24.dp, vertical = 12.dp),
            horizontalAlignment = Alignment.CenterHorizontally
        ) {
            // Drag handle at the top of the bottom sheet
            Box(
                modifier = Modifier
                    .width(50.dp)
                    .height(5.dp)
                    .clip(
                        RoundedCornerShape(
                            topStart = 8.dp,
                            topEnd = 8.dp,
                            bottomStart = 8.dp,
                            bottomEnd = 8.dp
                        )
                    )
                    .background(
                        MaterialTheme.colorScheme.onBackground.copy(alpha = 0.2f)
                    )
            )

            Spacer(modifier = Modifier.height(10.dp))

            // Title text with theme-appropriate color
            Text(
                text = title,
                style = MaterialTheme.typography.headlineSmall.copy(
                    fontWeight = FontWeight.Bold,
                    color = MaterialTheme.colorScheme.onBackground
                ),
                textAlign = TextAlign.Center
            )

            Spacer(modifier = Modifier.height(16.dp))

            // Face icon with pulsing animation
            Box(
                contentAlignment = Alignment.Center
            ) {
                // Face icon that scales up and down
                Icon(
                    imageVector = Icons.Default.Face,
                    contentDescription = "Biometric Icon",
                    modifier = Modifier
                        .size(62.dp)
                        .graphicsLayer(
                            scaleX = scale,
                            scaleY = scale
                        ),
                    tint = MaterialTheme.colorScheme.primary
                )

                // Circular background for the face icon
                Box(
                    modifier = Modifier
                        .size(100.dp)
                        .background(
                            color = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
                            shape = CircleShape
                        )
                )
            }

            Spacer(modifier = Modifier.height(24.dp))

            // Confirm button that spans the full width
            Button(
                onClick = onConfirmClick,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(50.dp)
            ) {
                // Button content with text and arrow icon
                Row(
                    horizontalArrangement = Arrangement.Center,
                    verticalAlignment = Alignment.CenterVertically
                ) {
                    // Button text with appropriate contrast color
                    Text(confirmText, style = MaterialTheme.typography.bodyLarge.copy(
                        color = MaterialTheme.colorScheme.onPrimary
                    ))
                    // Arrow icon that indicates action
                    Icon(
                        imageVector = Icons.AutoMirrored.Filled.ArrowForward,
                        contentDescription = "Confirm Icon",
                        tint = MaterialTheme.colorScheme.onPrimary,
                        modifier = Modifier
                            .padding(start = 8.dp)
                            .size(22.dp)
                    )
                }
            }

            Spacer(modifier = Modifier.height(12.dp))
        }
    }
}

FingerprintAuthDialog

这个文件自定义了Dialog对话框认证样式。

package com.maojiu.biometric_authorization

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Fingerprint
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog

/**
 * A Composable function that displays a custom dialog for fingerprint authentication.
 * This dialog shows a fingerprint icon, a title, and a cancel button.
 * It adapts its color scheme based on the system's dark theme setting.
 *
 * @param title The main text displayed in the dialog, usually indicating the purpose (e.g., "Fingerprint Authentication").
 * @param cancelText The text displayed on the cancel button.
 * @param onDismissRequest A lambda function invoked when the user attempts to dismiss the dialog
 *                         by interacting outside the dialog bounds or pressing the back button.
 *                         This is mandatory for the [Dialog] composable.
 * @param onCancel A lambda function invoked when the user explicitly clicks the cancel button within the dialog.
 */
@Composable
fun FingerprintDialog(
    title: String,
    cancelText: String,
    onDismissRequest: () -> Unit = {},
    onCancel: () -> Unit = {}
) {
    // Apply Material 3 theme, automatically selecting light or dark color scheme
    // based on the system settings.
    MaterialTheme(
        colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme()
    ) {
        // The Dialog composable provides the basic dialog window structure.
        Dialog(onDismissRequest = onDismissRequest) {
            // Surface provides a background, shape, and elevation for the dialog content.
            Surface(
                shape = MaterialTheme.shapes.medium, // Use medium rounded corners defined in the theme.
                color = MaterialTheme.colorScheme.surface, // Use the theme's surface color for the background.
                modifier = Modifier.padding(16.dp) // Apply padding around the Surface within the Dialog window.
            ) {
                // Column arranges its children vertically.
                Column(
                    modifier = Modifier
                        .padding(horizontal = 16.dp) // Inner padding for the content inside the Surface.
                        .fillMaxWidth(), // Make the column take the full width available within the padding.
                    horizontalAlignment = Alignment.CenterHorizontally, // Center children horizontally.
                    verticalArrangement = Arrangement.Center // Center children vertically within the column (less relevant here due to specific Spacers).
                ) {
                    // Vertical space before the icon.
                    Spacer(modifier = Modifier.height(24.dp))
                    // Display the fingerprint icon.
                    Icon(
                        imageVector = Icons.Filled.Fingerprint, // Use the standard fingerprint icon.
                        contentDescription = "Fingerprint Icon", // Accessibility description.
                        tint = MaterialTheme.colorScheme.primary, // Tint the icon with the theme's primary color.
                        modifier = Modifier.size(64.dp) // Set the size of the icon.
                    )
                    // Vertical space between the icon and the title.
                    Spacer(modifier = Modifier.height(24.dp))
                    // Display the dialog title.
                    Text(
                        text = title,
                        style = MaterialTheme.typography.titleMedium // Use the medium title text style from the theme.
                    )
                    // Vertical space between the title and the divider.
                    Spacer(modifier = Modifier.height(10.dp))
                    // A thin horizontal line separator.
                    HorizontalDivider()
                    // The cancel button.
                    TextButton(
                        onClick = onCancel, // Invoke the onCancel lambda when clicked.
                        modifier = Modifier
                            .fillMaxWidth() // Make the button span the full width.
                            .padding(top = 8.dp) // Add padding above the button.
                    ) {
                        // The text displayed within the cancel button.
                        Text(
                            text = cancelText,
                            color = MaterialTheme.colorScheme.primary // Use the primary color for the button text for emphasis.
                        )
                    }
                    // Vertical space after the cancel button.
                    Spacer(modifier = Modifier.height(8.dp))
                }
            }
        }
    }
}

FingerprintDialogFragment

在这里使用了自定义对话框进行认证。

package com.maojiu.biometric_authorization

import android.content.DialogInterface
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.material3.MaterialTheme // Using Material 3 Theme directly
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.fragment.app.DialogFragment

/**
 * A DialogFragment that hosts the [FingerprintDialog] Composable.
 * This fragment is responsible for displaying the fingerprint authentication dialog
 * and handling user interactions like cancellation or dismissal.
 */
class FingerprintDialogFragment : DialogFragment() {

    /** 
     * A lambda function to be executed when the dialog is cancelled or dismissed.
     * This is typically used to trigger the cancellation of the underlying fingerprint authentication process.
     */
    var onCancelAction: (() -> Unit)? = null

    companion object {
        private const val ARG_TITLE = "title"
        private const val ARG_CANCEL_TEXT = "cancel_text"

        /**
         * Factory method to create a new instance of [FingerprintDialogFragment].
         *
         * @param title The title string to be displayed in the dialog.
         * @param cancelText The text string for the cancel button.
         * @param onCancel A lambda function that will be invoked when the dialog is cancelled or dismissed.
         * @return A new instance of [FingerprintDialogFragment] with the provided arguments.
         */
        fun newInstance(title: String, cancelText: String, onCancel: () -> Unit): FingerprintDialogFragment {
            val fragment = FingerprintDialogFragment()
            fragment.arguments = Bundle().apply {
                putString(ARG_TITLE, title)
                putString(ARG_CANCEL_TEXT, cancelText)
            }
            // Store the lambda directly. While DialogFragments can be recreated by the system
            // (making direct lambda storage potentially fragile if the state needs to survive recreation),
            // for this specific use case where the dialog is shown and interacts immediately
            // with an ongoing process, this approach is often sufficient.
            // More robust alternatives for complex state include using the Fragment Result API or a shared ViewModel.
            fragment.onCancelAction = onCancel
            return fragment
        }
    }

    /**
     * Creates and returns the view hierarchy associated with the fragment.
     * Inflates the layout using [ComposeView] to host the Jetpack Compose UI.
     *
     * @param inflater The LayoutInflater object that can be used to inflate any views in the fragment.
     * @param container If non-null, this is the parent view that the fragment's UI should be attached to.
     * @param savedInstanceState If non-null, this fragment is being re-constructed from a previous saved state as given here.
     * @return Returns the View for the fragment's UI, or null.
     */
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        // Retrieve arguments passed via newInstance
        val title = arguments?.getString(ARG_TITLE) ?: "Fingerprint Authentication"
        val cancelText = arguments?.getString(ARG_CANCEL_TEXT) ?: "Cancel"

        return ComposeView(requireContext()).apply {
            // Set the strategy for managing the Compose Composition lifecycle.
            // DisposeOnViewTreeLifecycleDestroyed ensures the Composition is disposed when the Fragment's view lifecycle is destroyed,
            // preventing potential memory leaks.
            setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
            setContent {
                // Apply Material 3 Theme. Ensure the application's theme is correctly set up for Material 3.
                MaterialTheme {
                    // Embed the FingerprintDialog Composable within the ComposeView
                    FingerprintDialog(
                        title = title,
                        cancelText = cancelText,
                        onDismissRequest = {
                            // This lambda is invoked when the dialog is dismissed by interactions outside its bounds
                            // (e.g., tapping the scrim or pressing the back button).
                            onCancelAction?.invoke() // Trigger cancel action on dismiss
                            dismiss() // Dismiss the DialogFragment itself
                        },
                        onCancel = {
                            // This lambda is invoked when the user clicks the explicit 'Cancel' button within the dialog.
                            onCancelAction?.invoke() // Trigger cancel action on cancel click
                            dismiss() // Dismiss the DialogFragment itself
                        }
                    )
                }
            }
        }
    }

    /**
     * Called when the fragment's activity has been created and this fragment's view hierarchy instantiated.
     * Can be used to do final initialization once these pieces are in place.
     */
    override fun onStart() {
        super.onStart()
        // Optional: Set the dialog window background to transparent.
        // This is useful if the FingerprintDialog Composable defines its own background/shape (e.g., rounded corners within a Surface).
        // If not set, the DialogFragment might have its own default background that could interfere with the Composable's visuals.
        dialog?.window?.setBackgroundDrawableResource(android.R.color.transparent)
    }

    /**
     * This method will be called when the dialog is cancelled, either by pressing the back button,
     * tapping outside the dialog (if cancellable is true), or explicitly calling cancel().
     *
     * @param dialog The dialog that was canceled will be passed into the method.
     */
    override fun onCancel(dialog: DialogInterface) {
        super.onCancel(dialog)
        // Ensure the cancel action is invoked when the dialog is cancelled through standard mechanisms.
        // This acts as a fallback for the onDismissRequest lambda.
        onCancelAction?.invoke()
    }
} 

build.gradle

在build.gradle中进行相应的配置。

/**
 * Gradle build configuration for the Biometric Authorization plugin.
 * This file defines all the necessary configurations for building the Android component
 * of the Flutter plugin, including dependencies, SDK versions, and build options.
 */

// Define the group ID and version for the plugin
group = "com.maojiu.biometric_authorization"
version = "1.0-SNAPSHOT"

// Load local properties file that contains environment-specific configurations
// such as the path to the Flutter SDK
def localProperties = new Properties()
def localPropertiesFile = rootProject.file("local.properties")
if (localPropertiesFile.exists()) {
    localPropertiesFile.withReader("UTF-8") { reader ->
        localProperties.load(reader)
    }
}

// Verify that the Flutter SDK path is defined in local.properties
def flutterRoot = localProperties.getProperty("flutter.sdk")
if (flutterRoot == null) {
    throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}

/**
 * Buildscript configuration section.
 * This is where we define the Gradle plugins and repositories needed to build the project.
 */
buildscript {
    // Define Kotlin version used across the project
    ext.kotlin_version = "1.9.22"
    ext.kotlin_ext_version = "1.5.10"
    
    // Define repositories to download build dependencies
    repositories {
        google()  // Google's Maven repository for Android specific dependencies
        mavenCentral()  // Central repository for general Java/Kotlin libraries
    }

    // Define build dependencies needed by Gradle to compile the project
    dependencies {
        classpath("com.android.tools.build:gradle:8.2.0")  // Android Gradle Plugin
        classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version")  // Kotlin Gradle Plugin
    }
}

/**
 * Define repositories that will be used to resolve dependencies for all projects,
 * including the app and the libraries it depends on.
 */
allprojects {
    repositories {
        google()
        mavenCentral()
    }
}

// Apply the Android library plugin, which configures Gradle to build an Android library (.aar)
apply plugin: "com.android.library"
// Apply the Kotlin Android plugin for Kotlin language support
apply plugin: "kotlin-android"

/**
 * Android specific configuration.
 * This block defines all Android-specific build settings.
 */
android {
    // Package namespace for the library
    namespace = "com.maojiu.biometric_authorization"

    // Android SDK version to compile against
    compileSdk = 35
    // NDK version to use
    ndkVersion = "26.3.11579264"

    // Enable Jetpack Compose support
    buildFeatures {
        compose true
    }

    // Configure Jetpack Compose compiler options
    composeOptions {
        kotlinCompilerVersion("$kotlin_version")
        kotlinCompilerExtensionVersion("$kotlin_ext_version")
    }

    // Configure Java compatibility options
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }

    // Configure Kotlin compiler options
    kotlinOptions {
        jvmTarget = JavaVersion.VERSION_11
    }

    // Define source code directories
    sourceSets {
        main.java.srcDirs += "src/main/kotlin"  // Main source code directory
        test.java.srcDirs += "src/test/kotlin"  // Test source code directory
    }

    // Define the minimum SDK version required to run the library
    defaultConfig {
        minSdk = 21  // Android 5.0 (Lollipop) and above
    }

    /**
     * Dependencies section.
     * This is where we define external libraries that our plugin depends on.
     */
    dependencies {
        // Jetpack Compose dependencies
        implementation(platform("androidx.compose:compose-bom:2024.06.00"))  // Compose Bill of Materials
        implementation("androidx.compose.ui:ui")  // Compose UI core
        implementation("androidx.compose.ui:ui-tooling-preview")  // Compose UI tooling preview
        implementation("androidx.compose.material3:material3")  // Material 3 design system
        implementation("androidx.compose.material:material-icons-extended")
        implementation("androidx.activity:activity-compose:1.10.1")  // Activity with Compose support
        implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")  // ViewModel for Compose
        
        // Biometric authentication dependency
        implementation("androidx.biometric:biometric:1.1.0")  // Android Biometric API
        
        // Material design components
        implementation("com.google.android.material:material:1.12.0")

        // Testing dependencies
        testImplementation("org.jetbrains.kotlin:kotlin-test")  // Kotlin test utilities
        testImplementation("org.mockito:mockito-core:5.0.0")  // Mockito for mocking in tests

        // Flutter dependencies (currently commented out)
//        compileOnly files("$flutterRoot/bin/cache/artifacts/engine/android-arm-release/flutter.jar")
//        compileOnly("androidx.annotation:annotation:1.9.1")
    }

    /**
     * Test configuration options.
     * This block configures how tests are executed and how results are reported.
     */
    testOptions {
        unitTests.all {
            useJUnitPlatform()  // Use JUnit 5 platform for running tests

            // Configure test logging
            testLogging {
               events "passed", "skipped", "failed", "standardOut", "standardError"  // Log test events
               outputs.upToDateWhen {false}  // Always run tests
               showStandardStreams = true  // Show standard output and error streams
            }
        }
    }
}

基本使用

Android 设置

  1. 在 AndroidManifest.xml 文件中添加以下权限:
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
  1. 重要提示:你必须将 FlutterActivity 替换为 FlutterFragmentActivity。在你的 MainActivity.kt 文件中:
import io.flutter.embedding.android.FlutterFragmentActivity

class MainActivity: FlutterFragmentActivity() {
    // ...
}

iOS 设置

在你的 Info.plist 文件中添加以下内容:

<key>NSFaceIDUsageDescription</key>
<string>您的应用需要使用生物识别数据进行安全访问验证</string>

使用方法

检查生物识别可用性

final biometricAuth = BiometricAuthorization();

// 检查设备是否支持生物识别
bool isAvailable = await biometricAuth.isBiometricAvailable();

// 检查是否已经录入生物识别信息
bool isEnrolled = await biometricAuth.isBiometricEnrolled();

// 获取可用的生物识别类型
List<BiometricType> types = await biometricAuth.getAvailableBiometricTypes();

使用身份验证

使用默认系统界面
try {
  bool authenticated = await biometricAuth.authenticate(
    reason: '请验证身份以访问您的账户',
    title: '生物识别验证',
    confirmText: '验证',
  );

  if (authenticated) {
    // 用户验证成功
    print('验证成功');
  } else {
    // 验证失败或用户取消
    print('验证失败');
  }
} catch (e) {
  print('验证过程中出现错误: $e');
}
使用自定义界面
bool authenticated = await biometricAuth.authenticate(
  reason: '请验证身份以访问您的账户',
  title: '生物识别验证',
  confirmText: '验证',
  useCustomUI: true,
);
使用对话框界面(仅支持 Android)
bool authenticated = await biometricAuth.authenticate(
  reason: '请验证身份以访问您的账户',
  title: '生物识别验证',
  confirmText: '验证',
  useDialog: true
);

平台特有参数说明

authenticate 方法包含一些在不同平台行为不同的参数:

  • biometricType:该参数在 iOS 上是必需的,需要指定使用哪种生物识别方式(Face ID 或指纹)。而在 Android 上是可选的,系统会自动选择可用的方式。
// iOS 示例 - 必须指定 biometricType
await biometricAuth.authenticate(
  biometricType: BiometricType.face, // 使用 Face ID
  reason: '请验证身份以继续',
);
  • cancelText:该参数仅在 Android 上生效,用于设置认证对话框中的取消按钮文本。在 iOS 上无效。
// Android 示例 - 设置取消按钮文本
await biometricAuth.authenticate(
  reason: '请验证身份以继续',
  cancelText: '稍后再说',
);

效果

iOS Authentication

Touch ID - System UITouch ID - Custom UIFace ID - Custom UI
iOS Touch ID System UiOS Touch ID Custom UIiOS Face ID Custom UI

Android Authentication

Default UICustom UI (Sheet)Custom UI (Dialog)
Android Default UIAndroid Custom UI (Sheet)Android Custom UI (Dialog)

仓库地址:github.com/maojiu-bb/b…

插件地址:pub.dev/packages/bi…