给 Xamarin.Forms 开发者的 Flutter 指南

本文档旨在帮助 Xamarin.Forms 开发者利用已有的知识去构建 Flutter 移动应用。如果你懂得 Xamarin.Forms 框架的基本原理,那么你就可以将本文档当作你开始 Flutter 开发的不错的起点。

你的 Android 和 iOS 知识以及技能组合在构建 Flutter 时都是有价值的,因为 Flutter 依赖的原生系统配置都与你配置 Xamarin.Forms 原生项目时一样。Flutter 框架与你创建一个单独的界面时也是一样的,这在多个平台中同样适用。

本文档可用做可指导手册来翻查与你需求最为相关的问题。

项目设置

app 是如何运行的?

对于 Xamarin.Forms 里的每个平台,你可以调用 LoadApplication 方法,创建一个新应用并运行你的 app 。

  1. LoadApplication(new App());

在 Flutter 中,加载 Flutter app 的默认主入口点是 main

  1. void main() {
  2. runApp(new MyApp());
  3. }

在 Xamarin.Forms 中,你分配一个 PageApplication 类中的 MainPage 属性。

  1. public class App: Application
  2. {
  3. public App()
  4. {
  5. MainPage = new ContentPage()
  6. {
  7. new Label()
  8. {
  9. Text="Hello World",
  10. HorizontalOptions = LayoutOptions.Center,
  11. VerticalOptions = LayoutOptions.Center
  12. }
  13. };
  14. }
  15. }

在 Flutter 中,“万物皆 widget”,甚至连应用本身也是。接下来的示例展示了 MyApp ,一个简单的应用 Widget

  1. class MyApp extends StatelessWidget {
  2. // This widget is the root of your application.
  3. @override
  4. Widget build(BuildContext context) {
  5. return new Center(
  6. child: Text("Hello World!", textDirection: TextDirection.ltr));
  7. }
  8. }

如何创建一个页面?

Xamarin.Forms 拥有一些不同类型的页面;ContentPage 是最为通用的。在 Flutter 中,指定一个应用程序 widget 来控制你的根页面。你可以使用一个MaterialApp widget,它支持 Material Design,或者你也可以使用 CupertinoApp widget,它能用来创建 ios 风格的应用,或者你也可以使用等级较低的WidgetsApp,可供你随心所欲地定制。

接下来的代码定义了一个主页,一个有状态的 widget。在 Flutter 中,除了以下两个类型的 widget 外,其它 widget 都是不可变的:有状态和无状态 widget。无状态 widget 的示例都是标题、图标或图片。

下面的示例使用 MaterialApp,它在 home 属性中控制它的根页面。

  1. class MyApp extends StatelessWidget {
  2. // This widget is the root of your application(这个 widget 是你的应用程序的根 widget)。
  3.  
  4. @override
  5. Widget build(BuildContext context) {
  6. return new MaterialApp(
  7. title: 'Flutter Demo',
  8. theme: new ThemeData(
  9. primarySwatch: Colors.blue,
  10. ),
  11. home: new MyHomePage(title: 'Flutter Demo Home Page'),
  12. );
  13. }
  14. }

从这里开始,真正的首页是另一个你在里面创建了状态的 widget

一个有状态 widget,例如下面的 MyHomePage,包含两个部分。第一部分,是它自身不变的,创建一个状态对象(State object)来管控对象的状态。状态对象持续存在于 widget 的整个生命周期中。

  1. class MyHomePage extends StatefulWidget {
  2. MyHomePage({Key key, this.title}) : super(key: key);
  3.  
  4. final String title;
  5.  
  6. @override
  7. _MyHomePageState createState() => new _MyHomePageState();
  8. }

状态对象实现了有状态 widget 中的构建方法。

当 widget 树的状态发生了改变,将会调用 setState() 触发 widget 当中该部分UI的构建。确保只在需要时调用 setState() ,并且在只有部分 widget 树发生变化时调用,否则会造成糟糕的UI性能表现。

  1. class _MyHomePageState extends State<MyHomePage> {
  2. int _counter = 0;
  3.  
  4. void _incrementCounter() {
  5. setState(() {
  6. _counter++;
  7. });
  8. }
  9.  
  10. @override
  11. Widget build(BuildContext context) {
  12. return new Scaffold(
  13. appBar: new AppBar(
  14. // Take the value from the MyHomePage object that was created by
  15. // the App.build method, and use it to set the appbar title.
  16. title: new Text(widget.title),
  17. ),
  18. body: new Center(
  19. // Center is a layout widget. It takes a single child and positions it
  20. // in the middle of the parent.
  21. child: new Column(
  22. mainAxisAlignment: MainAxisAlignment.center,
  23. children: <Widget>[
  24. new Text(
  25. 'You have pushed the button this many times:',
  26. ),
  27. new Text(
  28. '$_counter',
  29. style: Theme.of(context).textTheme.display1,
  30. ),
  31. ],
  32. ),
  33. ),
  34. floatingActionButton: new FloatingActionButton(
  35. onPressed: _incrementCounter,
  36. tooltip: 'Increment',
  37. child: new Icon(Icons.add),
  38. ),
  39. );
  40. }
  41. }

在 Flutter 中的UI(也就是这里所说的 widget 树)是不可变的,意思是说它一旦被构建,你就无法再改变他的状态。当你修改状态 类中的字段,就要再次调用 setState 来重新构建整个 widget 树。

这个生成UI的方式不同于 Xamarin.Forms,但是这种方法却有很多益处。

视图

在 Flutter 中页面(Page)与元素(Element)的相同的是什么?

类 react 的风格,或者说是声明式编程风格,与传统的命令式编程风格有何不同?作为比较,可以参考 声明式 UI 介绍

一个 ContentPageTabbedPageMasterDetailPage就是你可以在 Xamarin.Forms 应用程序中使用的全部页面类型。这些页面会控制元素(Element)来显示各种控件。在 Xamarin.Forms 中,Entry 或者 Button 就是一个 元素 的示例。

在 Flutter 中,几乎所有东西都是 widget,一个页面在 Flutter 中被称作路由(Route),也是一个 widget。按钮、进度条、动画控制器都是 widget。当构建一个路由时,就会创建一棵 widget 树。

Flutter 包含 Material 组件 库。这些都是实现了 Material Design 指南 的 widget。Material Design 是一个灵活的 针对所有平台 的设计系统,包括 iOS。

不过, Flutter 有足够灵活和自描述性(expressive)去实现任何设计语言。举个例子,在 iOS 上,你可以用 Cupertino widget来生成一个看起来像 苹果 iOS 设计语言 的接口。

