Android 开发者的 Flutter 指南 — UI 相关

UI 异步

runOnUiThread() 对应什么?

Dart 是一种支持 Isolate(多线程)、事件循环和异步编程的单线程模型。除非使用 Isolate,否则 Dart 代码将由事件循环器驱动运行在主线程中。事件循环等同于 Android 中的 main Looper 。

单线程模型并不意味着需要将所有代码都以阻塞 UI 的方式进行操作。可以使用 Dart 提供的异步工具,如 async/await

示例:使用 async/await 请求网络且不阻塞 UI

loadData() async {
  String dataURL = "https://jsonplaceholder.typicode.com/posts";
  http.Response response = await http.get(dataURL);
  setState(() {
    widgets = json.decode(response.body);
  });
}

示例:加载数据,并显示到 ListView

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

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

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State {
  List widgets = [];

  @override
  void initState() {
    super.initState();

    loadData();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView.builder(
          itemCount: widgets.length,
          itemBuilder: (BuildContext context, int position) {
            return getRow(position);
          }));
  }

  Widget getRow(int i) {
    return Padding(
      padding: EdgeInsets.all(10.0),
      child: Text("Row ${widgets[i]["title"]}")
    );
  }

  loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = json.decode(response.body);
    });
  }
}

如何使用后台线程?

Android 中可能会用到 AsyncTask、LiveData、IntentService、JobSchedule、 RxJava

由于 Flutter 是单线程(类似 Node.js),执行事件循环的,所以不必担心如何创建和管理线程。

  • async/await:IO 操作,如磁盘存储、网络请求等;
  • Isolate:实现并发,类似线程,但不共享内存,是独立的程序执行环境,无法直接访问主线程变量或更新 UI。利用 CPU 多核的性质处理事务。默认环境 main isolate。
//示例:async/await
loadData() async {
  String dataURL = "https://jsonplaceholder.typicode.com/posts";
  http.Response response = await http.get(dataURL);
  setState(() {
    widgets = json.decode(response.body);
  });
}

//示例:isolate 中如何返回数据到主线程并更新 UI
loadData() async {
  ReceivePort receivePort = ReceivePort();
  await Isolate.spawn(dataLoader, receivePort.sendPort);

  // The 'echo' isolate sends its SendPort as the first message
  SendPort sendPort = await receivePort.first;

  List msg = await sendReceive(sendPort, "https://jsonplaceholder.typicode.com/posts");

  setState(() {
    widgets = msg;
  });
}

// The entry point for the isolate
static dataLoader(SendPort sendPort) async {
  // Open the ReceivePort for incoming messages.
  ReceivePort port = ReceivePort();

  // Notify any other isolates what port this isolate listens to.
  sendPort.send(port.sendPort);

  await for (var msg in port) {
    String data = msg[0];
    SendPort replyTo = msg[1];

    String dataURL = data;
    http.Response response = await http.get(dataURL);
    // Lots of JSON to parse
    replyTo.send(json.decode(response.body));
  }
}

Future sendReceive(SendPort port, msg) {
  ReceivePort response = ReceivePort();
  port.send([msg, response.sendPort]);
  return response.first;
}

dataLoader()就是运行在独立的线程 Isolate 中。

完整示例

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:async';
import 'dart:isolate';

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

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  showLoadingDialog() {
    if (widgets.length == 0) {
      return true;
    }

    return false;
  }

  getBody() {
    if (showLoadingDialog()) {
      return getProgressDialog();
    } else {
      return getListView();
    }
  }

  getProgressDialog() {
    return Center(child: CircularProgressIndicator());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Sample App"),
        ),
        body: getBody());
  }

  ListView getListView() => ListView.builder(
      itemCount: widgets.length,
      itemBuilder: (BuildContext context, int position) {
        return getRow(position);
      });

  Widget getRow(int i) {
    return Padding(padding: EdgeInsets.all(10.0), child: Text("Row ${widgets[i]["title"]}"));
  }

  loadData() async {
    ReceivePort receivePort = ReceivePort();
    await Isolate.spawn(dataLoader, receivePort.sendPort);

    // The 'echo' isolate sends its SendPort as the first message
    SendPort sendPort = await receivePort.first;

    List msg = await sendReceive(sendPort, "https://jsonplaceholder.typicode.com/posts");

    setState(() {
      widgets = msg;
    });
  }

  // the entry point for the isolate
  static dataLoader(SendPort sendPort) async {
    // Open the ReceivePort for incoming messages.
    ReceivePort port = ReceivePort();

    // Notify any other isolates what port this isolate listens to.
    sendPort.send(port.sendPort);

    await for (var msg in port) {
      String data = msg[0];
      SendPort replyTo = msg[1];

      String dataURL = data;
      http.Response response = await http.get(dataURL);
      // Lots of JSON to parse
      replyTo.send(json.decode(response.body));
    }
  }

  Future sendReceive(SendPort port, msg) {
    ReceivePort response = ReceivePort();
    port.send([msg, response.sendPort]);
    return response.first;
  }
}

