UI 異步
runOnUiThread() 對應什么?
Dart 是一種支持 Isolate(多線程)、事件循環(huán)和異步編程的單線程模型。除非使用 Isolate,否則 Dart 代碼將由事件循環(huán)器驅動運行在主線程中。事件循環(huán)等同于 Android 中的 main Looper 。
單線程模型并不意味著需要將所有代碼都以阻塞 UI 的方式進行操作。可以使用 Dart 提供的異步工具,如 async/await。
示例:使用 async/await 請求網(wǎng)絡且不阻塞 UI
loadData() async {
String dataURL = "https://jsonplaceholder.typicode.com/posts";
http.Response response = await http.get(dataURL);
setState(() {
widgets = json.decode(response.body);
});
}
示例:加載數(shù)據(jù),并顯示到 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<SampleAppPage> {
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),執(zhí)行事件循環(huán)的,所以不必擔心如何創(chuàng)建和管理線程。
- async/await:IO 操作,如磁盤存儲、網(wǎng)絡請求等;
- Isolate:實現(xiàn)并發(fā),類似線程,但不共享內存,是獨立的程序執(zhí)行環(huán)境,無法直接訪問主線程變量或更新 UI。利用 CPU 多核的性質處理事務。默認環(huán)境 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 中如何返回數(shù)據(jù)到主線程并更新 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<SampleAppPage> {
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 請求網(wǎng)絡。
雖然 http 擴展庫并未實現(xiàn) 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<SampleAppPage> {
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,或其它倍數(shù)。
沒有單位 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中聲明資產文件的路徑后便可以使用。
示例
- 創(chuàng)建 images 文件夾,并添加基礎圖片資源(1.0x),然后創(chuàng)建不同比例的子文件夾:
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
- 在
pubspec.yaml中聲明:
assets:
- images/my_icon.jpeg
- 使用圖片資源
//1. AssetImage
return AssetImage("images/a_dot_burr.jpeg");
//2. Image Widget
@override
Widget build(BuildContext context) {
return Image.asset("images/my_image.png");
}
如何存儲字符串,如何處多語言?
目前最佳方案:在類中添加靜態(tài)字段。
class Strings {
static String welcomeMessage = "Welcome To Flutter";
}
//使用
Text(Strings.welcomeMessage)
未來會支持 Flutter 訪問 Android 。
鼓勵開發(fā)人員使用 Intl 包進行國際化和本地化操作。
Gradle 文件對應什么,如何添加依賴?
Flutter 通過 Dart 語言獨自構建系統(tǒng),并通過 Pub 包管理。這些工具將原生 Android 和 iOS 的構建托付給了各自的構建系統(tǒng)。
雖然在 Flutter 項目中的 Android 文件夾下有 Gradle 文件,但只有添加平臺集成所需的依賴時才能使用(意思是需要單獨添加?)。一般在pubspec.yaml中聲明外部依賴項。更多依賴參閱 Pub 。
Activity 與 Fragment
在 Flutter 中,此兩種概念均屬于 Widget 的范疇。
了解更多,參閱: Flutter For Android Developers : How to design an Activity UI in Flutter.
如何監(jiān)聽 Android Activity 的生命周期?
可以通過綁定 WidgetsBinding 觀察者,并監(jiān)聽 didChangeAppLifecycleState() 事件來監(jiān)聽生命周期事件。
可監(jiān)聽生命周期事件:
- resumed:應用當前可見,可響應用戶操作,等同于 Android 的 onResume();
- paused:應用當前對用戶不可見,無法響應用戶操作,后臺運行,等同于 Android 的 onPause();
- suspending:應用暫停,僅適用 Android, 等同 onStop ();
- inactive:應用處于非活躍狀態(tài),未接收到用戶操作,僅適用iOS;
更多關于生命狀態(tài)的信息,參閱: AppLifecycleStatus 文檔
FlutterActivity 幾乎捕獲了所有生命周期事件,并交由 Flutter 系統(tǒng)處理,所以大多數(shù)情況下,沒有必要去監(jiān)聽處理。如果需要監(jiān)聽生命周期來獲取或釋放本地資源,那么應該在本地添加監(jiān)聽。
//示例
import 'package:flutter/widgets.dart';
class LifecycleWatcher extends StatefulWidget {
@override
_LifecycleWatcherState createState() => _LifecycleWatcherState();
}
class _LifecycleWatcherState extends State<LifecycleWatcher> 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: <Widget>[
Text('Row One'),
Text('Row Two'),
Text('Row Three'),
Text('Row Four'),
],
);
}
//column
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Column One'),
Text('Column Two'),
Text('Column Three'),
Text('Column Four'),
],
);
}
了解更多,參閱: Flutter For Android Developers : How to design LinearLayout in Flutter?
RelativeLayout 對應什么?
實現(xiàn)方式:
- 通過組合 Column、Row、Stack;
- 指定子部件相對于父部件的布局規(guī)則。
一個很好的示例: StackOverflow 。
ScrollView 對應什么?
最簡單的方式是使用 ListView ,F(xiàn)lutter 中的 ListView 等同于 Scroll View 和 Android 中的 ListView。
@override
Widget build(BuildContext context) {
return ListView(
children: <Widget>[
Text('Row One'),
Text('Row Two'),
Text('Row Three'),
Text('Row Four'),
],
);
}
如何處理橫屏切換?
當 AndroidManifest.xml 添加如下內容,F(xiàn)lutterView 將會處理配置更改:
android:configChanges="orientation|screenSize"

