Flutter密码锁插件pin_lock的使用

Flutter密码锁插件pin_lock的使用

所有应用在本地认证层(如锁屏和密码设置流程)的设计上都是独一无二的。然而,大多数实现的核心原理是相同的:只有授权用户才能查看应用内容。

pin_lock 包旨在为 Android 和 iOS 提供一个稳固的底层逻辑实现,同时给予开发者自由度来构建独特的交互界面。

目录

特性

锁定功能

  • ✅ 应用使用密码保护
  • ✅ 在后台一段时间后锁定应用
  • ✅ 使用数字密码解锁
  • ✅ 使用原生生物识别解锁(指纹、面部识别等)
  • ✅ 在指定次数错误输入密码后锁定一段时间
  • ✅ 可选地隐藏应用预览(缩略图)当切换应用时
    • ✅ iOS:添加自定义占位符资产以在应用切换器中显示
  • ✅ 支持同一设备上的多个账户

锁定设置

  • ✅ 实现标准的密码验证流程:
    • ✅ 启用密码(需要重新输入确认)
      • ✅ 禁用密码(需要当前密码)
      • ✅ 更改密码(需要当前密码、新密码和新密码确认)

计划

  • ⬜ TODO: 仅在启用密码时隐藏应用预览。
  • ⬜ TODO: 完善错误输入密码后的锁定时间
    • ⬜ TODO: 当认证被锁定时禁用密码输入
      • ⬜ TODO: 将锁定时间传递给UI
  • ⬜ TODO: 添加可选的二级密码(如安全问题),如果忘记主密码可以解锁应用
  • ⬜ TODO: 在锁定屏幕添加可选的登出按钮,允许用户无需卸载应用即可更改账户

权限与集成

iOS

如果你想要使用生物识别认证,需要在应用的 Info.plist 文件中添加 NSFaceIDUsageDescription

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<!-- 其他配置 -->
	<key>NSFaceIDUsageDescription</key>
	<string>你可以在登录时使用生物识别来保护你的数据。</string>
</dict>
</plist>

Android

确保你的主活动继承了 FlutterFragmentActivity,例如:

package ...your package name...

import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugins.GeneratedPluginRegistrant

class MainActivity: FlutterFragmentActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        GeneratedPluginRegistrant.registerWith(flutterEngine)
    }
}

WidgetsBindingObserver

要让应用在后台一段时间后自动锁定,需要让 pin_lock 插件观察应用生命周期。在你想锁定的部分应用的根部件(例如,你可能不希望包括引导或登录页面)添加以下代码:

  [@override](/user/override)
  void initState() {
    super.initState();
    WidgetsBinding.instance?.addObserver(authenticatorInstance);
  }

  [@override](/user/override)
  void dispose() {
    WidgetsBinding.instance?.removeObserver(authenticatorInstance);
    super.dispose();
  }

实际上,最好将 Authenticator 注册为观察者放在 AuthenticatorWidget 之前。

使用

大部分交互都涉及到三个主要插件组件:

  • 全局的 Authenticator 类,它是操作的大脑,通过它可以配置大多数本地认证的偏好设置。
  • AuthenticatorWidget 是受保护部分应用的根部件,通过它可以配置锁屏的UI。
  • AuthenticatonSetupWidget 接受不同阶段的认证设置描述。它应插入到应用的设置/偏好部分,并且可以根据你的应用风格进行定制。

设置 Authenticator

首先,当你整合包时,需要创建一个全局可访问(单例)的 Authenticator 实例。有两个方便的初始化方法可以使用。PinLock.baseAuthenticator() 是一种快速获取实例的方法,它使用所有默认设置。如果你想改变任何默认设置或使用自己的实现方式,可以使用 PinLock.authenticatorInstance() 工厂方法。

  authenticator = await PinLock.baseAuthenticator('1');
  // 或者
  authenticator = await PinLock.authenticatorInstance(userId: '1', ...其他配置参数...);

userId 是一个 String 参数,使多个用户能够使用不同的密码在同一个设备上使用应用。这里你需要提供一个能保证对于你的用户唯一(如用户名或用户 ID)的值。 如果你不想支持一个设备上的多个账户,可以提供一个硬编码的字符串值。