OkHttp 对应什么?

使用 http package 请求网络。

虽然 http 扩展库并未实现 OkHttp 的所有功能,但抽象出了很多常用的功能。

//在 pubspec.yaml 中添加依赖
dependencies:
  ...
  http: ^0.11.3+16

//使用
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
[...]
  loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = json.decode(response.body);
    });
  }
}

如何显示耗时任务进度?

使用 ProgressIndicator。通过布尔型的标志来控制进度显示。

import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

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

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    loadData();
  }

  showLoadingDialog() {
    return widgets.length == 0;
  }

  getBody() {
    if (showLoadingDialog()) {
      return getProgressDialog();
    } else {
      return getListView();
    }
  }

  getProgressDialog() {
    return Center(child: CircularProgressIndicator());
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Sample App"),
        ),
        body: getBody());
  }

  ListView getListView() => ListView.builder(
      itemCount: widgets.length,
      itemBuilder: (BuildContext context, int position) {
        return getRow(position);
      });

  Widget getRow(int i) {
    return Padding(padding: EdgeInsets.all(10.0), child: Text("Row ${widgets[i]["title"]}"));
  }

  loadData() async {
    String dataURL = "https://jsonplaceholder.typicode.com/posts";
    http.Response response = await http.get(dataURL);
    setState(() {
      widgets = json.decode(response.body);
    });
  }
}

项目结构与资源

分辨率相关的图片资源应该存储在哪里?

存储在 assets 资产文件夹下。
Flutter 遵循像 iOS 一样简单的分辨率格式,如 1.0x、2.0x、3.0x,或其它倍数。

没有单位 dp(device-independent pixels, dip),但有和 dp 类似的逻辑像素(logical pixels),称作 设备像素比例 devicePixelRatio —— 单个逻辑像素中物理像素的比例。

Android Flutter
ldpi 0.75x
mdpi 1.0x
hdpi 1.5x
xhdpi 2.0x
xxhdpi 3.0x
xxxhdpi 4.0x

Flutter 没有预加的文件结构,所以资产文件(Assets)可以放在任意文件夹下。在 pubspec.yaml中声明资产文件的路径后便可以使用。

示例

  1. 创建 images 文件夹,并添加基础图片资源(1.0x),然后创建不同比例的子文件夹:
images/my_icon.png       // Base: 1.0x image
images/2.0x/my_icon.png  // 2.0x image
images/3.0x/my_icon.png  // 3.0x image
  1. pubspec.yaml 中声明:
assets:
 - images/my_icon.jpeg
  1. 使用图片资源
//1. AssetImage
return AssetImage("images/a_dot_burr.jpeg");

//2. Image Widget
@override
Widget build(BuildContext context) {
  return Image.asset("images/my_image.png");
}

如何存储字符串,如何处多语言?

目前最佳方案:在类中添加静态字段。

class Strings {
  static String welcomeMessage = "Welcome To Flutter";
}

//使用
Text(Strings.welcomeMessage)

未来会支持 Flutter 访问 Android 。
鼓励开发人员使用 Intl 包进行国际化和本地化操作。

Gradle 文件对应什么,如何添加依赖?

Flutter 通过 Dart 语言独自构建系统,并通过 Pub 包管理。这些工具将原生 Android 和 iOS 的构建托付给了各自的构建系统。

虽然在 Flutter 项目中的 Android 文件夹下有 Gradle 文件,但只有添加平台集成所需的依赖时才能使用(意思是需要单独添加?)。一般在pubspec.yaml中声明外部依赖项。更多依赖参阅 Pub 。

Activity 与 Fragment

在 Flutter 中,此两种概念均属于 Widget 的范畴。
了解更多,参阅: Flutter For Android Developers : How to design an Activity UI in Flutter.

如何监听 Android Activity 的生命周期?

可以通过绑定 WidgetsBinding 观察者,并监听 didChangeAppLifecycleState() 事件来监听生命周期事件。

可监听生命周期事件:

  • resumed:应用当前可见,可响应用户操作,等同于 Android 的 onResume()
  • paused:应用当前对用户不可见,无法响应用户操作,后台运行,等同于 Android 的 onPause()
  • suspending:应用暂停,仅适用 Android, 等同 onStop ()
  • inactive:应用处于非活跃状态,未接收到用户操作,仅适用iOS;

更多关于生命状态的信息,参阅: AppLifecycleStatus 文档

FlutterActivity 几乎捕获了所有生命周期事件,并交由 Flutter 系统处理,所以大多数情况下,没有必要去监听处理。如果需要监听生命周期来获取或释放本地资源,那么应该在本地添加监听。

