Flutter中实现Autofill(Android)&Keychain(iOS)密码自动填充功能

为什么要写这篇文章,第一是做了这个功能,记录一下,为后来者提供思路和解决方案;第二是因为我们走了弯路,我们探了雷,所以为后来者提点注意事项;O(∩_∩)O哈哈~

某日,前沿战地提来需求,需要在登录页面添加密码自动填充,自动保存,还可以编辑删除功能。预研一下,并尽快评估一下开发耗时。

后方地勤人员接到命令后,火速赶往pub.dev。找出了最优的库,并demo试之。发现可行(仅仅能填入密码,没覆盖保存和删除功能),反馈于前沿征地。

一、开干

至于那个库就不说了,后面发现前面没测保存和删除功能,而也没有去研究Flutter TextFiled自带的功能(PS: 其实它是自带Autofill功能的,这后面再说)

然后,直接从原生入手,想着原生可以实现的,只要把原生的View写好,功能实现,在Flutter端直接引用原生UI就行了。这倒好,这又开始学了一把Flutter调用显示原生UI的技术(后边再写一文,讲讲),但是iOS实现了,Android这边出了问题,单个显示原生EditText时可以弹出输入框,展示多个EditText时候,输入框都弹不出来。

试了好多办法,单个的EditText不行,用LinearLayout或者其他FrameLayout包裹也不行,实在找不到原因了。就开始Google,却发现了新大陆。

二、撕开迷雾,初见光明

最开始呢,其他同学先做了iOS原生方式实现了KeyChain功能,想着Android也得做原生方式实现Autofill自动填充功能。只能交给我这根独苗苗来了,但是实际过程中,发现flutter调用原生视图方式输入框起不来。只能另寻他法了。

皇天不负有心人,让我发现了新大陆,欣喜若狂~

参考链接如下:
1. 自动填充框架(Android)
2. Level up your Flutter apps with autofill
3. How to implement autofill in your Flutter app
4. 其实最有用的是这个 -> Flutter Tutorial - Autofill Services In 5 Minutes - Android & iOS

前面的1. 自动填充框架(Android)是让我们能了解下Android端的自动填充是个什么玩意儿,原生端是怎么玩的。这对于我们在Flutter端去使用和开发Autofill功能是很有帮助的。

而4. 其实最有用的是这个 -> Flutter Tutorial - Autofill Services In 5 Minutes - Android & iOS这是最有用的,这是个GitHub上的一个Example,具体从哪找的,忘了。

三、Flutter端的开发

在Flutter端如果要实现KeyChain和Autofill功能,其实很简单。Flutter早已帮你实现,我们是纯属想多O(∩_∩)O哈哈~。

3.1 如果说我们要实现账户名密码的自动填充,提示登录成功后弹出保存按钮,只需要实现下面代码:
1. AutofillGroup
2. autofillHints: [AutofillHints.username]
3. TextInput.finishAutofillContext()

  Widget buildUsernameAndPassword() => AutofillGroup(
        child: Column(
          children: [
            TextField(
              controller: usernameController,
              decoration: InputDecoration(labelText: 'Username'),
              keyboardType: TextInputType.name,
              textInputAction: TextInputAction.next,
              autofillHints: [AutofillHints.username],
            ),
            TextField(
              controller: passwordController,
              decoration: InputDecoration(labelText: 'Password'),
              keyboardType: TextInputType.visiblePassword,
              autofillHints: [AutofillHints.password],
              onEditingComplete: () => TextInput.finishAutofillContext(),
            ),
          ],
        ),
      );

这里需要注意的是:

    1. 两个TextField需要被AutofillGroup包裹,它是将AutofillClient(这里是指TextField)进行组合在一起的Widget,这些被AutofillGroup包裹的AutofillClients需要一块构建,它们将被一起填充,比如说,点了用户名,提示账户名密码后,选择后会自动将用户名和密码输入框都填充好。
如果你保存过该App的账户密码,那么点击输入框的时候就会弹出存储的信息供你选择
选择某个账号信息后,因为是使用的AutofillGroup包裹,那它将会自动将用户名和密码输入框都自动填充
    1. 看到上面代码中TextField里的autofillHints: [AutofillHints.username]和autofillHints: [AutofillHints.password]这里就和iOS、Android原生类似了。在原生中: Android是在EditText属性中加入android:autofillHints="username";而iOS则是在textField.textContentType = UITextContentTypeUsername或UITextContentTypePassword;
    1. AutofillHints除了AutofillHints.username;AutofillHints.password外还有很多,像email、Address、telephoneNumber、creditCard等等,大家可以进autofill.dart这个类里看看很多种类的填充服务。
    1. 如果说你是输入了新的用户名和密码,需要保存到KeyChain或者AutofillService中去的时候,就需要调用TextInput.finishAutofillContext(),他可以放到Sign In按钮功能登录成功后调用,另外其实它有个可选参数的static void finishAutofillContext({ bool shouldSave = true });默认shouldSave为true表示需要保存,如果不需要保存的时候则可以传shouldSave为false。
  void doLogin(BuildContext context) async {
    TextInput.finishAutofillContext();
    Navigator.push(context, MaterialPageRoute(
        builder: (context){
          return Container(
            color: Colors.redAccent,
          );
        }
      )
    );
  }
3.2 如果说你点击输入框,弹出了用户名/密码选择器,你选了个密码,发现选错了删除后,再想从AutoFillService选择用户名/密码时,发现它弹不出来了。