⚠️ 如果硬编码了 userId,请确保在注销时禁用密码认证,否则下一个用户无法在同一个设备上使用应用而无需重新安装。

完成这一步之后,你的应用就有了一个知道如何处理其锁定行为并知道如何存储这些数据的 Authenticator 实例。

设置 AuthenticatorWidget

AuthenticatorWidget 是应用受保护部分的根部件。通常情况下,你希望它包含整个应用除了引导、注册和登录流程之外的所有部分(此时你也不知道用户的身份,因此不能提供可靠的 userId)。

AuthenticatorWidget 的核心参数包括:

  • child - 这是你的应用的正常小部件树
  • pinNodeBuilder - 这是一个构造函数,通过它可以提供有关每个输入字段的信息
  • lockScreenBuilder - 这是另一个构造函数,通过它可以描述你想要你的整个密码输入屏幕看起来的样子(根据 LockScreenConfiguration

如果你希望在应用进入后台一段时间后锁定(显示锁屏),不要忘记包括 WidgetsBindingObserver 步骤。

完成这一步之后,你的应用就知道了哪些部分由密码保护,并且锁屏应该是什么样子。

设置 AuthenticatonSetupWidget

最后一步是描述用户试图启用、禁用或更改密码时的用户界面。

AuthenticationSetupWidget 应该放置在应用的设置或偏好屏幕中。它需要你为不同的密码交互流设置构建器。

  • overviewBuilder 是用户在设置中看到的第一件事,描述当前是否启用了密码和生物识别认证。
  • enablingWidgetdisablingWidgetchangingWidget 应返回描述相应屏幕应该是什么样子的小部件。Configuration 参数包含了你需要显示正确状态的所有信息。例如,canSubmitChange 属性可以用来启用或禁用按钮,error 属性可以映射到更具描述性的本地化文本,告诉用户具体出了什么问题以及如何解决。

你可以在应用的不同位置使用 AuthenticationSetupWidget。例如,你可以将这个小部件作为设置屏幕中的 ListTile 的子部件,在这里只提供 overviewBuilder(并从其他构建器返回 Container)。这样你可以在应用的一般设置中预览当前的密码认证状态。点击这个选项可以打开一个新的屏幕,在其中可以有一个描述应用密码设置流程的 AuthenticationSetupWidget

示例代码

import 'dart:async';

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

// 应用中应该只有一个 [Authenticator] 实例,否则锁定行为可能会变得不可预测
late final Authenticator globalAuthenticator;

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  /// 最简单的初始化 [Authenticator] 的方法是使用 [baseAuthenticator]
  /// 它创建一个具有所有默认部分的实例,给定 [userId]
  /// 如果你想覆盖任何默认设置(例如,提供不同的本地存储库或在应用锁定/解锁时有自定义回调),请使用
  /// [PinLock.authenticatorInstance()]
  globalAuthenticator = await PinLock.baseAuthenticator('1');
  runApp(const MyApp());
}

class MyApp extends StatefulWidget {
  const MyApp({Key? key}) : super(key: key);
  [@override](/user/override)
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  [@override](/user/override)
  void initState() {
    super.initState();

    /// [Authenticator] 需要作为应用程序生命周期的观察者进行注册
    WidgetsBinding.instance.addObserver(globalAuthenticator);
  }

  [@override](/user/override)
  void dispose() {
    /// 在处理应用程序时,移除 [Authenticator] 对生命周期事件的订阅
    WidgetsBinding.instance.removeObserver(globalAuthenticator);
    super.dispose();
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData(
        primarySwatch: Colors.indigo,
        textTheme: TextTheme(
          bodyMedium: TextStyle(color: Colors.indigo),
        ),
      ),

      /// 使用 [AuthenticatorWidget] 作为需要通过密码锁保护的应用部分的根部件
      home: AuthenticatorWidget(
        /// 传递你的 [Authenticator] 单例引用
        authenticator: globalAuthenticator,

        /// 提供一个字符串,用户会在生物识别认证触发时看到
        userFacingBiometricAuthenticationMessage:
            '出于隐私原因,您的数据已被锁定。您需要解锁应用才能访问您的数据。',

        /// 提供一个表示单个密码输入字段的小部件
        inputNodeBuilder: (index, state) =>
            _InputField(state: state, index: index),

        /// 提供一个描述锁屏应该是什么样子的小部件
        lockScreenBuilder: (configuration) => _LockScreen(configuration),

        /// 可选图像用于防止在应用切换器中显示应用内容。
        iosImageAsset: 'AppIcon',

        /// [child] 应该是你通常传递给 [MaterialApp] 的 [home] 的小部件
        child: _Home(),
      ),
    );
  }
}

