给 Android 开发者的 Flutter 指南(上)

这篇文档旨在帮助 Android 开发者通过 Flutter 开发移动应用。如果你了解 Android 框架的基本知识,你就可以使用这篇文档作为 Flutter 开发的快速入门。

你的 Android 知识和技能对于 Flutter 开发是非常有用的,因为 Flutter 依赖于 Android 操作系统的多种功能和配置。Flutter 是一种全新的构建移动界面的方式,但是它有一套和 Android(以及 iOS)进行非 UI 任务通信的插件系统。如果你是一名 Android 专家,你就不必重新学习所有知识才能使用Flutter。

这篇文档可以用作随时查阅以及答疑解惑的专题手册。

本文结构如下:

1.视图(上)

2.如何在布局中添加或删除一个组件?(上)

3.Intents(上)

4.异步 UI(上)

5.工程结构和资源文件(上)

6.Activity 和 Fragment(上)

7.布局(下)

8.手势监听和触摸事件处理(下)

9.Listviews 和 adapters(下)

10.文字处理(下)

11.表单输入(下)

12.Flutter 插件(下)

13.主题(下)

14.数据库和本地存储(下)

15.通知(下)


一、视图

1.1 视图 在 Flutter 中的对应概念是什么?

响应式或者声明式的编程和传统的命令式风格有什么不同呢?作为对比,请查阅声明式 UI 介绍。

Android 中的 View 是显示在屏幕上的一切的基础。按钮、工具栏、输入框以及一切内容都是 View。而 Flutter 中 View 的大致对应物是Widget。Widget 并非完全对应于 Android 中的 View,但是在你熟悉 Flutter 的工作原理的过程中可以把它们看做“声明和构建 UI 的方式”。

然而,Widget 和 View 还是有一些差异。首先,Widget 有着不一样的生命周期:它们是不可变的, 一旦需要变化则生命周期终止。任何时候 Widget 或它们的状态变化时,Flutter 框架都会创建一个新的 Widget 树的实例。对比来看,一个 Android View 只会绘制一次,除非调用 invalidate 才会重绘。

Flutter 的 Widget 很轻量,部分原因在于它们的不可变性。因为它们本身既非视图,也不会直接绘制任何内容,而是 UI 及其底层创建真正视图对象的语义的描述。

Flutter 支持 Material Components 库。它提供实现了 Material Design 设计规范 的控件。 Meterial Design 是一套为所有平台优化(包括 iOS)的灵活的设计系统。

Flutter 非常灵活、有表达能力,它可以实现任何设计语言。例如,在 iOS 平台上,你可以使用 Cupertino widgets 创建 Apple 的 iOS 设计语言 风格的界面。

1.2 如何更新 Widget?

在 Android 中,你可以直接操作更新 View。然而在 Flutter 中,Widget 是不可变的,无法被直接更新,你需要操作 Widget 的状态。

这就是有状态 (Stateful) 和无状态 (Stateless) Widget 概念的来源。StatelessWidget 如其字面意思—没有状态信息的 Widget。

StatelessWidget 用于你描述的用户界面的一部分不依赖于除了对象中的配置信息以外的任何东西的场景。

例如在 Android 中,这就像显示一个展示图标的 ImageView。这个图标在运行过程中不会改变, 所以在 Flutter 中就使用 StatelessWidget。

如果你想要根据 HTTP 请求返回的数据或者用户的交互来动态地更新界面,那么你就必须使用 StatefulWidget, 并告诉 Flutter 框架 Widget 的状态 (State) 更新了,以便 Flutter 可以更新这个 Widget。

这里需要着重注意的是,无状态和有状态的 Widget 本质上是行为一致的。它们每一帧都会重建,不同之处在于 StatefulWidget有一个跨帧存储和恢复状态数据的 State 对象。

如果你有疑问,那么记住这条规则:如果一个 Widget 会变化(例如由于用户交互),它是有状态的。然而,如果一个 Widget 响应变化,它的父 Widget 只要本身不响应变化,就依然是无状态的。

StatelessWidget的使用方法如下,Text Widget 是一个普通的 StatelessWidget。如果你查看 Text Widget 的实现,你会发现它继承自 StatelessWidget。

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

