Flutter笔记:给Android开发者的Flutter指南

导引

本文原文在这里:Flutter for Android developers
本文属于半翻译半笔记注解,按照自己的描述整合了原文,如若读者读过原文会发现有些许不一样不要惊讶,但仍然完整表达了原文的意思,文中斜体加粗部分是我自己的注解。


前言

Android的知识和技能对Flutter开发很有价值,因为Flutter依赖于移动系统的众多功能和配置。Flutter构建App的UI的一种新方式,它有一个插件系统去连接Android/IOS原生系统来完成非UI的任务,如果你精通Android,你可以不用学习任何东西直接使用Flutter(当然需要看完这篇指南)

Views

在Flutter中什么东西相当于View

在Android里,View是所有布局的基础,所有的控件都继承于View。在Flutter中,大致相当于View的是Widget。Widget不能和View划上等号,当你熟悉了Flutter的开发后你可以将它们视为你声明和构建UI的方式。

Widget与View主要有这几点不同:

  1. Widgets一经创建,是不可改变的一直存活到他们必须改变为止。当Widgets或者他们的状态发生改变,Flutter会创建新的Widget树的实例,而View则是绘制完成后不会再重绘直到invalidate被调用。
  2. Widgets是轻量级的,部分原因在于他们的不变性。他们不能直接绘制任何东西,而是对UI及其语义的描述,会通过"inflated"生成真实的视图对象中去。
    Flutter包括 Material Components library.这些Widgets实现了Material Design guidelines.Material Design 是一个包括iOS在内的所有平台进行优化的灵活的设计。
    但是Flutter的灵活性和表现力足以实现任何设计语言。例如,在iOS上,您可以使用 Cupertino widgets生成一个类似于 Apple’s iOS design language的界面。

怎样更新Widgets

在Android中可以直接改变View,但是在Flutter里,Widgets 是不可以直接更新的,取而代之的你需要去改变Widgets的状态。
这就是有状态和无状态Widgets的概念来源。 StatelessWidget听起来就像是一个没有状态信息的Widget.
当你描述的UI里有一些部分不依赖于对象中的配置信息时, StatelessWidget非常有用。
例如,在Android中,这类似于放置Logo的ImageView。Logo在运行时不会更改,所以在Flutter中可以使用 StatelessWidget.
如果你想根据HTTP请求或用户交互后接收到的数据动态更改UI,那么你必须使用StatefulWidget,并告诉Flutter widget的状态已经更新,以便它可以更新该widget。
这里需要注意的重要一点是,无状态和有状态widget的行为都是相同的,每一帧都会重建,区别在于StatefulWidget有一个State对象,它跨越帧时序,并且恢复StatefulWidget的状态。
如果你有疑问,那么请始终记住这条规则:如果widget发生更改(例如,由于用户交互),那么它是有状态的。但是,如果widget对更改作出反应,但它的父widget仍然可以是无状态的,如果它不需要更改作出反应。
下面的例子使用了一个StatelessWidget.一个常见的StatelessWidgetText.如果你去看Text的实现你会发现它是Statelesswidget的子类。

Text(
  'I like Flutter!',
  style: TextStyle(fontWeight: FontWeight.bold),
);

如你所见,Text没有与之关联的状态,它只呈现在其构造函数中传递的内容。
但是,如果您想要动态地更改“I Like Flutter”,例如在单击FloatingActionButton时?
要实现这一点,请将Text包装在一个有StatefulWidget中,并在用户单击按钮时更新它。
例如:

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> {
  // Default placeholder text
  String textToShow = "I Like Flutter";

  void _updateText() {
    setState(() {
      // update the text
      textToShow = "Flutter is Awesome!";
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: Center(child: Text(textToShow)),
      floatingActionButton: FloatingActionButton(
        onPressed: _updateText,
        tooltip: 'Update Text',
        child: Icon(Icons.update),
      ),
    );
  }
}

怎么布局以及XML在哪里

在Android中,使用XML编写布局,但在Flutter中,使用控件树编写布局。
以下示例显示如何使用一个padding部件:

@override
Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: Text("Sample App"),
    ),
    body: Center(
      child: MaterialButton(
        onPressed: () {},
        child: Text('Hello'),
        padding: EdgeInsets.only(left: 10.0, right: 10.0),
      ),
    ),
  );
}