/// 表示密码的一个数字的视觉表示
/// [InputFieldState] 告诉UI它应该绘制哪种状态
/// 你可以选择根据输入字段的位置修改它们的外观,例如,如果你想要在密码小部件前缀或后缀添加一些内容,可以将其添加到第0个或第(n-1)个输入字段
class _InputField extends StatelessWidget {
  final InputFieldState state;
  final int index;
  const _InputField({
    Key? key,
    required this.state,
    required this.index,
  }) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    final borderColor = state == InputFieldState.error
        ? Theme.of(context).colorScheme.error
        : Theme.of(context).primaryColor;
    double borderWidth = 1;
    if (state == InputFieldState.focused ||
        state == InputFieldState.filledAndFocused) {
      borderWidth = 4;
    }
    return Container(
      height: 40,
      width: 46,
      margin: const EdgeInsets.all(5),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(20),
        border: Border.all(
          color: borderColor,
          width: borderWidth,
        ),
      ),
      child: state == InputFieldState.filled ||
              state == InputFieldState.filledAndFocused
          ? Center(
              child: Container(
                width: 6,
                height: 6,
                decoration: BoxDecoration(
                  shape: BoxShape.circle,
                  color: Colors.indigo,
                ),
              ),
            )
          : Container(),
    );
  }
}

class _Home extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('PinLock 示例应用'),
      ),
      body: Column(
        children: [
          Text(
            '这是主页',
            style: Theme.of(context).textTheme.displaySmall,
            textAlign: TextAlign.center,
          ),
          TextButton(
            onPressed: () {
              Navigator.of(context)
                  .push(MaterialPageRoute(builder: (_) => _SetupAuthWidget()));
            },
            child: Text('配置本地认证'),
          ),
        ],
      ),
    );
  }
}

/// 根据当前状态指定锁屏应该是什么样子
/// 详见 [LockScreenConfiguration] 文档了解所有可用信息
class _LockScreen extends StatelessWidget {
  final LockScreenConfiguration configuration;

  const _LockScreen(this.configuration, {Key? key}) : super(key: key);

  [@override](/user/override)
  Widget build(BuildContext context) {
    return SafeArea(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const Text('输入您的密码以解锁应用'),
          Row(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              /// [LockScreenConfiguration] 提供 [pinInputWidget],基于你在 [AuthenticatorWidget] 中提供的指令绘制
              /// 你需要确保它在锁屏上可见,而 PinLock 包负责它的状态
              configuration.pinInputWidget,

              /// 你可以检查是否可以使用生物识别认证,并相应地调整UI
              if (configuration.availableBiometricMethods.isNotEmpty)
                IconButton(
                  icon: Icon(Icons.fingerprint),
                  onPressed: configuration.onBiometricAuthenticationRequested,
                ),
            ],
          ),

          /// [LockScreenConfiguration] 提供 [error] 属性,基于此你可以向用户提供基于特定 [LocalAuthFailure] 的错误消息
          if (configuration.error != null)
            Text(
              configuration.error.toString(),
              style: const TextStyle(color: Colors.red),
            )
        ],
      ),
    );
  }
}