如上所示,这个 Text Widget 没有相关联的状态信息,它只渲染传入构造器的信息,仅此而已。

但是,假如你想要动态地改变“I Like Flutter”,例如当你点击一个 FloatingActionButton 的时候,该怎么办呢?

为了实现这个效果,将 Text Widget 嵌入一个 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),
      ),
    );
  }
}

1.3 如何布局 Widget?我的 XML 布局文件在哪里?

在 Android 中,你通过 XML 文件定义布局,但是在 Flutter 中,你是通过一个 Widget 树来定义布局的。

以下示例展示了如何显示一个带有填充 (padding) 的简单 Widget:

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

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

二、如何在布局中添加或删除一个组件?

在 Android 中,你通过调用父 View 的 addChild() 或 removeChild() 方法动态地添加或者删除子 View。在 Flutter 中,由于 Widget 是不可变的,所以没有 addChild() 的直接 对应的方法。不过,你可以给返回一个 Widget 的父 Widget 传入一个方法,并通过布尔标记值控制子 Widget 的创建。

例如,你可以在点击 FloatingActionButton 的时候在两个 Widget 之间切换。

import 'package:flutter/material.dart';

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

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

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

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

class _SampleAppPageState extends State<SampleAppPage> {
  // 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),
      ),
    );
  }
}

2.1 Widget 如何实现动画?

在 Android 中,你既可以通过 XML 文件定义动画,也可以调用 View 对象的 animate() 方法。在 Flutter 里,则使用动画库,通过将 Widget 嵌入一个动画 Widget 的方式实现 Widget 的动画效果。

Flutter 通过 Animation<double> 的子类 AnimationController 来暂停、播放、停止以及逆向播放动画。它需要一个 Ticker 在垂直同步 (vsync) 的时候发出信号,并且在运行的时候创建一个介于 0 和 1 之间的线性插值。然后你就可以创建一个或多个 Animation,并将它们绑定到控制器上。

例如,你可以使用 CurvedAnimation 来实现一个曲线插值的动画。在这种情况下,控制器决定了动画 进度,CurvedAnimation 计算用于替换控制器默认线性动画的曲线值。和 Widget 一样,Flutter 中 的动画效果也可以组合使用。

在构建 Widget 树的时候,你需要将 Animation 对象赋值给某个 Widget 的动画属性,例如 FadeTransition 的不透明度属性,并让控制器开始动画。

下述例子展示了如何实现点击 FloatingActionButton 的时候将一个 Widget 渐变为一个图标的 FadeTransition:

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

获取更多内容,请查看动画 Widget, 动画指南,以及动画概览。

2.2 如何使用 Canvas 进行绘制?

在 Android 中,你可以使用 Canvas 和 Drawable 将图片和形状绘制到屏幕上。Flutter 也有一个 类似于 Canvas 的 API,因为它基于相同的底层渲染引擎 Skia。因此,在 Flutter 中用画布 (canvas) 进行绘制对于 Android 开发者来说是一件非常熟悉的工作。

Flutter 有两个帮助你用画布 (canvas) 进行绘制的类:CustomPaint 和 CustomPainter,后者可以实现自定义的绘制算法。

如果想学习在 Flutter 中如何实现一个签名功能,可以查看 Collin 在 StackOverflow 上的回答。

import 'package:flutter/material.dart';

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

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

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

class SignatureState extends State<Signature> {
  List<Offset> _points = <Offset>[];
  Widget build(BuildContext context) {
    return GestureDetector(
      onPanUpdate: (DragUpdateDetails details) {
        setState(() {
          RenderBox referenceBox = context.findRenderObject();
          Offset localPosition =
          referenceBox.globalToLocal(details.globalPosition);
          _points = List.from(_points)..add(localPosition);
        });
      },
      onPanEnd: (DragEndDetails details) => _points.add(null),
      child: CustomPaint(painter: SignaturePainter(_points), size: Size.infinite),
    );
  }
}