你可以查看Flutter用作布局的控件类型:widget catalog

怎么添加或者删除一个布局中的控件

在Android中,可以对父布局调用addChild()removeChild()来动态添加或删除子布局。在Flutter中,因为控件是不可变的,所以没有与addChild()直接等价的方式。相反,可以将一个函数传递给返回小部件的父控件,并用一个boolean控制子控件的创建。
例如这里,你可以点击FloatingActionButton来切换两个控件:

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> {
  // Default value for toggle
  bool toggle = true;
  void _toggle() {
    setState(() {
      toggle = !toggle;
    });
  }

  _getToggleChild() {
    if (toggle) {
      return Text('Toggle One');
    } else {
      return MaterialButton(onPressed: () {}, child: Text('Toggle Two'));
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Sample App"),
      ),
      body: Center(
        child: _getToggleChild(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _toggle,
        tooltip: 'Update Text',
        child: Icon(Icons.update),
      ),
    );
  }
}

怎么给控件添加动画呢

在Android中,可以使用XML创建动画,也可以在View上调用animate()方法。在Flutter中,实现动画的方式是使用animated library中的animated widget来包装控件。
在Flutter中,使用AnimationController它是一个Animation<double>,可以实现暂停、搜索、停止、反转的动画。它需要一个Ticker作为vsync的信号发生器,并且在每一帧发出一个0-1之间的线性插值。你可以创建多个Animation使用同一个AnimationController
你可以使用CurvedAnimation去实现插值曲线的动画,从这个意义上来说,控制器是实现动画变换的主要部分,而使用CurvedAnimation可以实现曲线插值替换默认的线性运动,就像Widget一样,Flutter的动画也可以互相组合。
当创建控件树的时候可以将Animation赋给控件的动画属性,例如实现透明的FadeTransition,同时告诉控制器开始动画。
下面这个例子使用FadeTransition是实现了渐进显示Logo的一个效果:

import 'package:flutter/material.dart';

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

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

class MyFadeTest extends StatefulWidget {
  MyFadeTest({Key key, this.title}) : super(key: key);
  final String title;
  @override
  _MyFadeTest createState() => _MyFadeTest();
}

class _MyFadeTest extends State<MyFadeTest> with TickerProviderStateMixin {
  AnimationController controller;
  CurvedAnimation curve;

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
          child: Container(
              child: FadeTransition(
                  opacity: curve,
                  child: FlutterLogo(
                    size: 100.0,
                  )))),
      floatingActionButton: FloatingActionButton(
        tooltip: 'Fade',
        child: Icon(Icons.brush),
        onPressed: () {
          controller.forward();
        },
      ),
    );
  }
}

更多信息, 参考 Animation & Motion widgets, Animations tutorial, 和 Animations overview.

怎么使用Canvas来绘制?

在Android中,您可以使用CanvasDrawable在屏幕上绘制图像和形状。Flutter也有类似的Canvas API,因为它基于相同的底层渲染引擎,Skia。因此,对于Android开发人员来说,在Flutter中绘制到Canvas是一项非常熟悉的工作。
Flutter有两个类来帮助你绘制Canvas:CustomPaintCustomPainter,后者可以实现你的算法去绘制Canvas.
想了解如何使用Flutter实现一个签名绘画区,可以参考Collin的回答: StackOverflow.
这里需要注意的是Collin原回答不能实现绘画,需要使用下面的代码需要替换掉源码中这一行,修改CustomPaint的构造参数

child: CustomPaint(painter: SignaturePainter(_points), size: Size.infinite),

import 'package:flutter/material.dart';

class SignaturePainter extends CustomPainter {
  SignaturePainter(this.points);

  final List<Offset> points;

  void paint(Canvas canvas, Size size) {
    Paint paint = new Paint()
      ..color = Colors.black
      ..strokeCap = StrokeCap.round
      ..strokeWidth = 5.0;
    for (int i = 0; i < points.length - 1; i++) {
      if (points[i] != null && points[i + 1] != null)
        canvas.drawLine(points[i], points[i + 1], paint);
    }
  }

  bool shouldRepaint(SignaturePainter other) => other.points != points;
}

class Signature extends StatefulWidget {
  SignatureState createState() => new SignatureState();
}