/// [AuthenticationSetupWidget] 提供几个带有适当配置的构建器属性。
/// 如果你不希望你的应用支持某些功能(例如更改密码),只需从其构建器返回一个 `Container`
class _SetupAuthWidget extends StatelessWidget {
  [@override](/user/override)
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('设置')),

      /// 在设置屏幕或其他你希望用户能够更改密码偏好设置的地方放置 [AuthenticationSetupWidget]
      body: AuthenticationSetupWidget(
        /// 传递一个 [Authenticator] 单例引用
        authenticator: globalAuthenticator,

        /// 可以使用与锁屏相同的密码输入小部件,或者提供一个你希望在设置时使用的自定义UI
        pinInputBuilder: (index, state) =>
            _InputField(state: state, index: index),

        /// 概览是指用户在设置中看到的第一件事,以及他们在采取行动(例如更改密码)后看到的内容
        /// 详见 [OverviewConfiguration] 了解所有可用数据
        overviewBuilder: (config) => Center(
          /// [isLoading] 表示用户偏好设置仍在获取中
          child: config.isLoading
              ? CircularProgressIndicator()
              : Padding(
                  padding: const EdgeInsets.all(8.0),
                  child: Column(
                    children: [
                      Row(
                        mainAxisAlignment: MainAxisAlignment.spaceBetween,
                        children: [
                          /// [isPinEnabled] 仅在 [isLoading] 为 `true` 时为 `null`
                          Text('使用密码保护应用'),
                          Switch(
                            value: config.isPinEnabled!,

                            /// [onTogglePin] 回调用于触发用户更改其偏好设置的按钮(或开关)
                            onChanged: (_) => config.onTogglePin(),
                          ),
                        ],
                      ),

                      /// 如果出现错误,[OverviewConfiguration] 提供 [error] 属性
                      if (config.error != null)
                        Text(config.error!.toString(),
                            style: TextStyle(color: Colors.red)),

                      /// 如果生物识别认证可用,提供一个开关来启用或禁用它
                      if (config.isBiometricAuthAvailable == true)
                        Row(
                          mainAxisAlignment: MainAxisAlignment.spaceBetween,
                          children: [
                            const Text(
                                '使用指纹或面部识别解锁应用'),
                            Switch(
                              value: config.isBiometricAuthEnabled!,
                              onChanged: (_) => config.onToggleBiometric(),
                            ),
                          ],
                        ),

                      /// 如果启用了密码,可以给用户提供一个更改密码的选项
                      if (config.isPinEnabled == true)
                        OutlinedButton(
                          /// 如果你不希望密码可更改,只需从未触发 [config.onPasswordChangeRequested]
                          /// 如果这个回调从未被触发,[changingWidget] 构建器就永远不需要,所以可以简单地返回一个 `Container` 或 `SizedBox`
                          onPressed: config.onPasswordChangeRequested,
                          child: const Text('更改密码'),
                        ),
                    ],
                  ),
                ),
        ),

        /// EnablingWidget 是一个描述 [AuthenticationSetupWidget] 在启用密码时看起来什么样的构建器
        /// 详见 [EnablingPinConfiguration] 了解更多细节
        enablingWidget: (configuration) => Center(
          child: Column(
            children: [
              const SizedBox(height: 40),
              const Text('选择一个您能记住的密码'),

              /// 确保 [configuration.pinInputWidget] 和 [configuration.pinConfirmationWidget] 在屏幕上可见,因为
              /// 它们是用户与 PinLock 包互动的主要点
              configuration.pinInputWidget,
              const SizedBox(height: 24),
              const Text('再次输入相同的密码'),

              /// [pinInputWidget] 和 [pinConfirmationWidget] 可以并排展示或依次展示
              configuration.pinConfirmationWidget,

              /// [configuration.error] 提供详细信息,如果出现问题(例如,密码不匹配)
              if (configuration.error != null)
                Text(configuration.error.toString(),
                    style: TextStyle(color: Colors.red)),

              /// [configuration.canSubmitChange] 可以选择性地用于隐藏或禁用提交按钮
              /// 也可以监听此属性并程序化地触发 [config.onSubmitChange],
              /// 例如,如果你希望在字段填充后立即调用库,而不必让用户按下按钮
              if (configuration.canSubmitChange)
                OutlinedButton(
                  onPressed: configuration.onSubmitChange,
                  child: const Text('保存'),
                )
            ],
          ),
        ),

        /// DisablingWidget 是一个描述 [AuthenticationSetupWidget] 在禁用密码时看起来什么样的构建器
        /// 详见 [DisablingPinConfiguration] 了解更多细节
        disablingWidget: (configuration) => Center(
          child: Column(
            children: [
              const SizedBox(height: 40),
              Text('输入您的密码以禁用密码认证'),

              /// 确保 [configuration.pinInputWidget] 在屏幕上可见
              configuration.pinInputWidget,

              /// 显示错误信息
              if (configuration.error != null)
                Text(
                  configuration.error.toString(),
                  style: TextStyle(color: Colors.red),
                ),
              if (configuration.canSubmitChange)
                OutlinedButton(
                    onPressed: configuration.onChangeSubmitted,
                    child: Text('保存'))
            ],
          ),
        ),

        changingWidget: (configuration) => Column(
          children: [
            const Text('输入当前密码'),
            configuration.oldPinInputWidget,
            if (_isCurrentPinIssue(configuration.error))
              Text(
                configuration.error!.toString(),
                style: const TextStyle(color: Colors.red),
              ),
            const Text('输入新密码'),
            configuration.newPinInputWidget,
            const Text('确认新密码'),
            configuration.confirmNewPinInputWidget,
            if (configuration.error != null &&
                !_isCurrentPinIssue(configuration.error))
              Text(
                configuration.error!.toString(),
                style: const TextStyle(color: Colors.red),
              ),
            if (configuration.canSubmitChange)
              TextButton(
                onPressed: configuration.onSubimtChange,
                child: const Text('保存'),
              )
          ],
        ),
      ),
    );
  }

  bool _isCurrentPinIssue(LocalAuthFailure? error) {
    return error == LocalAuthFailure.wrongPin ||
        error == LocalAuthFailure.tooManyAttempts;
  }
}