手勢檢測與觸摸事件處理
Widget 如何添加點擊事件?
兩種方式:
- Widget 支持事件檢測,則可直接傳遞函數(shù)并處理:
@override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: () {
print("click");
},
child: Text("Button"));
}
- Widget 不支持事件檢測,則使用
GestureDetector包裝 Widget,并將函數(shù)傳遞給參數(shù)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 可以監(jiān)聽到很多手勢:
-
點擊 Tab
-
onTabDown: 按下 -
onTabUp: 抬起 -
onTab: 點擊 -
onTabCancel: 觸發(fā) onTabDown ,但未觸發(fā) onTabUp
-
-
雙擊 Double tab
-
onDoubleTab: 快速點擊同一位置兩次
-
-
長按 Long press
-
onLongPress:長時間按住某一位置
-
-
垂直拖動 Vertical drag
-
onVerticalDragStart: 手勢按下,開始垂直移動 -
onVerticalDragUpdate: 手勢滑動,屏幕跟隨垂直移動 -
onVerticalDragEnd: 手勢抬起,屏幕按照特定速度垂直滾動
-
-
水平拖動 Horizontal drag
-
onHorizontalDragStart:手勢按下,開始水平移動 -
onHorizontalDragUpdate: 手勢滑動,屏幕跟隨水平移動 -
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<SampleAppPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: ListView(children: _getListData()),
);
}
_getListData() {
List<Widget> 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<SampleAppPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Sample App"),
),
body: ListView(children: _getListData()),
);
}
_getListData() {
List<Widget> 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;
}
}
如何動態(tài)更新 ListVIew ?
此時,使用 setState() 方法并不會更新。因為調用 setState() 時,F(xiàn)lutter 渲染引擎會遍歷 Widget 樹(所有 Widget )查看是否有改變。當遍歷到 ListVIew 時,會做 ==操作檢查,并確認前后兩個 ListView 是相同的。所以沒有改變,就不會更新。
一種簡單的更新方式:
在 setState() 中創(chuàng)建一個新的列表(List),然后將舊列表數(shù)據(jù)全部復制到新列表。該方式簡單,但不建議在數(shù)據(jù)量大時使用。
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<SampleAppPage> {
List widgets = <Widget>[];
@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,十分適用于大數(shù)據(jù)量的動態(tài)更新。本質上與 Android 中的 RecyclerView 相同——會自動復用表元素。
兩個參數(shù):
- 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<SampleAppPage> {
List widgets = <Widget>[];
@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() 方法中并沒有重新創(chuàng)建列表,而是向列表中添加了新元素。
文本使用
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 參數(shù)自定義樣式:
| 名稱 |
|---|
| 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<SampleAppPage> {
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);
}
}