class SignatureState extends State<Signature> {
  List<Offset> _points = <Offset>[];

  Widget build(BuildContext context) {
    return new Stack(
      children: [
        GestureDetector(
          onPanUpdate: (DragUpdateDetails details) {
            RenderBox referenceBox = context.findRenderObject();
            Offset localPosition =
            referenceBox.globalToLocal(details.globalPosition);

            setState(() {
              _points = new List.from(_points)..add(localPosition);
            });
          },
          onPanEnd: (DragEndDetails details) => _points.add(null),
        ),
        CustomPaint(painter: new SignaturePainter(_points)),
      ],
    );
  }
}

class DemoApp extends StatelessWidget {
  Widget build(BuildContext context) => new Scaffold(body: new Signature());
}

void main() => runApp(new MaterialApp(home: new DemoApp()));

怎么创建自定义控件

在Android中,通过继承于View或者继承于一个已经存在的View,通过覆盖或者实现接口来完成期望的效果。
在Flutter中,通过组合更小控件(而不是继承)来构建一个自定义的控件。这有点类似于在Android中实现自定义ViewGroup,其中所有构建块都已经存在,但是你提供了不同的行为—例如,自定义布局逻辑。
例如,如何构建一个带有标签的CustomButton呢,通过组合RaisedButton包含一个lablel更好,而不是通过继承RaisedButton

class CustomButton extends StatelessWidget {
  final String label;

  CustomButton(this.label);

  @override
  Widget build(BuildContext context) {
    return RaisedButton(onPressed: () {}, child: Text(label));
  }
}

然后可以使用CustomButton就像你使用其他的Flutter的控件一样:

@override
Widget build(BuildContext context) {
  return Center(
    child: CustomButton("Hello"),
  );
}

Intents

Flutter中与Intent相对应的是什么?

Android中,Intent有两个主要用途:用于activity间的跳转、用于组件间的通信。而在Flutter中,没有Intent这个概念,虽然你依然可以通过本地集成(native integrations(使用插件))来启动Intent
Route可以看做是应用屏幕或者页面的抽象,而Navigator是一个管理Route的控件。可以粗略的将Route看成Activity,但是它们含义不同。Navigator通过pushpop操作Route在页面之间切换,Navigator就像是一个栈,push表示切换到新的页面,pop表示返回。
Android中,需要在AndroidManifest.xml中声明activity,而在Flutter中,你有以下页面切换选择:

  • 指定一个包含所有Route名字的Map(MaterialApp)
  • 直接切换到Route(WidgetApp)
    下面的实例里建立了一个Map
void main() {
  runApp(MaterialApp(
    home: MyAppHome(), // becomes the route named '/'
    routes: <String, WidgetBuilder> {
      '/a': (BuildContext context) => MyPage(title: 'page A'),
      '/b': (BuildContext context) => MyPage(title: 'page B'),
      '/c': (BuildContext context) => MyPage(title: 'page C'),
    },
  ));
}

通过压入对应的名字到Navigator来切换到对应的Route:

Navigator.of(context).pushNamed('/b');