如何更新 widget?

在 Xamarin.Forms 中,每一个页面或者元素都是一个有状态的类,拥有一些属性和方法。通过更新一个属性来更新你的元素,而且这会传递到原生控件。

在 Flutter 中,widget是不可变的,你不可以直接地通过修改一个属性来更新它们,而是应该使用 widget 的状态。

有状态 widget 和无状态 widget 的概念就是出自这里,无状态 widget(StatelessWidget)顾名思义,就是一个没有状态信息的 widget。

当你在描绘用户界面的一个不依赖除对象中的配置信息之外任何东西的部分时,StatelessWidgets 是有用的。

举个例子,在 Xamarin.Forms 中,可以轻而易举地用你的logo替换一张图片。这个logo将不会在运行过程中修改,所以在 Flutter 会使用StatelessWidget

如果你想动态地基于进行了 HTTP 调用或者用户交互后接收到的数据来修改 UI,你需要使用 StatefulWidget 并告诉 Flutter 框架这个 widget 的 状态(State)已经被更新了所以它可以更新那个 widget。

这里要记下的重要内容是有状态和无状态 widget 的核心行为都是一样的。他们重建每个结构,不同的是StatefulWidget拥有一个状态(State)对象来跨结构储存状态数据和恢复它。

如果你有疑惑,那么就记住这个规则:如果一个 widget 改变了(例如是因为用户交互),它就是有状态的。相反,如果一个 widget 对修改作出反应,包含它的父 widget 如果本身没有对修改作出反应,仍然可以是无状态的。

接下来的示例展示了如何使用一个StatelessWidget。一个公共的StatelessWidgetText widget。如果你查阅 Text widget 的实现,你会发现他是 StatelessWidget 的子类。

  1. new Text(
  2. 'I like Flutter!',
  3. style: new TextStyle(fontWeight: FontWeight.bold),
  4. );

如你所见,文本 widget 没有状态信息与它关联,它只渲染在它的构造函数中呈现的内容,没有更多。

但是,如果你想动态地作出 “I Like Flutter”的修改呢?例如在点击一个FloatingActionButton时。

为了实现这个目标,需要将 Text widget 封装到一个StatefulWidget中,并在用用户点击按钮时更新它,正如接下来的例子:

  1. import 'package:flutter/material.dart';
  2.  
  3. void main() {
  4. runApp(new SampleApp());
  5. }
  6.  
  7. class SampleApp extends StatelessWidget {
  8. // This widget is the root of your application.
  9. @override
  10. Widget build(BuildContext context) {
  11. return new MaterialApp(
  12. title: 'Sample App',
  13. theme: new ThemeData(
  14. primarySwatch: Colors.blue,
  15. ),
  16. home: new SampleAppPage(),
  17. );
  18. }
  19. }
  20.  
  21. class SampleAppPage extends StatefulWidget {
  22. SampleAppPage({Key key}) : super(key: key);
  23.  
  24. @override
  25. _SampleAppPageState createState() => new _SampleAppPageState();
  26. }
  27.  
  28. class _SampleAppPageState extends State<SampleAppPage> {
  29. // Default placeholder text
  30. String textToShow = "I Like Flutter";
  31.  
  32. void _updateText() {
  33. setState(() {
  34. // Update the text
  35. textToShow = "Flutter is Awesome!";
  36. });
  37. }
  38.  
  39. @override
  40. Widget build(BuildContext context) {
  41. return new Scaffold(
  42. appBar: new AppBar(
  43. title: new Text("Sample App"),
  44. ),
  45. body: new Center(child: new Text(textToShow)),
  46. floatingActionButton: new FloatingActionButton(
  47. onPressed: _updateText,
  48. tooltip: 'Update Text',
  49. child: new Icon(Icons.update),
  50. ),
  51. );
  52. }
  53. }

该如何布局我的 widget 呢?什么东西可以等价于一个 XAML 文件?

在 Xamarin.Forms 中,大部分开发者用 XAML 写布局,尽管有时用 C#。在 Flutter 中编码一棵 widget 树来编写布局。

接下来的示例展示如何显示一个简单的带填充(padding)的 widget:

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

您可以查看 Flutter 在 widget 目录 中提供的布局。

如何从布局中添加或移除一个元素?

在 Xamarin.Forms 中,你需要在代码中移除或添加一个 元素(Element)。如果它说一个列表,这将会涉及设置 Content 属性或者调用 Add() 或者 Remove() 方法。

在 Flutter 中,因为 widget 都是不可变的,所以没有直接对等的东西。相反,你可以将一个返回一个 widget 的函数传递给父级,并用布尔标控制它的子 widget 的创建。

下面的示例展示当用户点击 FloatingActionButton 时,如何在两个 widget 之间切换。

  1. class SampleApp extends StatelessWidget {
  2. // This widget is the root of your application.
  3. @override
  4. Widget build(BuildContext context) {
  5. return new MaterialApp(
  6. title: 'Sample App',
  7. theme: new ThemeData(
  8. primarySwatch: Colors.blue,
  9. ),
  10. home: new SampleAppPage(),
  11. );
  12. }
  13. }
  14.  
  15. class SampleAppPage extends StatefulWidget {
  16. SampleAppPage({Key key}) : super(key: key);
  17.  
  18. @override
  19. _SampleAppPageState createState() => new _SampleAppPageState();
  20. }
  21.  
  22. class _SampleAppPageState extends State<SampleAppPage> {
  23. // Default value for toggle
  24. bool toggle = true;
  25. void _toggle() {
  26. setState(() {
  27. toggle = !toggle;
  28. });
  29. }
  30.  
  31. _getToggleChild() {
  32. if (toggle) {
  33. return new Text('Toggle One');
  34. } else {
  35. return new CupertinoButton(
  36. onPressed: () {},
  37. child: new Text('Toggle Two'),
  38. );
  39. }
  40. }
  41.  
  42. @override
  43. Widget build(BuildContext context) {
  44. return new Scaffold(
  45. appBar: new AppBar(
  46. title: new Text("Sample App"),
  47. ),
  48. body: new Center(
  49. child: _getToggleChild(),
  50. ),
  51. floatingActionButton: new FloatingActionButton(
  52. onPressed: _toggle,
  53. tooltip: 'Update Text',
  54. child: new Icon(Icons.update),
  55. ),
  56. );
  57. }
  58. }

如何让一个 widget 动起来?