class SignaturePainter extends CustomPainter {
  SignaturePainter(this.points);
  final List<Offset> points;
  void paint(Canvas canvas, Size size) {
    var paint = 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;
}

2.3 如何创建自定义 Widget?

在 Android 中,一般通过继承 View 类,或者使用已有的视图类,再覆写或实现可以达到特定效果的方法。

在 Flutter 中,通过 组合 更小的 Widget 来创建自定义 Widget(而不是继承它们)。这和 Android 中实现一个自定义的 ViewGroup有些类似,所有的构建 UI 的模块代码都在手边,不过由你提供不同的行为—例如,自定义布局 (layout) 逻辑。

举例来说,你该如何创建一个在构造器接收标签参数的 CustomButton?你要组合 RaisedButton 和一个标签来创建自定义按钮,而不是继承RaisedButton:

class CustomButton extends StatelessWidget {
  final String label;

  CustomButton(this.label);

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

然后就像使用其它 Flutter Widget 一样使用 CustomButton:

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

三、Intents

3.1 Intent 在 Flutter 中的对应概念是什么?

在 Android 中,Intent 主要有两个使用场景:在 Activity 之前进行导航,以及组件间通信。Flutter 却没有 intent 这样的概念,但是你依然可以通过原生集成插件 来启动 intent。

Flutter 实际上并没有 Activity 和 Fragment 的对应概念。在 Flutter 中你需要使用 Navigator 和 Route 在同一个 Activity 内的不同界面间进行跳转。

Route 是应用内屏幕和页面的抽象,Navigator 是管理路径 route 的工具。一个 route 对象大致对应于一个 Activity,但是它的含义是不一样的。Navigator 可以通过对 route 进行压栈和弹栈操作实现页面 的跳转。Navigator 的工作原理和栈相似,你可以将想要跳转到的 route 压栈 (push()),想要返回的时候将 route 弹栈 (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'),
    },
  ));
}

通过将 route 名压栈 (push) 到 Navigator 中来跳转到这个 route。

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

Intent 的另一种常见的使用场景是调用外部的组件,例如相机或文件选择器。对于这种情况,你需要创建 一个原生平台集成(或者使用已有的插件)。

想要学习如何创建一个原生平台集成,请查看开发包和插件。

3.2 在 Flutter 中应如何处理从外部应用接收到的 intent?

Flutter 可以通过直接和 Android 层通信并请求分享的数据来处理接收到的 Android intent。

下面的例子中,运行 Flutter 代码的原生 Activity 注册了一个文本分享的 intent 过滤器,这样其它应用就可以和 Flutter 应用分享文本了。

从以上流程可以得知,我们首先在 Android 原生层面(在我们的 Activity 中)处理分享的文本数据, 然后 Flutter 再通过使用 MethodChannel 获取这个数据。

首先,在 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 准备好处理的时候,它会使用一个平台通道请求数据,数据便会从原生端发送过来:

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

最后,当 Widget 渲染的时候,从 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;
      });
    }
  }
}

3.3 startActivityForResult() 的对应方法是什么?

Navigator 类负责 Flutter 的导航,并用来接收被压栈的 route 的返回值。这是通过在 push() 后返回的 Future 上 await 来实现的。

例如,要打开一个让用户选择位置的 route,你可以这样做:

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

然后,在你的位置 route 内,一旦用户选择了位置,你就可以弹栈 (pop) 并返回结果:

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

四、异步 UI

4.1 runOnUiThread() 在 Flutter 中的对应方法是什么?

Dart 有一个单线程执行的模型,同时也支持 Isolate(在另一个线程运行 Dart 代码的方法), 它是一个事件循环和异步编程方式。除非你创建一个 Isolate,否则你的 Dart 代码会运行在主 UI 线程,并被一个事件循环所驱动。Flutter 的事件循环对应于 Android 里的主 Looper—也即绑定到主线程上的 Looper。

Dart 的单线程模型并不意味着你需要以会导致 UI 冻结的阻塞操作的方式来运行所有代码。不同于 Android 中 需要你时刻保持主线程空闲,在 Flutter 中,可以使用 Dart 语言提供的异步工具,例如 async/await 来 执行异步任务。如果你使用过 C# 或者 Javascript 中的 async/await 范式,或者 Kotlin 中的 协程,你应该对它比较熟悉。