另一种Intent常见的使用场景是调用外部组件比如相机、文件选择器,对于这种情况你需要创建一个原生平台集成(或者使用现成插件 existing plugin
关于构建原生平台集成,请查看 Developing Packages and Plugins.

Flutter 如何处理来自外部的Intent?

Flutter可以通过直接访问Android layer来处理来自Android的Intent,或者请求共享数据。
下面的实例注册了一个文本共享的Intent过滤器在运行我们Flutter代码的原生activity上,所以其他应用就能共享文本给我们的Flutter 应用。
基本流程就是先在Android 原生层(即Activity)先处理这些共享数据,然后等待Flutter请求,通过MethodChannel可以将这些数据提供给Flutter.
首先,在AndroidManifest.xml中注册Intent过滤器:

<activity
  android:name=".MainActivity"
  android:launchMode="singleTop"
  android:theme="@style/LaunchTheme"
  android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
  android:hardwareAccelerated="true"
  android:windowSoftInputMode="adjustResize">
  <!-- ... -->
  <intent-filter>
    <action android:name="android.intent.action.SEND" />
    <category android:name="android.intent.category.DEFAULT" />
    <data android:mimeType="text/plain" />
  </intent-filter>
</activity>

接着在MainActivity中处理Intent,从Intent中获取共享的数据,然后先暂时持有这些数据,当Flutter准备好处理时,它会通过平台通道(platform channel)进行请求,然后从原生Android层发送这些数据。

package com.example.shared;

import android.content.Intent;
import android.os.Bundle;

import java.nio.ByteBuffer;

import io.flutter.app.FlutterActivity;
import io.flutter.plugin.common.ActivityLifecycleListener;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugins.GeneratedPluginRegistrant;

public class MainActivity extends FlutterActivity {

  private String sharedText;

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    GeneratedPluginRegistrant.registerWith(this);
    Intent intent = getIntent();
    String action = intent.getAction();
    String type = intent.getType();

    if (Intent.ACTION_SEND.equals(action) && type != null) {
      if ("text/plain".equals(type)) {
        handleSendText(intent); // Handle text being sent
      }
    }

    new MethodChannel(getFlutterView(), "app.channel.shared.data").setMethodCallHandler(
      new MethodCallHandler() {
        @Override
        public void onMethodCall(MethodCall call, MethodChannel.Result result) {
          if (call.method.contentEquals("getSharedText")) {
            result.success(sharedText);
            sharedText = null;
          }
        }
      });
  }

  void handleSendText(Intent intent) {
    sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
  }
}

最后,当 Flutter的控件渲染完成时请求数据:

import 'package:flutter/material.dart';
import 'package:flutter/services.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 Shared App Handler',
      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> {
  static const platform = const MethodChannel('app.channel.shared.data');
  String dataShared = "No data";

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(body: Center(child: Text(dataShared)));
  }

  getSharedText() async {
    var sharedData = await platform.invokeMethod("getSharedText");
    if (sharedData != null) {
      setState(() {
        dataShared = sharedData;
      });
    }
  }
}

startActivityForResult()等价的是什么?

Flutter中Navigator除了用于处理Route,也可以获得已入栈Route的返回结果。只需要等push()返回的Future执行完即可。

Map coordinates = await Navigator.of(context).pushNamed('/location');

然后,在定位Route里,当用户选择完位置后,你就可以通过pop获取结果了:

Navigator.of(context).pop({"lat":43.821757,"long":-79.226392});

异步UI

runOnUiThread 在Flutter中等价于什么

Dart是单线程执行模型,支持Isolates(在另一个线程上运行Dart代码的方式)、事件循环和异步编程。 除非您启动一个Isolate,否则您的Dart代码将在主UI线程中运行,并由事件循环驱动(和JavaScript一样)。Flutter的事件驱动循环等价于Android的主线程/UI线程的Looper。
Dart虽然是单线程模型但是并不意味着你需要用一种阻塞式的方式来执行其他所有的代码造成UI线程被冻结。不像Android,你需要保持主线程一直空闲不被阻塞,在Flutter中,你可以使用Dart语言提供的异步工具,如async/await来执行异步操作。如果你用过C#,Javascript,或者你使用过Kotlin的协程的话,你可能会熟悉async/await这个范例。
例如,你可以使用async/await来执行网络请求代码同时不会引起UI线程挂起,让Dart完成繁重的操作:

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

一旦等到网络请求结束,就会调用setState()方法以更新UI,接着触发控件子树的重建并更新数据。
下面的示例描述了如何异步加载数据,然后填充到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);
    });
  }
}

上述代码需要引入http.dart(下文中也有提到,这里是为了方便顺序查看本文时验证demo所以先提到),可以访问http 0.12.0+2(当前版本)查看对应版本号在pubspec.yaml中引入:

dependencies:
  http: ^0.12.0+2

如何将任务放入后台线程?

在Android中,访问网络资源的一种典型方式就是在后台线程中去执行任务,从而避免在主线程中执行造成ANR。例如,你可以使用AsyncTask, LiveData, IntentService, JobScheduler或者RxJava Scheduler进行后台处理。
由于Flutter是一个单线程模型运行着一个事件循环(像Node.js一样),你不需要替线程管理和创建后台线程而操心。如果你进行I/O操作,例如硬盘访问或者网络请求,你可以简单的使用async/await来完成所有的操作,反过来说,如果你需要执行计算密集型的任务,捏可以把它放入一个Isolate中从而避免阻塞事件循环,就像Android中,你不会在主线程执行任何除UI更新相关的其他任何类型的任务。
I/O类的任务,只需要声明函数是一个async函数,然后在函数中,用await修饰在耗时任务之前:

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