在 Xamarin.Forms 中,你可以利用包括例如 FadeToTranslateTo 等方法的视图扩展(ViewExtensions)来创建简单的动画。你会在一个视图中使用这些方法来执行需要的动画。

  1. <Image Source="{Binding MyImage}" x:Name="myImage" />

然后再后面的代码或一个动作中,这个会在1秒内淡入这张图像。

  1. myImage.FadeTo(0, 1000);

在 Flutter 中,通过封装 widget 到一个动画 widget 中,可以使用动画类库来让 widget 动起来。使用一个 AnimationController ,即一个可以暂停、寻找、停止和倒退动画的 Animation<double> 。它需要一个滴答器(Ticker),当垂直同步(vsync)发生时,会发出信号,并在运行时的每一帧都会产生0和1之间的线性插值。然后你可以创建一个或多个动画并把它们附加到控制器上。

举个例子,你可以使用 CurvedAnimation 来实现一个沿着插值曲线的动画。在这个场景中,控制器说一个动画进展的“大师”源,而 CurvedAnimation 计算用来替代控制器默认线性运动的曲线。跟 widget 一样,Flutter 中的动画与组成一起工作。

当你在构建一个 widget 树,赋值一个动画(Animation)给一个 widget 的一个动画属性时,比如 渐退(FadeTransition)的不透明度,会告诉控制器开始执行动画。

下面的实例展示如何去写一个 渐退(FadeTransition),当你按下 FloatingActionButton 时,它会把 widget 渐变到一个logo。

  1. import 'package:flutter/material.dart';
  2.  
  3. void main() {
  4. runApp(new FadeAppTest());
  5. }
  6.  
  7. class FadeAppTest extends StatelessWidget {
  8. // This widget is the root of your application.
  9. @override
  10. Widget build(BuildContext context) {
  11. return new MaterialApp(
  12. title: 'Fade Demo',
  13. theme: new ThemeData(
  14. primarySwatch: Colors.blue,
  15. ),
  16. home: new MyFadeTest(title: 'Fade Demo'),
  17. );
  18. }
  19. }
  20.  
  21. class MyFadeTest extends StatefulWidget {
  22. MyFadeTest({Key key, this.title}) : super(key: key);
  23. final String title;
  24. @override
  25. _MyFadeTest createState() => new _MyFadeTest();
  26. }
  27.  
  28. class _MyFadeTest extends State<MyFadeTest> with TickerProviderStateMixin {
  29. AnimationController controller;
  30. CurvedAnimation curve;
  31.  
  32. @override
  33. void initState() {
  34. controller = new AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
  35. curve = new CurvedAnimation(parent: controller, curve: Curves.easeIn);
  36. }
  37.  
  38. @override
  39. Widget build(BuildContext context) {
  40. return new Scaffold(
  41. appBar: new AppBar(
  42. title: new Text(widget.title),
  43. ),
  44. body: new Center(
  45. child: new Container(
  46. child: new FadeTransition(
  47. opacity: curve,
  48. child: new FlutterLogo(
  49. size: 100.0,
  50. )))),
  51. floatingActionButton: new FloatingActionButton(
  52. tooltip: 'Fade',
  53. child: new Icon(Icons.brush),
  54. onPressed: () {
  55. controller.forward();
  56. },
  57. ),
  58. );
  59. }
  60. }

更多信息,可以查阅 动画 & 运动 widget动画教程,以及动画概述

如何在屏幕上绘图?

Xamarin.Forms 从来没有任何内置的方法来直接在屏幕上绘图。如果他们需要一个自定义图像绘制,大多数使用 SkiaSharp。在 Flutter中,你可以直接访问 Skia 画布(Skia Canvas)方便地在屏幕上绘图。

Flutter 拥有两个类来帮助你在画布上绘图:CustomPaintCustomPainter,后者实现了你在画布上绘图的算法。

如果想学习如何在 Flutter 中实现一个签名画手,请阅读 Collin 在 StackOverflow 的回答。

  1. import 'package:flutter/material.dart';
  2.  
  3. void main() => runApp(new MaterialApp(home: new DemoApp()));
  4.  
  5. class DemoApp extends StatelessWidget {
  6. Widget build(BuildContext context) => new Scaffold(body: new Signature());
  7. }
  8.  
  9. class Signature extends StatefulWidget {
  10. SignatureState createState() => new SignatureState();
  11. }
  12.  
  13. class SignatureState extends State<Signature> {
  14. List<Offset> _points = <Offset>[];
  15. Widget build(BuildContext context) {
  16. return new GestureDetector(
  17. onPanUpdate: (DragUpdateDetails details) {
  18. setState(() {
  19. RenderBox referenceBox = context.findRenderObject();
  20. Offset localPosition =
  21. referenceBox.globalToLocal(details.globalPosition);
  22. _points = new List.from(_points)..add(localPosition);
  23. });
  24. },
  25. onPanEnd: (DragEndDetails details) => _points.add(null),
  26. child: new CustomPaint(painter: new SignaturePainter(_points), size: Size.infinite),
  27. );
  28. }
  29. }
  30.  
  31. class SignaturePainter extends CustomPainter {
  32. SignaturePainter(this.points);
  33. final List<Offset> points;
  34. void paint(Canvas canvas, Size size) {
  35. var paint = new Paint()
  36. ..color = Colors.black
  37. ..strokeCap = StrokeCap.round
  38. ..strokeWidth = 5.0;
  39. for (int i = 0; i < points.length - 1; i++) {
  40. if (points[i] != null && points[i + 1] != null)
  41. canvas.drawLine(points[i], points[i + 1], paint);
  42. }
  43. }
  44. bool shouldRepaint(SignaturePainter other) => other.points != points;
  45. }

widget 的不透明度在哪里?

Xamarin.Forms 上,所有 虚拟元素(VisualElement)都拥有一个不透明度。在 Flutter 中,你需要封装一个 widget 到一个 不透明度 widget 来实现它。

如何构建一个自定义 widget ?

在 Xamarin.Forms 中,通常派生 VisualElement 或使用一个已有的 VisualElement ,来重写和实现所需行为的方法。

在 Flutter 中,通过组合(composing)更小的 widget(而不是扩展它们)来构建一个自定义 widget。这有点类似于基于 Grid 实现自定义控件,其中添加了大量 VisualElement,同时使用自定义逻辑进行扩展。

举个例子,如何构建一个在构造器接受一个标签的自定义按钮?创建一个组合了一个带有标签的RaisedButton的自定义按钮,而不是扩展 RaisedButton

  1. class CustomButton extends StatelessWidget {
  2. final String label;
  3.  
  4. CustomButton(this.label);
  5.  
  6. @override
  7. Widget build(BuildContext context) {
  8. return new RaisedButton(onPressed: () {}, child: new Text(label));
  9. }
  10. }