例如,你可以通过使用 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);
  });
}

一旦用 await 修饰的网络操作完成,再调用 setState() 更新 UI,这会触发 Widget 子树的重建并更新数据。

下面的例子展示了异步加载数据并将之展示在 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);
    });
  }
}

参考下一节内容获取更多关于后台任务以及 Flutter 与 Android 的差异的信息。

4.2 如何将任务转移到后台线程?

在 Android 中,当你想要访问一个网络资源却又不想阻塞主线程并避免 ANR 的时候,你一般会将任务放到一个后台线程中运行。例如,你可以使用一个 AsyncTask、一个 LiveData、一个 IntentService、一个 JobScheduler 任务或者通过 RxJava 的管道用调度器将任务切换到后台线程中。

由于 Flutter 是单线程并且运行一个事件循环(类似 Node.js),你无须担心线程的管理以及后台线程的创建。如果你在执行和 I/O 绑定的任务,例如存储访问或者网络请求,那么你可以安全地使用 async/await,并无后顾之忧。再例如,你需要执行消耗 CPU 的计算密集型工作,那么你可以将其转移到一个 Isolate 上以避免阻塞事件循环,就像你在 Android 中会将任何任务放到主线程之外一样。

对于和 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 中没有对应的 API,你只需要 await 一个耗时方法调用,Dart 的事件循环 就会帮你处理剩下的事情。

然而,有时候你可能需要处理大量的数据并挂起你的 UI。在 Flutter 中,可以通过使用 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;
  }
}

4.3 OkHttp 在 Flutter 中的对应物是什么?

Flutter 中使用流行的 ref="https://pub.flutter-io.cn/packages/http">http 包进行网络请求是很简单的。

虽然 http 包没有 OkHttp 中的所有功能,但是它抽象了很多通常你会自己实现的网络功能,这使其本身在执行网络请求时简单易用。

如果要使用 http 包,需要在 pubspec.yaml 文件中添加依赖:

dependencies:
  ...
  http: ^0.11.3+16

如果要发起一个网络请求,在异步 (async) 方法 http.get() 上调用 await 即可:

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

4.4 如何为耗时任务显示进度?

在 Android 中你通常会在后台执行一个耗时任务的时候显示一个 ProgressBar 在界面上。

在 Flutter 中,我们使用 ProgressIndicator Widget。通过代码逻辑使用一个布尔标记值控制进度条的渲染。

下述例子中,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);
    });
  }
}

五、工程结构和资源文件

5.1 在哪里放置分辨率相关的图片文件?

虽然 Android 区分对待资源文件 (resources) 和资产文件 (assets),但是 Flutter 应用只有资产文件 (assets)。所有原本在 Android 中应该放在 res/drawable-* 文件夹中的资源文件,在 Flutter 中都放在一个 assets 文件夹中。

Flutter 遵循一个简单的类似 iOS 的密度相关的格式。文件可以是一倍 (1.0x)、两倍 (2.0x)、三倍 (3.0x) 或其它的任意倍数。Flutter 没有 dp 单位,但是有逻辑像素尺寸,基本和设备无关的像素尺寸是一样的。名称为 devicePixelRatio 的尺寸表示在单一逻辑像素标准下设备物理像素的比例。

和 Android 的密度分类的对照表如下:

Android 密度修饰符

| Flutter 像素比例 — | — ldpi | 0.75x mdpi | 1.0x hdpi | 1.5x xhdpi | 2.0x xxhdpi | 3.0x xxxhdpi | 4.0x

文件放置于任意文件夹中—Flutter 没有预先定义好的文件夹结构。你在 pubspec.yaml 文件中定义文件(包括位置信息),Flutter 负责找到它们。

需要注意的是,在 Flutter 1.0 beta 2 之前,在 Flutter 中定义的文件不能被原生端访问,反之亦然,原生端定义的资产文件 (assets) 和资源文件 (resources) 也无法被 Flutter 访问,因为它们是放置于 不同的文件夹中的。

至于 Flutter beta 2,文件是放置于原生端的 asset 文件夹中,所以可以被原生端的 AssetManager 访问:

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