更多关于Flutter密码锁插件pin_lock的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter密码锁插件pin_lock的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是如何在Flutter项目中使用pin_lock插件来实现一个简单的密码锁功能的示例代码。pin_lock插件允许你创建一个自定义的PIN码输入界面,非常适合用于密码锁功能。

首先,确保你已经在pubspec.yaml文件中添加了pin_lock依赖:

dependencies:
  flutter:
    sdk: flutter
  pin_lock: ^2.0.0  # 请检查最新版本号

然后运行flutter pub get来获取依赖。

接下来,创建一个简单的Flutter应用,并在其中使用PinLock小部件。以下是一个完整的示例:

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

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Pin Lock Example',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: PinLockScreen(),
    );
  }
}

class PinLockScreen extends StatefulWidget {
  @override
  _PinLockScreenState createState() => _PinLockScreenState();
}

class _PinLockScreenState extends State<PinLockScreen> {
  final _controller = TextEditingController();
  final _pinLockKey = GlobalKey<PinLockState>();
  String? _result;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Pin Lock Example'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            SizedBox(
              height: 200,
              child: PinLock(
                key: _pinLockKey,
                length: 4, // 设置PIN码长度
                controller: _controller,
                onCompleted: (pin) {
                  setState(() {
                    _result = 'PIN entered: $pin';
                  });
                  // 在这里可以添加验证PIN码的逻辑
                },
                onChanged: (pin) {
                  // 实时显示输入的PIN码(可选)
                  print('Current PIN: $pin');
                },
                decoration: PinLockDecoration(
                  activeColor: Colors.blue,
                  inactiveColor: Colors.grey,
                  borderColor: Colors.black,
                  borderWidth: 2.0,
                  activeBorderColor: Colors.blueAccent,
                  activeBorderWidth: 3.0,
                ),
              ),
            ),
            SizedBox(height: 20),
            Text(
              _result ?? 'Enter your PIN',
              style: TextStyle(fontSize: 20),
            ),
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}

代码解释

  1. 依赖添加:在pubspec.yaml文件中添加pin_lock依赖。

  2. PinLockScreen:创建了一个有状态的PinLockScreen小部件,用于显示PIN码输入界面。

  3. TextEditingController:使用TextEditingController来管理PIN码的输入。

  4. GlobalKey:使用GlobalKey来访问PinLock小部件的状态,以便在需要时调用其方法或访问其属性。

  5. PinLockPinLock小部件用于显示PIN码输入界面。参数length设置PIN码的长度,controller管理输入内容,onCompleted是PIN码输入完成时的回调,onChanged是PIN码变化时的回调(可选)。

  6. PinLockDecoration:用于自定义PinLock小部件的外观,如活动和非活动状态下的颜色、边框颜色和宽度等。

  7. 结果显示:使用Text小部件显示输入的PIN码结果。

  8. dispose方法:在dispose方法中释放TextEditingController资源。

这个示例展示了如何使用pin_lock插件创建一个基本的PIN码输入界面。你可以根据需要进一步自定义和扩展这个示例,例如添加PIN码验证逻辑、错误提示等。

回到顶部