这就是网络请求、数据库操作等的典型做法,它们都是I/O操作。
在Android中,当你继承AsyncTask,那么通常你需要重写三个方法,onPreExecute()doInBackground()onPostExecute(),而在Flutter中则没有与之等价的方式,因为await修饰的耗时任务函数,剩余的工作都交给Dart的事件循环去处理。
然而当你处理大量数据时,你的UI会挂起,所以在Flutter中使用Isolate来充分利用多核CPU去执行耗时的计算密集型任务。
Isolate是独立的执行线程,它不会与主线程共享内存堆,这就意味着你不能在Isolate中直接访问主线程的变量,或者调用setState更新UI。不像Android中的线程,Isolate是名副其实的隔离区,不能共享内存(比如不能以静态字段的方式共享等)。
下面示例展示了一个简单的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执行在它拥有的独立执行线程中。在Isolate中,可以执行CPU密集型任务(比如解析超大的Json数据),或者执行计算密集型的数学运算,比如加密或者信号处理等。
你可以跑一下下面这个完整的示例:

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;
  }
}

在Flutter中与OkHttp等价的是什么?

在Flutter中使用流行的 httppackage调用一个网络请求是很简单的。
虽然在http包中没有OkHttp所有的功能,它抽象了大多你通常需要自己实现的网络功能,使得网络请求更加简单。
要使用http包,需要在pubspec.yaml中添加如下依赖:

dependencies:
  ...
  http: ^0.11.3+16

发起一个网络请求可以使用await调用一个async函数例如http.get():

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);
    });
  }
}

如何显示耗时任务的进度?

在Android中,当在后台线程中执行一个耗时任务时,通常显示一个ProgressBar在UI上。
在Flutter中,则是使用ProgressIndicator控件。通过boolean标记位来控制何时开始渲染,然后在耗时任务开始之前显示它,并在任务结束时隐藏掉。
下面示例中,build函数分割成了三个不同的子函数,如果showLoadingDialog()返回true(当widgets.length == 0),则渲染ProgressIndicator,否则就将网络请求返回的数据渲染到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();
  }

  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);
    });
  }
}

项目结构&资源文件

在哪里存放不同分辨率的图片文件?

Android中,resourcesassets是两个独立的文件夹,而在Flutter中,只有assets,所有放在Androidres/drawable-*文件夹中的文件全都放在Flutter中的assets文件夹中。

Flutterios一样遵循简单的基于密度(density-base)的格式,assets包含1.0x2.0x3.0x或者更高乘数,Flutter中并没有dp这一说,而是使用与设备无关的逻辑像素,在devicePixelRatio 中描述了单个逻辑像素与物理像素的比例。

对应于Android密度清单如下:

Android density qualifier Flutter pixel ratio
ldpi 0.75x
mdpi 1.0x
hdpi 1.5x
xhdpi 2.0x
xxhdpi 3.0x
xxxhdpi 4.0x

Assets可以存在任意的文件夹,Flutter没有预定义的文件夹结构。需要在pubspec.yaml声明assets对应的物理位置,Flutter就能正确的读取到。
注意,在Flutter 1.0 beta 2之前,Flutter定义的assets无法被原生端访问,反之亦然,原生定义的assetsresources也不能被Flutter访问,因为他们位于不同的文件夹中。
而从Flutter 1.0 beta2开始,assets存储于本地层的assets文件夹中,且可以被本地层通过AssetManager访问,但是beta2版本中,Flutter依然不能访问本地层的resourcesassets

val flutterAssetStream = assetManager.open("flutter_assets/assets/my_flutter_asset.png")

例如,要将名为my_icon.png的新图像资源添加到我们的Flutter项目中,比方说,把它放到一个我们任意命名为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

接下来你需要去声明这些图片在pubspec.yaml文件中:

assets:
 - images/my_icon.jpeg

你可以用AssetImage去访问这些图片:

return AssetImage("images/a_dot_burr.jpeg");

或者直接可以在Image控件中写入路径:

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

在哪里存放strings字符串资源,怎么实现本地化?