然后就可以像使用其他 Flutter widget 一样使用这个自定义按钮

  1. @overrideWidget build(BuildContext context) { return new Center( child: new CustomButton("Hello"), );}

导航

如何在页面之间导航?

在 Xamarin.Forms 中,在页面之间导航通常会通过一个 CarouselPage。在 Flutter 中,你可以使用一个 NavigationPage 来管理页面栈去显示。

Flutter 也有类似的实现,使用了一个导航器(Navigator)路由(Routes)。一个路由是一个应用程序里一个页面的抽象,而一个导航器是一个管理路由的 widget

一个路由大致上映射到一个页面。导航器以类似 Xamarin.Forms NavigationPage 的方式工作,在里面可以 push()pop() 路由,依赖于你是否想导航到一个视图,或者从它返回。

在页面间导航,你有几个选择:

  • Specify a Map of route names. (MaterialApp)
  • Directly navigate to a route. (WidgetApp)

  • 指定路由名称的一个映射。(MaterialApp)

  • 直接导航到一个路由。(WidgetApp)

接下来构建一个映射的示例。

  1. void main() {
  2. runApp(new MaterialApp(
  3. home: new MyAppHome(), // becomes the route named '/'
  4. routes: <String, WidgetBuilder> {
  5. '/a': (BuildContext context) => new MyPage(title: 'page A'),
  6. '/b': (BuildContext context) => new MyPage(title: 'page B'),
  7. '/c': (BuildContext context) => new MyPage(title: 'page C'),
  8. },
  9. ));
  10. }

通过推入一个路由的名称到导航器来导航到这个路由。

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

导航器是一个管理你的应用程序的路由的堆栈。把一个路由推入堆栈可以移动到这个路由,而从堆栈弹出一个路由可以返回到前一个路由。这是通过等待push() 返回的 未来(Future) 来完成的。

Async/await 与 .NET 的实现非常类似,并且是在 Async UI中有更详尽的解释。

举个例子,要开始一个让用户选择他们的定位的 定位(location) 路由,你需要做以下步骤:

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

然后,在你的“定位”路由里,一旦用户选择他们的定位,使用结果来 pop() 这个堆栈。

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

如何导航到其它应用程序?

在 Xamarin.Forms 中,需要用指定的 URI 协议并使用 Device.OpenUrl("mailto://") 来传送用户到其它应用程序。

为了在 Flutter 中实现这个功能,需要创建一个原生平台集成,或者使用 已有的插件,比如 url_launcher,可与在 [pub.dev 上的许多其他包一起使用。

异步 UI

在 Flutter 中有什么是跟 Device.BeginOnMainThread() 方法是相等的?

Dart 拥有一个单线程执行模型,支持“隔离”(在另一个线程上运行Dart代码的方法)、事件循环和异步编程。除非生成一个“隔离”,否则您的Dart代码会在主UI线程中运行,并由一个事件循环来驱动。

Dart 的单线程模型并不意味着需要以会导致UI冻结的阻塞操作方式来运行所有内容。更多地像 Xamarin.Forms 一样需要让 UI 线程保持空闲。您将使用“async”/“wait”来执行任务,其中必须等待响应。

在 Flutter 中,使用 Dart 语言提供的异步工具(也称为 async/await)来执行异步工作。这跟 C# 很像,并且对于 Xamarin.Forms 开发者来说应该是非常容易使用的。

例如,您可以使用 async/await 运行网络请求代码,而不会导致UI挂起,并让Dart完成繁重的工作:

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

一旦完成等待的网络调用后,通过调用 setState() 更新UI,这将触发 widget 子树的重新构建并更新数据。

下面的实例异步加载数据并在一个 ListView 中显示:

  1. import 'dart:convert';
  2.  
  3. import 'package:flutter/material.dart';
  4. import 'package:http/http.dart' as http;
  5.  
  6. void main() {
  7. runApp(new SampleApp());
  8. }
  9.  
  10. class SampleApp extends StatelessWidget {
  11. @override
  12. Widget build(BuildContext context) {
  13. return new MaterialApp(
  14. title: 'Sample App',
  15. theme: new ThemeData(
  16. primarySwatch: Colors.blue,
  17. ),
  18. home: new SampleAppPage(),
  19. );
  20. }
  21. }
  22.  
  23. class SampleAppPage extends StatefulWidget {
  24. SampleAppPage({Key key}) : super(key: key);
  25.  
  26. @override
  27. _SampleAppPageState createState() => new _SampleAppPageState();
  28. }
  29.  
  30. class _SampleAppPageState extends State<SampleAppPage> {
  31. List widgets = [];
  32.  
  33. @override
  34. void initState() {
  35. super.initState();
  36.  
  37. loadData();
  38. }
  39.  
  40. @override
  41. Widget build(BuildContext context) {
  42. return new Scaffold(
  43. appBar: new AppBar(
  44. title: new Text("Sample App"),
  45. ),
  46. body: new ListView.builder(
  47. itemCount: widgets.length,
  48. itemBuilder: (BuildContext context, int position) {
  49. return getRow(position);
  50. }));
  51. }
  52.  
  53. Widget getRow(int i) {
  54. return new Padding(
  55. padding: new EdgeInsets.all(10.0),
  56. child: new Text("Row ${widgets[i]["title"]}")
  57. );
  58. }
  59.  
  60. loadData() async {
  61. String dataURL = "https://jsonplaceholder.typicode.com/posts";
  62. http.Response response = await http.get(dataURL);
  63. setState(() {
  64. widgets = json.decode(response.body);
  65. });
  66. }
  67. }

有关在后台工作、以及 Flutter 与 Android 的不同之处的更多信息,请参考下一节。

如何将工作转移到后台线程?

因为 Flutter 是单线程的,并且运行一个事件循环,所以您不必担心线程管理或产生后台线程。这一点与 Xamarin.Forms 非常相似。如果您正在做 I/O 密集型的工作,比如磁盘访问或网络调用,那么您可以安全地使用 async/await,这样就一切就绪了。

另一方面,如果您需要做计算密集型的工作,使CPU保持忙碌,那么您希望将它移动到“隔离”状态,以避免阻塞事件循环,就像您将任何类型的工作放在主线程之外一样。这类似于通过 Xamarin.Forms 中的 Task.Run() 将内容移动到另一个线程。

对于 I/O 密集型的工作,将函数声明为一个 异步 函数,并在函数内部 等待 长时间运行的任务:

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

这是您通常执行网络或数据库调用的方式,它们都是I/O操作。

然而,有时您可能正在处理大量数据而UI挂起了。在 Flutter 中,使用隔离来利用多个CPU内核来执行长时间运行或计算密集型任务。

隔离线程是独立的执行线程,不与主执行内存堆共享任何内存。这是与 Task.Run() 的区别。这意味着您不能从主线程访问变量,也不能通过调用 setState() 更新UI。

下面的示例以简单的方式展示了如何将数据共享回主线程以更新UI。

loadData() async {
  ReceivePort receivePort = new 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 = new 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 = new ReceivePort();
  port.send([msg, response.sendPort]);
  return response.first;
}

在这里,dataLoader() 是在它自己单独的执行线程中运行的隔离。在隔离中,您可以执行更多的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(new SampleApp());
}

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

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

  @override
  _SampleAppPageState createState() => new _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 new Center(child: new CircularProgressIndicator());
  }

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

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

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

  loadData() async {
    ReceivePort receivePort = new 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 = new 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 = new ReceivePort();
    port.send([msg, response.sendPort]);
    return response.first;
  }
}