然而对于 Flutter beta 2,Flutter 依然无法访问原生资源文件(resources),也无法访问原生资产文件(assets)。

如果你要向 Flutter 项目中添加一个新的叫 my_icon.png 的图片资源,并且将其放入我们随便起名的叫做 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 AssetImage("images/a_dot_burr.jpeg");

或者通过 Image Widget 直接访问:

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

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

Flutter 当下并没有一个特定的管理字符串的资源管理系统。目前来讲,最好的办法是将字符串作为静态域存放在类中,并通过类访问它们。例如:

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

接着在你们的代码中,你可以这样访问你的字符串:

Text(Strings.welcomeMessage)

Flutter 在 Android 上提供无障碍的基本支持,但是这个功能当下仍在开发。

我们鼓励 Flutter 开发者使用 intl 包进行国际化和本地化。

5.3 Gradle 文件的对应物是什么?我该如何添加依赖?

在 Android 中,你在 Gradle 构建脚本中添加依赖。Flutter 使用 Dart 自己的构建系统以及 Pub 包管理器。构建工具会将原生 Android 和 iOS 壳应用的构建代理给对应的构建系统。

虽然在你的 Flutter 项目的 android 文件夹下有 Gradle 文件,但是它们只用于给对应平台的集成添加原生依赖。一般来说,在 pubspec.yaml 文件中定义在 Flutter 里使用的外部依赖。Pub 是查找 Flutter package 的好地方。

六、Activity 和 Fragment

6.1 Activity 和 Fragment 在 Flutter 中的对应概念是什么?

在 Android 中,一个 Activity 代表用户可以完成的一件独立任务。一个 Fragment 代表一个 行为或者用户界面的一部分。Fragment 用于模块化你的代码,为大屏组合复杂的用户界面,并适配应用的界面。在 Flutter 中,这两个概念都对应于 Widget。

如果要学习更多的关于 Activity 和 Fragment 创建界面的内容,请阅读社区贡献的 Medium 文章, 给 Android 开发者的 Flutter 指南:如何在 Flutter 中设计一个 Activity 界面。

就如在 Intents 部分所提,Flutter 中的界面 都是以 Widget 表示的,因为 Flutter 中一切皆为 Widget。你使用 Navigator 在表示不同屏幕或页面,或者仅仅是相同数据的不同状态和渲染的各个 Route 之间进行导航。

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

在 Android 中,你可以覆写 Actvity 的生命周期方法来监听其生命周期,也可以在 Application 上 注册 ActivityLifecycleCallbacks。在 Flutter 中,这两种方法都没有,但是你可以通过绑定 WidgetsBinding 观察者并监听 didChangeAppLifecycleState() 的变化事件来监听生命周期。

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

  • inactive — 应用处于非活跃状态并且不接收用户输入。这个事件只适用于 iOS,Android 上没有对应的事件。

  • paused — 应用当前对用户不可见,无法响应用户输入,并运行在后台。这个事件对应于 Android 中的 onPause()。

  • resumed — 应用对用户可见并且可以响应用户的输入。这个事件对应于 Android 中的 onPostResume()。

  • suspending — 应用暂时被挂起。这个事件对应于 Android 中的 onStop;iOS 上由于没有对应的事件, 因此不会触发此事件。

想要了解这些状态含义的更多细节,请查看 AppLifecycleStatus 文档。

你可能已经注意到,只有一小部分的 Activity 生命周期事件是可用的;虽然 FlutterActivity 在内部捕获了几乎所有的 Activity 生命周期事件并将它们发送给 Flutter 引擎,但是它们大部分都向你屏蔽了。Flutter 为你管理引擎的启动和停止,在大部分情况下几乎没有理由要在 Flutter 一端监听 Activity 的生命周期。如果你需要通过监听生命周期来获取或释放原生的资源,你无论如何都应该在原生一端做这件事。

下面的例子展示了如何监听容器 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()));
}

(未完待续)

往期精彩回顾

640?wx_fmt=gif
戳 “阅读原文”一起来充电吧
相关推荐
©️2020 CSDN 皮肤主题: Age of Ai 设计师:meimeiellie 返回首页