Android 開發(fā)者的 Flutter 指南 — UI 相關

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中聲明資產文件的路徑后便可以使用。

示例

  1. 創(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
  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");
}

如何存儲字符串,如何處多語言?

目前最佳方案:在類中添加靜態(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 如何添加點擊事件?

兩種方式:

  1. Widget 支持事件檢測,則可直接傳遞函數(shù)并處理:
@override
Widget build(BuildContext context) {
  return RaisedButton(
      onPressed: () {
        print("click");
      },
      child: Text("Button"));
}
  1. 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
    1. onTabDown: 按下
    2. onTabUp: 抬起
    3. onTab: 點擊
    4. onTabCancel: 觸發(fā) onTabDown ,但未觸發(fā) 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<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 CookbookRetrieve 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);
  }
}
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容