如何发送一个网络请求?

在 Xamarin.Forms 中,你可以使用 HttpClient。当您使用流行的 http package 包时,在 Flutter 中进行网络调用就很容易了。这将抽象出许多您通常可能自己实现的网络,从而使网络调用变简化。

要使用 http 包,请将它添加到 pubspec.yaml 文件中的依赖项中:

dependencies:
  ...
  http: ^0.11.3+16

To make a network request, call await on the async function 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);
    });
  }
}

如何显示长时间运行的任务的进度?

在 Xamarin.Forms 中常会创建一个加载指示器,可以直接在XAML中创建,也可以通过第三方插件创建,比如 AcrDialogs。

在 Flutter 中,使用一个 加载指示器( ProgressIndicator)widget。通过一个布尔标志控制何时渲染来以编程方式显示进度。告诉 Flutter 在长时间运行的任务开始之前更新它的状态,并在任务结束后隐藏它。

在下面的示例中,build 函数被分成三个不同的函数。如果 showLoadingDialog()true (即当widgets.length == 0时)就会渲染出 进度指示器。另一方面,用网络调用返回的数据渲染 列表视图(ListView)

import 'dart:convert';

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

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

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

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

  @override
  _SampleAppPageState createState() => new _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 new Center(child: new CircularProgressIndicator());
  }

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

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

  Widget getRow(int i) {
    return new Padding(padding: new EdgeInsets.all(10.0), child: new 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);
    });
  }
}

项目结构与资源

如何储存我的图片文件?

Xamarin.Forms 没有独立于平台的存储图像的方法,您必须放置图片在 iOS 的 xcasset 文件夹,或 Android 的 drawable 文件夹中。

Android和iOS将资源(resources)和资产(assets)视为不同的项目,而 Flutter 应用程序只有资产(assets)。Resources/drawable-* 文件夹中的所有资源都放在一个 Flutter 的资产文件夹中。

Flutter 遵循一种与 iOS 类似的简单的基于密度(density-based)的格式。资产可能是 1.0x2.0x3.0x 或任何其他倍数。Flutter 没有 dp,但是有逻辑像素,这基本上是与设备无关像素相同。用所谓 devicePixelRatio 表示单个逻辑像素中物理像素的比例。

与 Android 的密度桶相等的是:

Android density qualifierFlutter pixel ratio
ldpi0.75x
mdpi1.0x
hdpi1.5x
xhdpi2.0x
xxhdpi3.0x
xxxhdpi4.0x

资产位于任意文件夹中— Flutter 没有预定义的文件夹结构。在 pubspec.yaml 文件中声明资产(带有位置),Flutter 就会得到它们。

注意,在 Flutter 1.0 beta 2 之前的版本中,Flutter 中定义的资产并不能从原生一侧访问,反之亦然,原生资产和资源对 Flutter 无效,就像他们被放在单独的文件夹中。

在 Flutter beta 2 版本中,资产都被存储在原生的资产文件夹中,并且可以通过 Android 的资产管理器(AssetManager) 从原生一侧被访问。

在 Flutter beta 2 版本中,Flutter 仍然不能访问原生资源,也不能访问原生资产。

例如,如果要新建一个新的名为 my_icon.png 的图像资产到我们的 Flutter 项目,并决定它应该放在一个被我们随意命名为 images 的文件夹中,你需要把基础图像(1.0x)放到 images 文件夹中,而所有的其他变量的文件放在以与之对应的比率乘数命名的子文件夹中:

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 new AssetImage("images/a_dot_burr.jpeg");

或者可以直接在一个 Image widget 中访问:

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

更多详尽的信息可以在 在 Flutter 中添加资产和图像 中找到。

在哪里存储字符串?如何处理本地化?

与 .NET 拥有 resx 文件不同,Flutter 目前没有一个专门的字符串类资源系统。此时,最佳实践是将复制文本作为静态字段保存在类中,并从那里访问它们。举个例子:

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

那么在你的代码中,你可以像这样访问你的字符串:

new Text(Strings.welcomeMessage)

默认情况下,Flutter 的字符串只支持美式英语。如果你需要添加其他语音的支持,可以包含 flutter_localizations 包。你可能还需要添加 Dart的 intl包来使用 i10n 装置,例如日期、时间的格式化。

dependencies:
  # ...
  flutter_localizations:
    sdk: flutter
  intl: "^0.15.6"

使用 flutter_localizations 包时,要在应用程序的 widget 上指定 localizationsDelegatessupportedLocales

import 'package:flutter_localizations/flutter_localizations.dart';

new MaterialApp(
 localizationsDelegates: [
   // Add app-specific localization delegate[s] here.
   GlobalMaterialLocalizations.delegate,
   GlobalWidgetsLocalizations.delegate,
 ],
 supportedLocales: [
    const Locale('en', 'US'), // English
    const Locale('he', 'IL'), // Hebrew
    // ... other locales the app supports
  ],
  // ...
)

委托包含实际的本地化值,而 supportedLocales 定义了应用程序支持哪些本地化。上面的示例使用了一个 MaterialApp,因此它为基本 widget 本地化值提供了一个 GlobalWidgetsLocalizations,为Material widget 的本地化提供了一个 MaterialWidgetsLocalizations。如果你的应用程序使用 WidgetsApp ,你就不需要后者了。请注意,这两个委托包含“默认”值,但是如果您希望它们也本地化,则需要为您自己的应用程序的可本地化副本提供一个或多个委托。