//示例
import 'package:flutter/widgets.dart';

class LifecycleWatcher extends StatefulWidget {
  @override
  _LifecycleWatcherState createState() => _LifecycleWatcherState();
}

class _LifecycleWatcherState extends State with WidgetsBindingObserver {
  AppLifecycleState _lastLifecycleState;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    super.dispose();
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    setState(() {
      _lastLifecycleState = state;
    });
  }

  @override
  Widget build(BuildContext context) {
    if (_lastLifecycleState == null)
      return Text('This widget has not observed any lifecycle changes.', textDirection: TextDirection.ltr);

    return Text('The most recent lifecycle state this widget observed was: $_lastLifecycleState.',
        textDirection: TextDirection.ltr);
  }
}

void main() {
  runApp(Center(child: LifecycleWatcher()));
}

布局

LinearLayout 对应什么?

Flutter 中使用 Row 或 Column 可以达到同样的效果。

//row
@override
Widget build(BuildContext context) {
  return Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      Text('Row One'),
      Text('Row Two'),
      Text('Row Three'),
      Text('Row Four'),
    ],
  );
}

//column
@override
Widget build(BuildContext context) {
  return Column(
    mainAxisAlignment: MainAxisAlignment.center,
    children: [
      Text('Column One'),
      Text('Column Two'),
      Text('Column Three'),
      Text('Column Four'),
    ],
  );
}

了解更多,参阅: Flutter For Android Developers : How to design LinearLayout in Flutter?

RelativeLayout 对应什么?

实现方式:

  • 通过组合 Column、Row、Stack;
  • 指定子部件相对于父部件的布局规则。

一个很好的示例: StackOverflow 。

ScrollView 对应什么?

最简单的方式是使用 ListView ,Flutter 中的 ListView 等同于 Scroll View 和 Android 中的 ListView。

@override
Widget build(BuildContext context) {
  return ListView(
    children: [
      Text('Row One'),
      Text('Row Two'),
      Text('Row Three'),
      Text('Row Four'),
    ],
  );
}

如何处理横屏切换?

当 AndroidManifest.xml 添加如下内容,FlutterView 将会处理配置更改:

android:configChanges="orientation|screenSize"

手势检测与触摸事件处理

Widget 如何添加点击事件?

两种方式:

  1. Widget 支持事件检测,则可直接传递函数并处理:
@override
Widget build(BuildContext context) {
  return RaisedButton(
      onPressed: () {
        print("click");
      },
      child: Text("Button"));
}
  1. Widget 不支持事件检测,则使用 GestureDetector 包装 Widget,并将函数传递给参数 onTab
class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
      child: GestureDetector(
        child: FlutterLogo(
          size: 200.0,
        ),
        onTap: () {
          print("tap");
        },
      ),
    ));
  }
}

如何处理 Widget 的其他手势?

使用 GestureDetector 可以监听到很多手势:

  • 点击 Tab
    1. onTabDown: 按下
    2. onTabUp: 抬起
    3. onTab: 点击
    4. onTabCancel: 触发 onTabDown ,但未触发 onTabUp
  • 双击 Double tab
    1. onDoubleTab: 快速点击同一位置两次
  • 长按 Long press
    1. onLongPress:长时间按住某一位置
  • 垂直拖动 Vertical drag
    1. onVerticalDragStart: 手势按下,开始垂直移动
    2. onVerticalDragUpdate: 手势滑动,屏幕跟随垂直移动
    3. onVerticalDragEnd: 手势抬起,屏幕按照特定速度垂直滚动
  • 水平拖动 Horizontal drag
    1. onHorizontalDragStart:手势按下,开始水平移动
    2. onHorizontalDragUpdate: 手势滑动,屏幕跟随水平移动
    3. onHorizontalDragEnd:手势抬起,屏幕按照特定速度水平滚动

示例:双击旋转Logo

AnimationController controller;
CurvedAnimation curve;

@override
void initState() {
  controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
  curve = CurvedAnimation(parent: controller, curve: Curves.easeIn);
}

class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
          child: GestureDetector(
            child: RotationTransition(
                turns: curve,
                child: FlutterLogo(
                  size: 200.0,
                )),
            onDoubleTap: () {
              if (controller.isCompleted) {
                controller.reverse();
              } else {
                controller.forward();
              }
            },
        ),
    ));
  }
}

ListView & Adapter

ListView 对应什么?

对应 ListView。
由于 Flutter Widget 是不可变的,只需要将一个 List 传递给 ListView,然后 Flutter 会确保其快速流畅的滑动。

import 'package:flutter/material.dart';

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

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView(children: _getListData()),
    );
  }

  _getListData() {
    List widgets = [];
    for (int i = 0; i < 100; i++) {
      widgets.add(Padding(padding: EdgeInsets.all(10.0), child: Text("Row $i")));
    }
    return widgets;
  }
}