目前Flutter没有像strings这样的专用资源系统,所以目前来说,最佳的方法就是把你的字符串以static字段的方式存放在一个类中,然后从这个类去访问它们。如下示例:

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

然后在代码中你可以这样调用你的字符串:

Text(Strings.welcomeMessage)

Flutter对Android上的辅助功能提供了基本支持,目前工作正在进行中。官方鼓励Flutter开发者使用intl package来实现国际化和本地化。

与Gradle文件等价的是什么?该怎么添加依赖?

在Android中,使用Gradle构建脚本添加依赖。在Flutter中,使用Dart自己的构建系统和Pub包管理器。该工具会把原生的Android和IOS的包装应用的构建过程委托给各自的构建系统。
gradle文件在Flutter工程目录的android文件夹下,只有需要针对单个平台添加本地依赖时才使用gradle,通常来说在Flutter中直接在pubspec.yaml声明外部依赖即可。找Flutter包的好地方就是 Pub.

Activities和fragments

Flutter中与activitiesfragments相对应的是啥?

在Android中,activity是用户可以操作的一个焦点物,而Fragment则代表着一个行为表现或者是UI的一部分。Fragment可以模块化你的代码,可以给大屏设备组合出复杂的UI部分,帮助你拓展你的应用UI.在Flutter中,这两个概念都落在了Widget的范畴里。
要了解有关使用ActivityFragment构建UI的更多信息,可以看社区里的文章,Flutter For Android Developers : How to design an Activity UI in Flutter.
正如在Intent部分所提到的,在Flutter中,Widget就代表着屏幕,因为在Flutter中万物皆Widget。我们使用Navigator来切换Route,这代表着不同屏幕或页面,亦或是不同状态或是渲染相同的数据。

如何监听Android中activity的生命周期事件

在Android中, 通过重写Activity中的方法来捕获生命周期函数的回调,或者实现ActivityLifecycleCallbacksApplication上。而在Flutter中,没有这个概念,但你可以通过hookWidgetsBinding并且监听didChangeAppLifecycleState()的变化事件来代替监听生命周期。
可观察的生命周期事件有:

  • inactive — 应用处于非活动状态,不接受用户输入。这个事件只在IOS中有效,因为在Android中没有与此等价的事件。
  • paused — 当前应用对用户不可见,不响应用户输入,且运行在后台。等同于Android中的onPause。(如果等同于Android中onPause的话这里应该是可见但是不在顶层可见)
  • resumed — 此时应用可见且响应用户输入。等同于Android中的onPostResume()
  • suspending — 此时应用暂时挂起。等同于Android中的onStop;不会触发IOS上的事件,IOS中没有等价的事件。
    想要了解更多关于这些状态的信息,可以参见 AppLifecycleStatus documentation.
    正如你注意到的,只有少数的Activity生命周期可用,而FlutterActivity确实在内部捕获了几乎所有的Activity生命周期事件然后把它们发送到Flutter的引擎中去,然而很多事件都替你屏蔽了。Flutter会替你管理引擎的启动和关闭,所以大多数情况下我们几乎不需要去观察activity的生命周期在Flutter中。如果你一定要在生命周期里获取或者释放本地资源,你至少可以在原生端做这些事。
    以下示例描述了如何监听Activity中的生命周期事件:
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等价的是什么

在Android中,LinearLayout用于横向和纵向布局控件,而在Flutter中则是使用RowColumn控件来实现相同的行为。
你可能注意到两段示例代码实现相同,仅仅RowColumn控件不同。子控件是完全相同的,这样的特性可以被用来开发具有相同子控件但是随着时间推移会变化的富布局。

@override
Widget build(BuildContext context) {
  return Row(
    mainAxisAlignment: MainAxisAlignment.center,
    children: <Widget>[
      Text('Row One'),
      Text('Row Two'),
      Text('Row Three'),
      Text('Row Four'),
    ],
  );
}
@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等价的是什么

使用相对布局可以将你的子控件按照彼此的位置相互放置。在Flutter中,有很多方法可以达到同样的效果。
你可以组合Column,RowStack控件来实现与相对布局一样的效果。你可以在widgets的构造函数中指定子控件相对于父布局的规则。
有关在FLutter中构建RelativeLayout的好例子,可以参考Collin's的回答: StackOverflow.

ScrollView等价的是什么