初始化后, WidgetsApp (或 MaterialApp)为您创建一个Localizationswidget,其中包含您指定的委托。设备的当前区域设置总是可以从当前上下文的 Localizations widget (以 Locale 对象的形式)或使用 Window.locale 访问。

要访问本地化的资源,请使用 Localizations.of() 方法去访问一个由给定委托提供的特定本地化类。使用 intl_translation 包将可翻译的文本拷贝到 arb 文件中进行翻译,并将其导入到应用程序中与 intl 一起使用。

要了解更多关于 Flutter 国际化和本地化的细节,请查阅 国际化指南,它有带和不带 intl 包的示例代码。

我的项目文件在哪里?

Xamarin.Forms 中有一个 csproj 文件。在 Flutter 中最接近的它的是 pubspec.yaml,其中包含包依赖项和各种项目细节。就像 .NET Standard,相同目录中的文件被认为是项目的一部分。

Nuget 的等价物是什么?如何添加依赖项?

在 .NET 生态系统中,原生 Xamarin 项目和 Xamarin.Forms 项目都可以访问 Nuget 和内置的包管理系统。Flutter 应用程序包含一个原生Android 应用程序,原生 iOS 应用程序和 Flutter 应用程序。

在Android中,您可以通过向Gradle添加构建脚本来添加依赖项。而在iOS中,你可以通过添加到 Podfile 来添加依赖项。

Flutter 使用 Dart 自己的构建系统和 Pub 包管理器。这些工具将原生 Android 和 iOS 封装应用程序的构建委托给各自的构建系统。

一般来说,使用 pubspec.yaml 来声明要在 Flutter 中使用的外部依赖项。Pub 是一个寻找 Flutter 包的好地方。

应用程序生命周期

如何侦听应用程序的生命周期事件?

在 Xamarin.Forms 中,拥有一个包含 OnStart,、OnResumeOnSleep应用程序。在 Flutter 中,您可以通过挂钩到 WidgetsBinding 观察者并监听 didChangeAppLifecycleState() 更改事件来监听类似的生命周期事件。

可观察的生命周期事件有:

  • inactive
  • inactive — 应用程序处于非活动状态,并且没有接收用户输入。此事件仅适用于iOS。
  • paused
  • 应用程序当前对用户不可见,不响应用户输入,但是在后台运行。

  • resumed

  • 应用程序是可见的,并响应用户输入。

  • suspending

  • 应用程序暂时暂停。此事件仅限Android。

有关这些状态的含义的更多细节,可参考 AppLifecycleStatus 文档

布局

什么东西与 StackLayout 等效?

在 Xamarin.Forms 中,可以创建一个带水平或垂直方向StackLayout 。Flutter 也有类似的方法,不过您将使用 RowColumn widget。

如果您注意到除了“Row” 和“Column” widget 之外,这两个代码示例是相同的。这些子元素是相同的,可以利用这个特性开发丰富的布局,这些布局可以随着时间的推移而改变。

@overrideWidget build(BuildContext context) {  return new Row(    mainAxisAlignment: MainAxisAlignment.center,    children: <Widget>[      new Text('Row One'),      new Text('Row Two'),      new Text('Row Three'),      new Text('Row Four'),    ],  );}

@overrideWidget build(BuildContext context) {  return new Column(    mainAxisAlignment: MainAxisAlignment.center,    children: <Widget>[      new Text('Column One'),      new Text('Column Two'),      new Text('Column Three'),      new Text('Column Four'),    ],  );}

什么东西与网格(Grid)等价?

Grid最接近的对等项是 GridView。这比您在 Xamarin.Forms 中习惯使用的功能强大得多。GridView 在内容超出其可视空间时自动滚动。

GridView.count(
  // Create a grid with 2 columns. If you change the scrollDirection to
  // horizontal, this would produce 2 rows.
  crossAxisCount: 2,
  // Generate 100 widgets that display their index in the List
  children: List.generate(100, (index) {
    return Center(
      child: Text(
        'Item $index',
        style: Theme.of(context).textTheme.headline,
      ),
    );
  }),
);

您可能在 Xamarin.Forms 中使用 Grid 来实现覆盖其他 widget 的 widget。在 Flutter 中,您可以使用 Stack widget 来完成这一操作。

这个示例创建了两个相互重叠的图标。

child: new Stack(
  children: <Widget>[
    new Icon(Icons.add_box, size: 24.0, color: const Color.fromRGBO(0,0,0,1.0)),
    new Positioned(
      left: 10.0,
      child: new Icon(Icons.add_circle, size: 24.0, color: const Color.fromRGBO(0,0,0,1.0)),
    ),
  ],
),

有什么等同于 ScrollView ?

在 Xamarin.Forms 中,ScrollView 封装了 VisualElement ,如果内容大于设备屏幕,它就会滚动。

在 Flutter 中,最接近的是 SingleChildScrollView widget。您只需用想要可滚动的内容来填充 widget。

@overrideWidget build(BuildContext context) {  return new SingleChildScrollView(    child: new Text('Long Content'),  );}

如果您想在滚动条中包含许多项,即使是不同的Widget类型,也可以使用 ListView。这可能看起来有点过火,但在 Flutter 中,它比 Xamarin.Forms 的回到平台特定控件的 ListView 优化得多,松散得多。

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

在 Flutter 中如何处理横向过渡 ?

通过在 AndroidManifest.xml 中设置 configChanges 属性,可以自动处理横向转换。

android:configChanges="orientation|screenSize"

手势检测和触摸事件处理

如何在Flutter中向 widget 添加手势识别器?

在 Xamarin.Forms 中,元素(Element)可能包含一个可供附加(attach)的单击事件。许多元素还包含一个与此事件关联的 命令 。或者你可以使用 TapGestureRecognizer。在 Flutter 中有两种非常相似的方式:

  • 如果 widget 支持事件发现(detection),那么可以将函数传递给它并在函数中处理它:
@override
Widget build(BuildContext context) {
  return new RaisedButton(
      onPressed: () {
        print("click");
      },
      child: new Text("Button"));
}
  • If the widget doesn’t support event detection, wrap thewidget in a GestureDetector and pass a function to theonTap parameter.

如果 widget 不支持事件发现,则将 widget 封装在手势检测器(GestureDetector)中,并将函数传递给“onTap”参数。

<!-- skip -->
```dart
class SampleApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new Scaffold(
        body: new Center(
      child: new GestureDetector(
        child: new FlutterLogo(
          size: 200.0,
        ),
        onTap: () {
          print("tap");
        },
      ),
    ));
  }
}
```

