发布时间:2020年8月8日-6分钟阅读

这是官方的, Flutter 1.20来了 。在所有的好东西和改进中,移动自动填充在标语中得到了强调,甚至有一个gif展示了用户体验是多么的光滑。遗憾的是,目前还没有太多例子、教程或窍门,因此才有了这篇文章的诞生。

什么/为什么要自动填充?

自动填报在行动,功不可没的Flutter团队

来自 Android开发者指南

填写表格是一项耗时且容易出错的任务。用户很容易对需要这种操作的应用程序感到沮丧。自动填写框架通过提供以下好处来改善用户体验。

减少填写字段的时间 。自动填写节省了用户重新输入信息的时间。

尽量减少用户输入错误 。输入容易出错,尤其是在移动设备上。最大限度地减少输入信息的需求,也最大限度地减少输入错误。

开始使用没有自动填写的表单

我们先做一个简单的表格,里面有地址、电话,还有一个以后不要自动填写的字段。

代码可以在 这个repo 上找到。如果你关注这些字段中的任何一个,你应该看到光标闪烁,但没有自动填充弹出。

安装Android自动填充框架示例

你可能已经有了一些自动填写服务(如LastPass、Bitwarden),也可能没有。无论哪种情况,建议你在开发过程中不要使用它们,而是使用服务的参考实现。按照本 CodeLabs 第 1 步 的指示,在设备上设置 Android 自动填充框架示例。

样品安装好后,选择调试 Debug Autofill Service 。现在,进入任何一个使用OEM控件(如使用Android SDK、React Native或Ionic编写的应用,但 不能 使用Flutter也不能使用Unity),关注任何一个文本字段,你会发现自动填写服务出现了不理想的情况。一般来说,我们希望我们的应用只在某些字段可以自动填写,比如 姓名 地址 信用卡 密码 等,而在其他字段,比如 备注 搜索 等字段,则不能自动填写。

在Flutter应用程序中从未发生过意外的自动填充弹出。

自动填充服务以某种方式意外出现的原因是,如果开发者没有明确地编写代码,那么利用OEM控件的Android应用程序可以从其控件ID中推断出自动填充语义。这种推断在大多数时候是有效的,但并非总是如此。

Android XML可以从widget ID推断自动填充语义。截图来自 《确保应用程序与Android自动填充兼容的快速方法》,2:11

与Android XML不同,您的Flutter应用程序不会自动具有自动填充功能,即使您针对Flutter 1.20重新编译您的代码库。事实上,根本没有相当于 android:importantForAutofill 的标签。原因是,虽然在Android中,一个元素可以有 android:id 标签,但在Flutter中却不是这样。

在Flutter中没有对应的android:importantForAutofill。截图来自 《确保应用兼容Android自动填充的快速方法》,4:03

我们Flutter应用中的第一个自动填充弹出窗口

第一步很简单,所有由 EditableText 或其祖先组成的widget,如 TextFormField TextField 都有我们可以设置的 autofillHints 参数。