如何知道点击了哪个子项?

通过传入的子项 Widget 的手势事件进行处理。

import 'package:flutter/material.dart';

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

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView(children: _getListData()),
    );
  }

  _getListData() {
    List widgets = [];
    for (int i = 0; i < 100; i++) {
      widgets.add(GestureDetector(
        child: Padding(
            padding: EdgeInsets.all(10.0),
            child: Text("Row $i")),
        onTap: () {
          print('row tapped');
        },
      ));
    }
    return widgets;
  }
}

如何动态更新 ListVIew ?

此时,使用 setState() 方法并不会更新。因为调用 setState() 时,Flutter 渲染引擎会遍历 Widget 树(所有 Widget )查看是否有改变。当遍历到 ListVIew 时,会做 ==操作检查,并确认前后两个 ListView 是相同的。所以没有改变,就不会更新。

一种简单的更新方式
在 setState() 中创建一个新的列表(List),然后将旧列表数据全部复制到新列表。该方式简单,但不建议在数据量大时使用。

import 'package:flutter/material.dart';

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

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    for (int i = 0; i < 100; i++) {
      widgets.add(getRow(i));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: ListView(children: widgets),
    );
  }

  Widget getRow(int i) {
    return GestureDetector(
      child: Padding(
          padding: EdgeInsets.all(10.0),
          child: Text("Row $i")),
      onTap: () {
        setState(() {
          widgets = List.from(widgets);
          widgets.add(getRow(widgets.length + 1));
          print('row $i');
        });
      },
    );
  }
}

推荐方式
推荐使用 ListView.Builder,十分适用于大数据量的动态更新。本质上与 Android 中的 RecyclerView 相同——会自动复用表元素。

两个参数:

  • itemCount:列表初始长度
  • itemBuilder:类似 Android 适配器中的 getView 方法
import 'package:flutter/material.dart';

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

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State {
  List widgets = [];

  @override
  void initState() {
    super.initState();
    for (int i = 0; i < 100; i++) {
      widgets.add(getRow(i));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Text("Sample App"),
        ),
        body: ListView.builder(
            itemCount: widgets.length,
            itemBuilder: (BuildContext context, int position) {
              return getRow(position);
            }));
  }

  Widget getRow(int i) {
    return GestureDetector(
      child: Padding(
          padding: EdgeInsets.all(10.0),
          child: Text("Row $i")),
      onTap: () {
        setState(() {
          widgets.add(getRow(widgets.length + 1));
          print('row $i');
        });
      },
    );
  }
}

注意:onTab() 方法中并没有重新创建列表,而是向列表中添加了新元素。

文本使用

Text Widget 如何自定义字体?

将字体文件存入项目文件夹,并在 pubspec.yaml 中声明,类似使用图片文件。

//声明
fonts:
   - family: MyCustomFont
     fonts:
       - asset: fonts/MyCustomFont.ttf
       - style: italic

//使用
@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text("Sample App"),
    ),
    body: Center(
      child: Text(
        'This is a custom font text',
        style: TextStyle(fontFamily: 'MyCustomFont'),
      ),
    ),
  );
}

Text Widget 如何设置样式?

通过 Text Widget 的 TextStyle 参数自定义样式:

名称
color
decoration
decorationColor
decorationStyle
fontFamily
fontSize
fontStyle
fontWeight
hashCode
height
inherit
letterSpacing
textBaseline
wordSpacing

表单

了解更多表单,参阅 Flutter Cookbook 的 Retrieve the value of a text field 。

输入框如何显示 hint ?

body: Center(
  child: TextField(
    decoration: InputDecoration(hintText: "This is a hint"),
  )
)

如何显示验证错误信息 ?

通过 setState 更新,并传递新的 InputDecoration

import 'package:flutter/material.dart';

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

class SampleApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Sample App',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: SampleAppPage(),
    );
  }
}

class SampleAppPage extends StatefulWidget {
  SampleAppPage({Key key}) : super(key: key);

  @override
  _SampleAppPageState createState() => _SampleAppPageState();
}

class _SampleAppPageState extends State {
  String _errorText;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: Center(
        child: TextField(
          onSubmitted: (String text) {
            setState(() {
              if (!isEmail(text)) {
                _errorText = 'Error: This is not an email';
              } else {
                _errorText = null;
              }
            });
          },
          decoration: InputDecoration(hintText: "This is a hint", errorText: _getErrorText()),
        ),
      ),
    );
  }

  _getErrorText() {
    return _errorText;
  }

  bool isEmail(String em) {
    String emailRegexp =
        r'^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$';

    RegExp regExp = RegExp(emailRegexp);

    return regExp.hasMatch(em);
  }
}

你可能感兴趣的:(Android 开发者的 Flutter 指南 — UI 相关)