我如何处理 widget 上的其他手势?

在 Xamarin.Forms 中你可以在VisualElement中添加一个手势识别器(GestureRecognizer)。您通常只能使用 TapGestureRecognizerPinchGestureRecognizerPanGestureRecognizer,除非您构建了自己的实现。

在Flutter中,使用手势检测器,你可以监听到各种各样的手势,比如:

  • 单击
  • onTapDown
  • 当指尖在特定位置与屏幕接触产生点击事件。

  • onTapUp

  • 当指尖触发的点击事件已经停止在特定位置与屏幕接触。

  • onTap

  • 一个点击事件已经发生

  • onTapCancel

  • 触发了 onTapDown 事件之后的指尖没有导致点击事件。
  • 双击
  • onDoubleTap
  • 用户在同一位置连续快速点击屏幕两次。
  • 长按
  • onLongPress
  • 指尖长时间保持与屏幕在同一位置的接触。
  • 垂直拖动
  • onVerticalDragStart
  • 指尖与屏幕接触后,可能开始垂直移动。

  • onVerticalDragUpdate

  • 指尖与屏幕接触并在垂直方向上移动得更远。

  • onVerticalDragEnd

  • 指尖在之前与屏幕接触并垂直移动,当不再与屏幕接触时触发这个事件。当它停止与屏幕接触时,它会以特定的速度移动。
  • 水平拖动
  • onHorizontalDragStart
  • 指尖与屏幕接触,开始水平移动时触发。

  • onHorizontalDragUpdate

  • 指尖与屏幕接触并在水平方向上移动得更远。

  • onHorizontalDragEnd

  • 指尖在之前与屏幕接触并水平移动,当不再与屏幕接触时会触发这个事件。当它停止与屏幕接触时,它正在以特定的速度移动。

下面的例子展示了一个手势检测器,它可以在双击下旋转 Flutter 的 logo:

AnimationController controller;
CurvedAnimation curve;

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

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

列表视图和适配器

在 Flutter 中,与列表视图等价的是什么?

在Flutter中与 ListView 等价的是……一个 ListView

在一个 Xamarin.Forms 的 ListView 中,你可以创建一个 ViewCell 可能还有一个 DataTemplateSelector 并将其传递到 ListView 中,该视图将用您的DataTemplateSelector 或者 ViewCell 的返回数据渲染每一行。但是,您通常必须确保打开单元格回收,否则会遇到内存问题和会使滚动速度变慢。

由于 Flutter 的不可变的 widget 模式,您将一个 widget 列表传递给您的 ListView,Flutter 会负责确保滚动速度快且平稳。

import 'package:flutter/material.dart';

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

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

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

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

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

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

如何知道哪个列表项被点击了?

在 Xamarin.Forms 中,ListView 拥有一个ItemTapped 方法能找出哪个列表项被单击了。您可能还使用了许多其他技术,比如检查 SelectedItemEventToCommand 的行为何时会发生更改。

在 Flutter 中,使用传入 widget 提供的触摸处理。

import 'package:flutter/material.dart';

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

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

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

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

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

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

如何动态更新 ListView ?

在 Xamarin.Forms 中,如果将 ItemsSource 属性绑定到一个 ObservableCollection,就只需要更新视图模型中的列表。另一种方法是,你可以给属性 ItemsSource 分配一个新的 列表

在 Flutter 中,情况略有不同。如果您要在 setState() 内更新 widget 列表,您将很快看到您的数据在视觉上没有发生变化。这是因为当 setState() 被调用时,Flutter 的渲染引擎会检查 widget 树是否发生了更改。当它到达您的 ListView 时,会执行 == 检查,并确定这两个 ListView 是相同的。没有任何更改,就不需要更新。

要更新 ListView 的有一个简单方法,请在 setState() 中创建一个新 列表 ,并将数据从旧列表复制到新列表。虽然这种方法很简单,但不推荐用于大型数据集,如下例所示。

import 'package:flutter/material.dart';

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

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

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

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

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

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

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

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

推荐的、高效的、有效的列表构建方法是使用 ListView.Builder。在您有一个动态列表或一个包含大量数据的列表时,这种方法非常棒。这基本上相当于 Android 上的 RecyclerView,它会自动回收列表元素:

import 'package:flutter/material.dart';

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

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

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

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

class _SampleAppPageState extends State<SampleAppPage> {
  List widgets = [];

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

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

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

与创建一个“列表视图” 相比,创建一个 ListView.builder需要接受两个关键参数:列表的初始长度和 ItemBuilder 函数。

ItemBuilder 函数类似于 Android 适配器中的 getView 函数;它接受一个位置,并返回您希望的在该位置呈现的行。

最后,但也是最重要的,要注意 onTap() 函数不再重新创建列表,而是用 .add 添加给它的。

更多信息,请访问编写你的第一个 Flutter 应用程序,第1部分编写你的第一个 Flutter 应用程序,第2部分

文本处理

如何在文本(Text) widget 上设置自定义字体?

在 Xamarin.Forms 中,您必须在每个原生项目中添加自定义字体。然后在你的 元素 中,你会使用 filename#fontnameFontFamily 属性分配这个字体名,而在iOS中只使用 fontname

在 Flutter 中,将字体文件放在一个文件夹中,并在 pubspec.yaml 中引用它,这跟导入图像的方式类似。

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

Then assign the font to your Text widget:

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

如何设置文本 widget 的样式?

除了字体,您还可以在文本 widget 上定制其他样式元素。文本 widget 的样式参数接受一个 TextStyle 对象,您可以在其中定制许多参数,比如:

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

表单录入

如何检索用户输入?

Xamarin.Forms 的元素允许您直接查询元素来确定它的任何属性的状态,或者它被绑定到视图模型中的属性。

在 Flutter 中检索信息是由专门的 widget 处理的,这是跟原来的习惯不同的。如果你有一个 TextFieldTextFormField ,你可以提供一个 TextEditingController 来检索用户输入:

class _MyFormState extends State<MyForm> {
  // Create a text controller and use it to retrieve the current value
  // of the TextField.
  final myController = new TextEditingController();