diff --git a/lib/main.dart b/lib/main.dart
index 1460ece..ae75939 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -35,12 +35,14 @@ class _MyStatefulWidgetState extends State<MyStatefulWidget> {
         padding: EdgeInsets.symmetric(horizontal: 16),
         children: [
           TextFormField(
+            autofillHints: [AutofillHints.streetAddressLine1],
             decoration: const InputDecoration(
               labelText: 'Shipping address 1',
               hintText: 'Number and street',
           TextFormField(
+            autofillHints: [AutofillHints.streetAddressLine2],
             decoration: const InputDecoration(
               labelText: 'Shipping address 2',
               hintText: '(optional) APT number, c/o',

尽管这个参数接受一个字符串列表(确切地说,是可迭代的),但建议你不要传入一个任意的字符串,而是使用AutofillHints的静态值,这是一个常用的自动填充提示集合。这将帮助你避免错别字,也能保证跨平台的兼容性。

自动填写提示(AutofillHints),集合了常用的自动填写提示,有助于避免错别字,保证跨平台的兼容性。

现在,回到我们的应用中,关注 "送货地址1",我们会看到自动填写的弹窗出现。让我们点开后,对 "送货地址2 "进行同样的操作。

在我们的Flutter应用中首次弹出了自动填充的对话框,Yay!

AutofillGroup

到目前为止,要填写这2行地址,用户要点两次自动填写的弹窗。这比理想中的要费力,也比较容易出错。比如说,如果用户,有多个地址。

比如: 华尔街11号

纽约州纽约市10005

1600 Pennsylvania Avenue NW

Washington, DC 20500

有可能是用户误选了不同字段的不同条目,导致数据损坏。

华尔街11号

Washington, DC 20500

在这种情况下,可以使用AutofillGroup,这样用户只需点击一次,服务就会自动填充所有相关字段,这些字段是widget树中最近的AutofillGroup的子代。

diff --git a/lib/main.dart b/lib/main.dart
index ae75939..4f1704b 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -33,6 +33,9 @@ class _MyStatefulWidgetState extends State<MyStatefulWidget> {
   @override
   Widget build(BuildContext context) => ListView(
         padding: EdgeInsets.symmetric(horizontal: 16),
+        children: [
+          AutofillGroup(
+            child: Column(
               children: [
                 TextFormField(
                   autofillHints: [AutofillHints.streetAddressLine1],
@@ -48,6 +51,9 @@ class _MyStatefulWidgetState extends State<MyStatefulWidget> {
                     hintText: '(optional) APT number, c/o',
+              ],
+            ),
+          ),
           CheckboxListTile(
             contentPadding: EdgeInsets.zero,
             title: const Text("Billing address same as shipping address"),

将自动填充功能添加到我们的应用程序的其他地方

这一步很直接,和之前的步骤类似。

diff --git a/lib/main.dart b/lib/main.dart
index 4f1704b..526ac61 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -65,15 +65,18 @@ class _MyStatefulWidgetState extends State<MyStatefulWidget> {
           if (!isSameAddress)
-            Column(
+            AutofillGroup(
+              child: Column(
                 children: [
                   TextFormField(
+                    autofillHints: [AutofillHints.streetAddressLine1],
                     decoration: const InputDecoration(
                       labelText: 'Billing address 1',
                       hintText: 'Number and street',
                   TextFormField(
+                    autofillHints: [AutofillHints.streetAddressLine2],
                     decoration: const InputDecoration(
                       labelText: 'Billing address 2',
                       hintText: '(optional) APT number, c/o',
@@ -81,17 +84,27 @@ class _MyStatefulWidgetState extends State<MyStatefulWidget> {
+            ),
+          AutofillGroup(
+            child: Column(
+              children: [
                 TextFormField(
+                  autofillHints: [AutofillHints.creditCardNumber],
                   decoration: const InputDecoration(
                     labelText: 'Credit Card #',
                 TextFormField(
+                  autofillHints: [AutofillHints.creditCardSecurityCode],
                   decoration: const InputDecoration(
                     labelText: 'CCV',
+              ],
+            ),
+          ),
           TextFormField(
+            autofillHints: [AutofillHints.telephoneNumber],
             decoration: const InputDecoration(
               labelText: 'Contact Phone Number',

嵌套的AutofillGroup

在这一点上,我们可以很满意我们的应用程序。但是,电话号码通常是和送货地址一起填写的。换句话说,电话号码字段应该与地址行1和地址行2在同一个AutofillGroup中。但是,在我们的widget中,这就不太容易了,因为,在中间,我们还有账单地址字段。

这时,我正准备用GlobalKey来获取AutofillGroupState,但事实证明,还有一个更简单的方法。AutofillGroup可以嵌套,子孙字段只会在widget树中最近的AutofillGroup widget下进行分组。

diff --git a/lib/main.dart b/lib/main.dart
index 526ac61..439d0fb 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -31,11 +31,11 @@ class _MyStatefulWidgetState extends State<MyStatefulWidget> {
   // has been removed
   @override
-  Widget build(BuildContext context) => ListView(
+  Widget build(BuildContext context) => AutofillGroup(
+        child: ListView(
           padding: EdgeInsets.symmetric(horizontal: 16),
           children: [
-          AutofillGroup(
-            child: Column(
+            Column(
               children: [
                 TextFormField(
                   autofillHints: [AutofillHints.streetAddressLine1],
@@ -53,7 +53,6 @@ class _MyStatefulWidgetState extends State<MyStatefulWidget> {
-          ),
             CheckboxListTile(
               contentPadding: EdgeInsets.zero,
               title: const Text("Billing address same as shipping address"),
@@ -116,5 +115,6 @@ class _MyStatefulWidgetState extends State<MyStatefulWidget> {
+        ),

我们可以看到信用卡字段仍然被正确地分组,即使它被嵌套在另一个AutofillGroup中,而这个的目的是用来填写送货地址和电话号码。

一些需要注意的问题

在演讲《快速确保应用兼容Android自动填充的方法》中,有一个警告,Android SDK中的一个反模式,视图层次结构是在onStart()生命周期上创建的,会使自动填充服务循环地要求用户认证,从而破坏自动填充的用户体验。幸运的是,我们不能在Flutter中出现这种情况,因为Flutter是(半)声明式的,不可能在生命周期事件上强制创建/绘制视图。

然而,如果你有条件地构建widgets,要小心上面第4步中嵌套的AutofillGroup,在我们的例子中,if (!isSameAddress)。试着自动填写信用卡字段,然后取消勾选,我们可以看到信用卡字段的值会被移动到账单地址字段。

解决办法是使用UniqueKey。解释值得一整篇文章,[Flutter]Keys!它们有什么用?,但简短的回答是,由于Flutter框架为重建做树状区分的方式,如果没有额外的key的帮助,它不能可靠地区分相同类型的widgets(在这种情况下,TextFormField)。

diff --git a/lib/main.dart b/lib/main.dart
index 439d0fb..fafcf3a 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -27,6 +27,9 @@ class MyStatefulWidget extends StatefulWidget {
 class _MyStatefulWidgetState extends State<MyStatefulWidget> {
   var isSameAddress = true;
+  final billingAddress1Key = UniqueKey();
+  final billingAddress2Key = UniqueKey();
   // For the sake of simplicity, Form widget, _formkey, and textController
   // has been removed
@@ -68,6 +71,7 @@ class _MyStatefulWidgetState extends State<MyStatefulWidget> {
                 child: Column(
                   children: [
                     TextFormField(
+                      key: billingAddress1Key,
                       autofillHints: [AutofillHints.streetAddressLine1],
                       decoration: const InputDecoration(
                         labelText: 'Billing address 1',
@@ -75,6 +79,7 @@ class _MyStatefulWidgetState extends State<MyStatefulWidget> {
                     TextFormField(
+                      key: billingAddress2Key,
                       autofillHints: [AutofillHints.streetAddressLine2],
                       decoration: const InputDecoration(
                         labelText: 'Billing address 2',

正如发布公告中提到的那样,自动填充是一段时间以来要求最多的Flutter功能之一,我们终于得到了它,Android和iOS都有(web在路上,我们还没有桌面上的自动填充生态系统)。事实证明,与Android不同,开发者必须明确地修改他们的代码来支持自动填充。此外,Flutter中的字段和自动填充的问题与Android中的有很大不同。

本文中使用的示例代码和差异在:github.com/truongsinh/…

www.twitter.com/FlutterComm