在Android中,当你的内容超出设备屏幕时,使用ScrollView来布局。
在Flutter中,最简单的实现方式就是使用ListView控件。这与Android看起来仿佛有点矫枉过正,但是在Flutter中一个ListView即是Android中的ScrollView也是ListView

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

在Flutter中如何处理横屏反转

只要在AndroidManifest.xml中包含下列内容FlutterView就会处理这些配置变化:

android:configChanges="orientation|screenSize"

手势检测和触摸事件处理

Flutter中怎么给一个空间添加点击事件

在Android中你可以通过setOnClickListener来给一个View,例如Button添加点击事件。
在Flutter中,有下面两种方式添加触摸事件监听:

  1. 如果控件支持事件检测,那可以给它传入一个函数去处理这个事件。比如,RaisedButton包含一个onPressd参数:
@override
Widget build(BuildContext context) {
  return RaisedButton(
      onPressed: () {
        print("click");
      },
      child: Text("Button"));
}
  1. 如果空间不支持事件检测,可以把它包装在GestureDetector中通过onTap参数传递一个监听函数。
class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
      child: GestureDetector(
        child: FlutterLogo(
          size: 200.0,
        ),
        onTap: () {
          print("tap");
        },
      ),
    ));
  }
}

如何处理控件的其他手势?

使用GestureDetector可以监听广泛的手势,例如:

  • 列表第一项
    • 列表第二项
      • 列表第二项
  • 单击(Tab)
    • onTabDown: 触发单击事件的指针已经与屏幕上特定点联系在一起
    • onTapUp: 触发单击事件的指针停止与屏幕在特定点上的联系
    • onTap: 单击事件发生
    • onTapCancel: 触发时会导致之前触发onTabDown的指针无法形成单击事件
  • 双击(Double Tab)
    • onDoubleTab: 用户在屏幕的同一个点上连续快速点击了两次
  • 长按Long Press
    • onLongPress: 指针和屏幕上同一个位置发生一段较长事件的联系
  • 垂直拖动(Vertical drag)
    • onVerticalDragStart: 指针已经和屏幕联系,并且可能开始垂直移动。
    • onVerticalDragUpdate: 正在和屏幕联系的指针已经开始在垂直方向上进行移动。
    • onVerticalDragEnd: 之前与屏幕进行联系且在垂直方向移动的指针,现在已经不需要再与屏幕联系了,并且在停止联系的瞬间,指针依然以一定的速度移动。
  • 水平拖动(Horizontal drag)
    • onHorizontalDragStart: 指针已经和屏幕联系,并且可能开始水平移动。
    • onHorizontalDragUpdate: 正在和屏幕联系的指针已经开始在水平方向上进行移动。
    • onHorizontalDragEnd: 之前与屏幕进行联系且在水平方向移动的指针,现在已经不需要再与屏幕联系了,并且在停止联系的瞬间,指针依然以一定的速度移动。

下面示例展示了使用GestureDetector监听双击Flutter logo时,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

在Flutter中替代ListView的是什么

在Flutter中与之对应的就是ListView
在Android中,ListView使用一个适配器模式,每条数据都由你的适配器来渲染返回。然而你必须确保每条数据都被你回收,否则你可能会遇到各种显示错乱和内存问题。
而在Flutter中控件不可变,你提供一个控件的列表给你的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;
  }
}

如何知道哪一条列表项被点击

在Android中,onItemClickListener会找出哪一条列表项被点击。在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(GestureDetector(
        child: Padding(
            padding: EdgeInsets.all(10.0),
            child: Text("Row $i")),
        onTap: () {
          print('row tapped');
        },
      ));
    }
    return widgets;
  }
}

如何动态更新ListView

在Android中是通过适配器调用notifyDataSetChanged来更新。
而在Flutter中,如果你是在setState()方法中更新控件列表,那么你很快会发现你的数据没有更新视图,这是因为当setState()被调用时,Flutter渲染引擎会在控件树中搜索是否存在发生改变的东西,而当它找到了你的ListView,它会进行==判断,然后判定这两个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<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来建立ListView,这种方式在你的数据量巨大或者需要动态改变数据的情况下非常有用,这实质上等价于Android中的RecyclerView,因为它会自动循环复用列表元素:

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');
        });
      },
    );
  }
}