  @override
  void dispose() {
    // Clean up the controller when disposing of the widget.
    myController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text('Retrieve Text Input'),
      ),
      body: new Padding(
        padding: const EdgeInsets.all(16.0),
        child: new TextField(
          controller: myController,
        ),
      ),
      floatingActionButton: new FloatingActionButton(
        // When the user presses the button, show an alert dialog with the
        // text that the user has typed into our text field.
        onPressed: () {
          return showDialog(
            context: context,
            builder: (context) {
              return new AlertDialog(
                // Retrieve the text that the user has entered using the
                // TextEditingController.
                content: new Text(myController.text),
              );
            },
          );
        },
        tooltip: 'Show me the value!',
        child: new Icon(Icons.text_fields),
      ),
    );
  }
}

你可以在 Flutter 实用教程 中的 获取文本框的输入值 找到更多的信息和完整的代码清单。

在入口的占位符 (Placeholder) 与什么等价?

在 Xamarin.Forms 中,一些元素支持占位符(Placeholder)属性,可以给它赋一个值。如:

<Entry Placeholder="This is a hint">

在 Flutter 中,通过在文本 widget 的装饰器构造函数参数中添加 InputDecoration 对象,可以轻松地为输入显示“提示”或占位符文本。

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

如何显示验证错误?

使用 Xamarin.Forms 时,如果您希望提供验证错误的可视化提示,则需要创建新属性和 虚拟元素(VisualElement) 来包围具有验证错误的元素。

在 Flutter 中,我们将 InputDecoration 对象传递给文本 widget 的装饰器构造函数。

然而,您不希望从显示错误开始。相反,当用户输入无效数据时,应该更新状态,并传递一个新的 InputDecoration 对象。

import 'package:flutter/material.dart';

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

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

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

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

class _SampleAppPageState extends State<SampleAppPage> {
  String _errorText;

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text("Sample App"),
      ),
      body: new Center(
        child: new TextField(
          onSubmitted: (String text) {
            setState(() {
              if (!isEmail(text)) {
                _errorText = 'Error: This is not an email';
              } else {
                _errorText = null;
              }
            });
          },
          decoration: new 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 = new RegExp(emailRegexp);

    return regExp.hasMatch(em);
  }
}

Flutter 插件

与硬件、第三方服务和平台交互

应该如何与平台以及平台原生代码交互?

Flutter 不直接在底层平台上运行代码。相反,构成一个 Flutter 应用程序的 Dart 代码是在设备上原生运行的,“绕开”了平台提供的 SDK。这意味着,例如,当您在 Dart 中执行网络请求时,它将直接运行在 Dart 上下文中。在编写原生应用程序时,您通常不会使用 Android 或 iOS 的 API。Flutter 应用程序仍然作为视图驻留在原生应用程序的 ViewControllerActivity 中,但您不能直接访问这个或原生框架。

这并不意味着 Flutter 应用程序不能与这些原生 API 或您自己的任何原生代码交互。Flutter 提供 平台通道,可以与托管 Flutter 视图的 ViewControllerActivity 通信和交换数据。平台通道本质上是一个异步消息传递机制,它将 Dart 代码与 ViewControllerActivity 宿主以及它所运行的 iOS 或 Android 框架桥接起来。例如,您可以使用平台通道在原生端执行一个方法,或者从设备的传感器检索一些数据。

除了直接使用平台通道外,您还可以使用各种预制 插件,它们封装了针对特定目标的原生代码和Dart代码。例如,您可以使用插件直接从Flutter访问相机交卷和设备相机,而无需编写自己的集成。插件可以在 Pub、Dart 和 Flutter 的开源包存储库中找到。有些包可能支持iOS上的本地集成,有些支持Android,还有两者都兼而有之的。

如果在Pub上找不到适合您需求的插件,您可以编写自己的插件在Pub上发布

如何访问 GPS 传感器?

使用 geolocator 社区插件.

如何访问摄相机?

image_picker 是流行的访问相机的插件。

如何通过 Facebook 登录?

To log in with Facebook, use the

使用 flutter_facebook_login 社区插件来通过 Facebook 登录。

如何使用 Firebase 特性?

大多数 Firebase 功能被 第一方插件 覆盖。

你也可以在 Pub 上找一些第三方 Firebase 插件,它们覆盖了第一方插件没有直接覆盖的区域。

如何构建自定义的原生集成?

如果有 Flutter 或它的社区插件没有的指定平台的功能,可以根据 开发包与插件 页面自己构建。

简单地说,Flutter 的插件架构很像在 Android 中使用事件总线:您发出一条消息,让接收方处理并向您发回一个结果。在这个例子中,接收方是运行在 Android 或 iOS 上的原生代码。

主题(样式)

如何美化我的应用程序?

Flutter 附带了一个内建的漂亮的 Material Design 实现,它处理了许多您通常会做的样式和主题需求

Xamarin.Forms 确实有一个全局的 资源字典,可以为你的应用程序共享样式。另外,预览版目前还支持主题。

在 Flutter 中,需要在最顶级 widget 中声明主题。

要在应用程序中充分利用 Material 组件,需要声明一个最顶级 widget MaterialApp 作为应用程序的入口点。MaterialApp 是一个方便的 widget,它封装了许多实现Material Design的应用程序通常需要的各种 widget。它通过添加 Material 的指定功能来构建一个 WidgetsApp。

还可以使用一个 WidgetApp 作为应用程序的 widget,它提供了一些相同的功能,但没有 MaterialApp 丰富。

要定制任何子组件的颜色和样式,请将主题数据(ThemeData)对象传递给MaterialApp widget。例如,在下面的代码中,主色调设置为蓝色,文本选择颜框色为红色。

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

数据库与本地存储

如何访问共享首选项或用户默认值?

Xamarin.Forms 开发者可能会熟悉 Xam.Plugins.Settings 插件。

在 Flutter 中,使用 Shared Preferences 插件 就可以访问相同的功能。这个插件封装了 用户默认值 和等同 Android 的 共享首选项

在 Flutter 中如何访问 SQLite

在 Xamarin.Forms 中大多数应用会使用 sqlite-net-pcl 插件来访问 SQLite 数据库。

在 Flutter 中,使用 SQFlite 插件来访问这个功能。

调试

应该使用什么工具调试我的 Flutter 应用?

请使用 开发者工具 debug 你的 Flutter 和 Dart 应用。

开发者工具包含了 profiling 构建、检查堆栈、检视 widget 树、诊断信息记录、调试、执行代码行观察、调试内存泄漏和内存碎片等。有关更多信息,请参阅 开发者工具 文档。

通知

如何设置通知推送?

在 Android 中,你可以利用 Firebase Cloud Messaging 来给应用程序设置通知推送。

在 Flutter 中,通过 Firebase_Messaging 插件来访问这个功能。更多关于使用 Firebase Cloud Messaging API 的信息,可以参考 firebase_messaging 插件文档。