那么这时候TextInput.finishAutofillContext()就派上用场,我的做法是在空白处点击,当用户名或者密码有一个为空的时候调用TextInput.finishAutofillContext(shouldSave: false);当调用这句代码的时候代表此处AutofillService结束,输入框也会取消焦点,你再次点击输入框的时候又会再次弹出用户名/密码选择列表。

Scaffold(
          body: Container(
              child: GestureDetector(
                  behavior: HitTestBehavior.translucent,
                  onTap: () {
                    String password = passwordController.text;
                    String userName = userNameController.text;
                    if (StringFormatUtil.isStringEmpty(userName) ||
                        StringFormatUtil.isStringEmpty(password)) {
                      TextInput.finishAutofillContext(shouldSave: false);
                    }
                  },
                 ),
            ),
          ),
3.3 上面所说的是,当两个输入框用户名和密码输入框都在一起的时候,当用户名和密码输入框在两个页面的时候咋办?

我这里的情况是这样的:

  • 第一个页面输入用户名,并Check正确后在跳转到密码页
  • 将用户名带到密码页,并显示用户名,和密码输入框


    用户名页
密码页
    1. 针对第一个用户名页面,需要填充UserName时,还是一样玩:需要AutofillGroup包裹TextField(如果没有包裹,点击输入框也可以弹出用户名选择器,但是点击了不能自动填充),autofillHints这个属性就是老生常谈了。
  Widget buildUserNameView(BuildContext context) {
    return AutofillGroup(
        child: TextField(
              controller: usernameController,
              decoration: InputDecoration(labelText: 'Username'),
              keyboardType: TextInputType.name,
              textInputAction: TextInputAction.next,
              autofillHints: [AutofillHints.username],
            ),
    );
  }
    1. 在密码页,因为之前UserName是使用Text控件而不是TextField,如果需要实现Autofill保存功能,只能是两个TextField在上下一起,即:用户名输入框和密码输入框。
密码页

如果我们要实现这样的UI同时又满足都是TextField情况,那么只能吧UserName显示的Widget从Text改为TextField,而这个用户名的输入框,没有背景并且不能点击且autofillHints: [AutofillHints.username]。

但是实际过程中发现,如果设置了TextField属性readOnly: true或者enabled: false和autofillHints: [AutofillHints.username],会发现报错。也就是当TextField 不可点击状态和autofillHints,不能同时存在,它们互斥。找到editable_text。我们看到了报错处:会断言有readOnly就不能有autofillHints;有autofillHints就不能readOnly。

      assert(
         !readOnly || autofillHints == null,
         "Read-only fields can't have autofill hints.",
       ),

这里的话我们就只能魔改,使之成为本地Widget,将这个断言注释了。

import 'package:base_widget/editable_text/text_field.dart' as MargicTextField;

userNameController.value = TextEditingValue(text: userName);
Widget buildPasswordTextFiledWidget() => AutofillGroup(
        child: Column(
          children: [
            //这里是魔改注释了上面断言的本地TextField
            MargicTextField.TextField(
              controller: usernameController,
              textInputAction: TextInputAction.next,
              readOnly: true,
              textAlign: TextAlign.center,
              autofillHints: [AutofillHints.username],
            ),
            TextField(
              controller: passwordController,
              decoration: InputDecoration(labelText: 'Password'),
              keyboardType: TextInputType.visiblePassword,
              autofillHints: [AutofillHints.password],
              onEditingComplete: () => TextInput.finishAutofillContext(),
            ),
          ],
        ),
      );

Flutter端的用法就差不多这样了。

四、Android端运行需要做的事儿

如果在Flutter端已经配置后autofillHints和TextInput.finishAutofillContext()的话,就很简单了。Android手机上需要打开AutofillService服务,因为它默认是关闭的。

路径: 设置 -> 系统 -> 语言与输入法 -> 高级 -> 输入帮助 -> 自动填充服务

藏得比较深,有些手机路径可能不绝对这样,但也差不多。这里选择一个自动填充服务就好了。功能就可以使用了。

自动填充服务选择

五、iOS端的配置

iOS就比较麻烦了:

    1. 首先得将设置 -> 密码 -> 自动填充密码 开关打开
    1. 然后需要配置Web Credentials,如果没有配置的话,能取到密码,但是不能保存。

用户在Safari登录网站时,通常会在iCloud钥匙串中保存用户名和密码。随后,用户可能会打开源于同一个开发者的应用程序来访问同一个帐户。使用webcredentials,应用可以访问为网站存储的证书,无需用户重新输入用户名和密码。用户还可以在应用内创建新帐户,更新密码或删除帐户,Safari会保存并使用这些修改。

具体如何配置看看这篇文章UniversalLinks和Web Credentials配置

将Web Credentials相关文件(需命名为apple-app-site-association)写好,并由后端人员将apple-app-site-association文件上传到HTTPS Web服务器(该步骤需要将写好的apple-app-site-association文件交给服务端人员,让他们完成上传过程。)。放在服务器的根目录或.well-known子目录中。https:///apple-app-site-associationor https:///.well-known/apple-app-site-association。

另外需要在工程下做如下配置:

  • xcode打开iOS工程配置Associated Domains


    配置好

六、结语

到这,KeyChain/Autofill自动填充功能就介绍完了,留个脚印,以备不时之需~

你可能感兴趣的:(Flutter中实现Autofill(Android)&Keychain(iOS)密码自动填充功能)