与创建ListView不同的是,创建ListView.Builder需要传入两个命名参数:初始列表的长度和ItemBuilder函数。
ItemBuilder与Android适配器中的getView()方法类似,它给你一个位置,然后返回你想要在对应位置上渲染的行。
最后,且最重要的一点是, onTab函数已经不需要在重新创建数据列表了,取而代之的是通过.add()来添加到控件列表中。

文字处理

如何给文字控件设置自定义字体

在Android SDK(Android O),可以创建一个Font资源,然后作为FontFamily参数传入TextView。而在Flutter中,则是将字体文件放到一个文件夹中,然后在pubspec.yaml中引用即可,跟引入图片是类似的。

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

然后给你的Text指定字体:

@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控件自定义风格

除了字体,我们还可以定义Text控件的其他风格属性,Text控件的风格参数中包含一个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"等价的是什么

在Flutter中,可以通过给Text控件传入一个InputDecoration对象来显示“hint”或者占位字符,如下示例:

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

如何展示验证错误?

和显示hint一样,传一个InputDecoration对象给Text控件的构造函数即可。

然而,你肯定不想一开始就显示错误,而是当用户键入一个非法值才显示,并传入一个新的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);
  }
}

Flutter Plugins

如何访问GPS传感器

使用 geolocator社区插件。

如何访问相机

流行的方式是使用 image_picker插件。

如何登录Facebook

flutter_facebook_login插件可以用来登录到FaceBook.

如何使用Firebase

大部分Firebase函数转换自 first party plugins,以下是第一批集成的插件,由Flutter团队维护:

如何构建自定义的 native integrations(本地集成)

如果没有Flutter或者社区插件没有提供那些平台特定的功能,你可以构建自己的,参考developing packages and plugins页面。
Flutter的插件架构,简而言之,类似于在Android中使用EventBus:发出消息给接收器处理,接收器处理完成后将结果发射回给你。在这里,接收器代码运行在本地层,也就是AndroidIOS.

如何在Flutter应用中使用NDK

如果你已经在Android应用中使用了NDK,并且想要让Flutter也能利用到这些类库,那么就需要构建自定义插件了。
你的自定义插件先和Android应用交互,即在Android应用中通过JNI调用native函数,当响应准备好时,就发送给Flutter,然后渲染结果。
目前不支持直接从Flutter中调用native代码。

主题

如何给应用定制主题

直接使用的情况下(Out of the box),Flutter自带漂亮的Material Design实现,它可以满足你通常所需的大量样式和主题需求。不像Android在XML文件中声明主题,然后在AndroidManifest.xml中使用。在Flutter中是在顶层控件中声明主题的。
为了能充分利用Material组件,你可以声明一个顶层控件MaterialApp作为应用的入口。MaterialApp是一个很便携的控件,他包装了实现了Material Design且应用需要的控件。他建立在WidgetsApp之上,添加了Material特定的功能。
同样也可以使用WidgetsApp作为应用控件,它提供了一些相同的功能,但不如MaterialApp丰富。
想要在任意子组件上自定义颜色和风格的话,那么给MaterialApp传入一个ThemeData对象,例如,在下面代码中,初始样本设置为蓝色,而文字选择后显示为红色。

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

数据库与本地存储

怎样访问Share Preferences

在Android中,你可以使用SharePreferences来存储一些小的键值对。
在Flutter中,使用Shared_Preferences plugin来访问这一功能。这个插件包装了Shared PreferencesNSUserDefaults(IOS中等价的功能)两个功能。

import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() {
  runApp(
    MaterialApp(
      home: Scaffold(
        body: Center(
          child: RaisedButton(
            onPressed: _incrementCounter,
            child: Text('Increment Counter'),
          ),
        ),
      ),
    ),
  );
}

_incrementCounter() async {
  SharedPreferences prefs = await SharedPreferences.getInstance();
  int counter = (prefs.getInt('counter') ?? 0) + 1;
  print('Pressed $counter times.');
  prefs.setInt('counter', counter);
}

如何访问SQLite数据库

在Flutter中使用SQFlite插件实现此功能。

通知

如何推送通知

(国内环境中firebase基本也没有用)
Flutter中,通过Firebase_Messaging插件可以使用该功能,更多关于使用Firebase Cloud Messaging API的信息,请查看firebase_messaging插件文档。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容