Flutter 入门

原文:Getting Started with Flutter
作者:Joe Howard
译者:kmyhy

自从十多年前 iOS 和 Android 平台爆发性增长,移动开发界的目标就转向了跨平台开发。只需同时为 iOS 和 Android 编写一个 app 这一点能节省公司和团队的大量时间和工作。

快平台开发的工具近年来出现了许多,比如 Adobe 基于 web 的 PhoneGap,微软的强力工具 Xamarin,以及更新的框架如 Facebook 的 React Native。这些工具各有优劣,它们在移动行业中都获得了一定的成功。

最近,跨平台开发的竞技场上又添加了一位新人,它就是 Google 的 Flutter,在 2018 年 2 月的移动全球大会上宣布它进入 beta 版。Fluter 的特点是快速的开发周期,快速的 UI 渲染及独特的 UI 设计,以及在两个平台上的性能接近原生。

Flutter 介绍

Flutter app 使用了 Dart 语言,源自于 Google,现在是 ECMA 的标准。Dart 吸收了许多现代语言的特性,比如 Kotlin 和 Swift,同时能被转译成 js 代码。

作为一个跨平台框架,Flutter 和 React Native 非常像,因为 Flutter 支持响应式和声明式语法。但和 React Native 不同,Flutter 不需要 Javascript 桥接,这显著提升了 app 的加载时间和整体性能。Dart 是通过 AOT 编译来做到这一点的。

Dart 还有一个独特的地方,它也支持 JIT 编译。Flutter 的 JIT 编译改变了开发流程,它允许在开发中通过热加载来更新 UI,而无需进行新的 build。

你将在本教程中看到,Flutter 框架非常依赖 widget 的概念。在 Flutter 中, widget 不仅仅用在 app 的视图上,也可用于整在整个屏幕甚至是 app 自身。

除了 iOS 和 Android 平台,学习 Flutter 还会让你体验到在 Fuchsia 平台上的开发,这是 Google 开发中的实验性操作系统。Fuchsia 被认为很可能在未来用于替换 Android。

在本教程中,你将编写一个 Flutter app,通过 GitHub API 查询 GitHub 组织的团队成员,并在滚动列表中显示团队成员的信息:

你可以用 iOS 模拟器、Android 模拟器或者两者一起来开发这个 app!

在编写这个 app 的过程中,你将学到:

  • 配置开发环境
  • 创建项目
  • 热加载
  • 导入文件和包
  • widget 的创建和使用
  • 网络调用
  • 在列表中显示内容
  • 添加 app 主题

同时,你还会学到一点 Dart :]

开始

可以在 macOS 、Linux 或者 windows 中进行 Flutter 开发。你可以用任意编辑器 + Flutter 工具链开发,也可以使用和 IntelliJ IDEA、、Android 和 Visual Studio Code 配套的 IDE 插件来让开发更轻松。本教程中使用的是 Visual Studio Code。

配置开发环境

配置 Flutter 开发环境的详细指南请看这里。它的基本步骤因平台而异,但大部分都包括:

  1. 克隆 Flutter git 库
  2. 将 Flutter 的 bin 目录添加到 path 环境变量
  3. 运行 flutter doctor 命令,这会安装 Flutter 框架,包括 Dart,并提示你缺少的依赖项
  4. 安装缺失的依赖项
  5. 在你的 IDE 中安装 Flutter 插件/扩展
  6. 运行测试 app

在 Flutter 网站上的指南很详细,让你很容易就根据你的平台上配置好开发环境。本文接下来会假设你的 VSCode 已经为 Flutter 开发配置好了,同时通过 flutter doctor 解决了所有的问题。

如果你使用 Android Studio 的话,请确定你能够跟上进度。你还需要 iOS 模拟器、Android 模拟器或者拥有一台激活的 iOS 设备,或者可用于开发的 Android 设备。

注:要在 iOS 模拟器或 iOS 设备上进行编译和调试,你必须使用 macOS 并装好 Xcode。

创建项目

在 VSCode 中安装好 Flutter extension 后,点击 View > Command Palette… 菜单或快捷键 cmd+shift+P (macOS) 或 ctrl+shift+P(Linux 或 Windows) 打开命令面板。在命令面板中输入 Flutter: New Project 然后回车 。

项目名称输入 ghflutter,回车。选择项目文件夹,等待 Flutter 在 VSCode 中创建项目。创建好项目后,maid.dart 会在编辑器中打开。

在 VSCode 中,你会看到左边有一个面板,显示了项目结构。有 iOS 和 Android 两个目录,以及一个包含了 main.dart 以及应用到两个平台的 lib 目录。在本教程中,你只用到 lib 目录。

将 main.dart 修改为:

import 'package:flutter/material.dart';

void main() => runApp(new GHFlutterApp());


class GHFlutterApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'GHFlutter',
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text('GHFlutter'),
        ),
        body: new Center(
          child: new Text('GHFlutter'),
        ),
      ),
    );
  }
}

在顶部,main() 函数用到了 => 运算符,因为它只有一行语句,就是运行这个app。然后是一个名为 GHFlutterApp 的类。

看到没,这个 app 自身也是一个 StatelessWidget。在 flutter app 中大部分对象都是 widget,无状态的或者有状态的。你覆盖了 widget 的 build() 方法,用以创建一个 app widget。然后用一个 MaterialApp widget 提供材料设计中会用到的大量组件。

在本教程一开始,需要删除测试文件 widget_test.dart,请选择它并用 delete 键从项目中删除它。

如果你使用 macOS,请启动 iOS 模拟器。你以可以使用 Android 模拟器,它支持 macOS、Linux 或 Windows。 如果 iOS 模拟器和 Android 模拟器同时在运行,你可以用 VSCode 右下角的菜单切换它们:

点击 F5 或者 Debug > Start Debugging,Build & run 项目。Debug 控制台会打开,如果运行在 iOS 上,会用 Xcode 来编译项目。如果在 Android 中运行,则会调用 Gradle 来编译。

在 iOS 模拟器中运行 app:

在 Android 模拟器中运行:

“Slow mode” 标签表明 app 正在以调试模式运行。

点击工具栏右边的 stop 按钮可以终止 app:

通过 VSCode 左上角的图标(或者 View > Explorer)返回项目视图。

热加载

Flutter 开发最好用的一个功能是在修改 app 之后热加载 app。这就类似于 Android Studio 的 Instant Run。

Build & run,让 app 在模拟器上运行:

现在,不要停止 app,修改 bar 上的字符串:

appBar: new AppBar(
  title: new Text('GHFlutter App'),
)

然后点击工具栏中的热加载按钮:

1-2 秒之后,你会看到正在运行的 app 变成:

由于 Flutter 当前是 beta 版,热加载功能可能不是很稳定,但在构建 UI 时这真的节省了许多时间。

导入文件

除了将所有的 Dart 代码都放在一个 main.dart 文件中外,你也可以从其它类中导入代码。现在来看一个导入字符串的例子,这可以用对用户看到的字符串进行本地化时。

右键点击 lib 文件夹,选择 New File,在lib 文件夹中新建一个文件,叫做 strings.dart。

在文件中声明类:

class Strings {
  static String appTitle = "GHFlutter";
}

在 main.dart 顶部加入 import 语句:

import 'strings.dart';

修改 app 的标题:

return new MaterialApp(
  title: Strings.appTitle,

让其它字符串也改成使用新的 strings 文件中的值,修改后的 GHFlutterApp 类变成:

class GHFlutterApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: Strings.appTitle,
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text(Strings.appTitle),
        ),
        body: new Center(
          child: new Text(Strings.appTitle),
        ),
      ),
    );
  }
}

按下 F5,Build & run。你会发现什么也没改变,但其实你已经将字符串的内容变成了 string 文件中的值了。

Widgets

基本上 app 中的每个元素都是一个 widget。Widget 被设计为不可变的,因为不可变的 widget 有助于让 app 的 UI 保持“轻量”。

你会用到两种基本的 widget:

  • 无状态的(Stateless):widget 只依赖于它们自己的配置信息,比如 image view 中的静态图片。
  • 有状态的(Stateful): widget 必须维护动态信息,并和某个状态对象交互。

两种 widget 都会在每一帧时进行重绘。不同的是有状态 widget 将它们的属性委托给一个状态对象。

来创建一个自己的 widget,在 main.dart 中添加一个类:

class GHFlutter extends StatefulWidget {
  @override
  createState() => new GHFlutterState();
}

你创建了一个 StatefulWidget 的子类并在重载方法 createState() 中,该方法用于创建状态对象。接着在 GHFlutter 上面添加一个 GHFlutterState 类:

class GHFlutterState extends State<GHFlutter> {
}

GHFlutterState 继承了 State,同时有一个 GHFlutter 泛型参数。

在编写 widget 时,最重要的是重写 build() 方法,当 widget 渲染到屏幕时会调用这个方法。

重写 GHFlutterState 的 build() 方法:

@override
Widget build(BuildContext context) {
​    
}

修改 build() 方法为:

@override
Widget build(BuildContext context) {
  return new Scaffold (
    appBar: new AppBar(
      title: new Text(Strings.appTitle),
    ),
    body: new Text(Strings.appTitle),
  );
}

Scaffold 是一个容器,用于材料设计的 widget。它充当 widget 层级树中的 root。你可以加一个 AppBar 和一个 body,然后让它们都包含一个 Text widget。

修改 GHFlutterApp ,让用 GHFlutter widget 作为它的 home 属性, 而不是用一个scaffold:

class GHFlutterApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: Strings.appTitle,
      home: new GHFlutter(),
    );
  }
}

Build & run,你会看到新的 widget:

没有太多不一样的地方,但现在你使用了新的 widget。

网络调用

先前你将 strings.dart 导入到项目中。你同样可以导入在 Flutter 框架和 Dart 中的其它包。

例如,你将用到一个用于创建 HTTP 网络请求并将 JSON 解析成 Dart 对象的包。在 main.dart 顶部加入两句 import 语句:

import 'dart:convert';

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

import 'strings.dart';

你会看到在 import 语句上有警告,告诉你这个包当前为用到。

Dart app 是单线程的,但 Dart 通过 async/await 模式提供了在其它线程中执行代码,以及不会阻塞 UI 线程异步执行代码的支持。

你将通过异步网络调用来获取 GitHub 团队成员列表。在 GHFlutterState 中添加一个 list 属性,以及一个 TextSytle 属性。

var _members = [];

final _biggerFont = const TextStyle(fontSize: 18.0);

成员变量前面的下划线表示这是私有属性。

来创建异步请求 HTTP。在 GHFlutterState 中添加一个方法 _loadData()

_loadData() async {
  String dataURL = "https://api.github.com/orgs/raywenderlich/members";
  http.Response response = await http.get(dataURL);
  setState(() {
    _members = JSON.decode(response.body);
  });
}

在 _loadData() 中使用了 async 关键字,用于告诉 Dart 它是异步方法,http.get() 前面的 await 关键字则表明这是阻塞调用。dataUrl 设置为 GitHub API 的端点,这个端点用于从 GitHub 组织获取成员。

在 HTTP 请求之后,你可以向 setStat() 中传递一个回调,这个方法是在 UI 线程中异步执行的。在这个回调中,你解析了 JSON 并向 _memebers list 赋值。

为 GHFlutterState 添加一个覆盖方法 initState(),在方法中,当状态被初始化后,调用 _loadData() 方法:

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

  _loadData();
}

ListView 的使用

在拥有了 members 之后,需要在 UI 中将它们显示到列表。Dart 提供了一个 ListView widget,允许你显示列表数据。ListView 类似于 Android 中的 RecyclerView 和 iOS 中的 UITableView,当用户在列表中滚动时,会重用视图,以实现平滑滚动的性能。

在 GHFlutterState 中添加 _buildRow() 方法:

Widget _buildRow(int i) {
  return new ListTile(
    title: new Text("${_members[i]["login"]}", style: _biggerFont)
  );
}

你现在返回了一个 ListTile widget,用于显示第 i 个成员的 login 值,这里用到了之前创建的 TextStyle。

修改 build 方法,将 body 修改为一个 ListView.builder:

body: new ListView.builder(
  padding: const EdgeInsets.all(16.0),
  itemCount: _members.length,
  itemBuilder: (BuildContext context, int position) {
    return _buildRow(position);
  }),

这里添加了 padding,将 itemCount 设置为 memebers 的成员个数,对于指定的 position,调用 _buildRow() 方法来设置 itemBuilder。

你可以测试一下热加载,但你会得到一个 “Full restart may be required” 的消息。如果这样,请点击 F5,buid & run 这个 app:

创建网络调用、解析数据、在表格中显示结果就是这样简单!

添加分隔线

要向表格中加入分隔线,你需要让 item 数翻倍,然后当 position 为奇数时返回一个 Divider widget:

body: new ListView.builder(
  itemCount: _members.length * 2,
  itemBuilder: (BuildContext context, int position) {
    if (position.isOdd) return new Divider();

    final index = position ~/ 2;

    return _buildRow(index);
  }),

确保 itemCount 属性被乘以 2。你现在删除了 padding,因为现在已经有分隔线了。在 itemBuider 中,你要么返回一个 Divider();要么整除 2 获得新的索引然后调用 _buildRow() 来创建 item。

进行热加载,你可以看到分隔线了:

想将 padding 重新放回每一行中,可以在 _buildRow() 方法中使用一个 Padding widget:

Widget _buildRow(int i) {
  return new Padding(
    padding: const EdgeInsets.all(16.0),
    child: new ListTile(
      title: new Text("${_members[i]["login"]}", style: _biggerFont)
    )
  );
}

ListTile 现在是 padding widget 的子 widget。热加载,看一下效果。

解析自定义类型

在上一节中,JSON 解析器将 JSON 中的每个成员以 Dart Map 类型添加到 _memeber list。该类型等同于 Kotlin 中的 Map 或者 Swift 中的字典。

但是,你可能更想用自定义类型。
在 main.dart 文件中添加一个 Member 类型:

class Member {
  final String login;

  Member(this.login) {
    if (login == null) {
      throw new ArgumentError("login of Member cannot be null. "
          "Received: '$login'");
    }
  }
}

Member 有一个 login 属性和一个当 login 为空时会抛出 error 的构造器。

修改 GHFlutterState 中的 _members,将它改成 Memeber 对象数组:

var _members = <Member>[];

修改 _buildRow() 方法,用 Memeber 对象的 login 属性来代替 Map 的 login 键:

title: new Text("${_members[i].login}", style: _biggerFont)

现在修改 _loadData() 方法的 setState() 方法调用,将 map 转换成 Memeber 对象并添加到 members 数组中:

setState(() {
  final membersJSON = JSON.decode(response.body);

  for (var memberJSON in membersJSON) {
    final member = new Member(memberJSON["login"]);
    _members.add(member);
  }
});

如果进行热加载,你可能会得到一个错误,但当你点击 F5 build & run,你会看到和之前一样的效果,但这次你使用了 Member 类。

用 NetworkImage 下载图片

每个成员都一个 URL 表示他们的头像。在 Member 类中添加一个头像属性并显示出来。

修改 Member 类,添加一个不能为空的 avatarUrl 属性:

class Member {
  final String login;
  final String avatarUrl;

  Member(this.login, this.avatarUrl) {
    if (login == null) {
      throw new ArgumentError("login of Member cannot be null. "
          "Received: '$login'");
    }
    if (avatarUrl == null) {
      throw new ArgumentError("avatarUrl of Member cannot be null. "
          "Received: '$avatarUrl'");
    }
  }
}

修改 _buildRow() 方法,用一个 NetworkImage 和 CircleAvatar widget 显示头像:

Widget _buildRow(int i) {
  return new Padding(
    padding: const EdgeInsets.all(16.0),
    child: new ListTile(
      title: new Text("${_members[i].login}", style: _biggerFont),
      leading: new CircleAvatar(
        backgroundColor: Colors.green,
        backgroundImage: new NetworkImage(_members[i].avatarUrl)
      ),
    )
  );
}

将 ListTile 的 leading 属性设置为头像,会在行的 title 之前显示头像。用 Colors 类设置图片的背景色。

现在来修改 _loadData(),在实例化 Member 对象时使用 map 的 avatar_url 值。

final member = new Member(memberJSON["login"], memberJSON["avatar_url"]);

Build & run,你会看到每一行都显示了头像:

整理代码

现在大部分代码都放在了 main.dart。要让代码更加清晰,可以将 widget 进行重构,将其它类放在自己的文件中。

在 lib 文件夹下新建文件 member.dart、ghflutter.dart。将 Member 类移到 member.dart,GHFlutterState 和 GHFlutter 类移到 ghflutter.dart。

不需要在 member.dart 中使用任何 import 语句,但在 ghflutter.dart 中需要添加下列 import 语句:

import 'dart:convert';

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

import 'member.dart';
import 'strings.dart';  

还要修改 main.dart 中的 import 语句,这下整个文件变成:

import 'package:flutter/material.dart';

import 'ghflutter.dart';
import 'strings.dart';

void main() => runApp(new GHFlutterApp());


class GHFlutterApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: Strings.appTitle,
      home: new GHFlutter(),
    );
  }
}

Build & run,你会发现没有任何改变,但代码变得更清晰了。

添加主题

你可以在 main.dart 中为 MaterialApp 添加一个 theme 属性,为 app 添加主题:

class GHFlutterApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: Strings.appTitle,
      theme: new ThemeData(primaryColor: Colors.green.shade800), 
      home: new GHFlutter(),
    );
  }
}  

用淡绿色作为主题的材料设计颜色。

Build & run,看一下新主题:

多数截图都是 Android 模拟器的。你也可以在 iOS 模拟器中运行主题化后的 app:

这就是我说的跨平台:]

Dart 2

Dart 有一个正在开发中的新版本,Dart 2。另外,Dart 2 增加了强编译时检查,并去掉了创建新对象时必须使用的 new 关键字。

例如,在 Dart 2 中,_buildRow() 方法应该是这样:

_buildRow(int i) =>
  Padding(
    padding: EdgeInsets.all(16.0),
    child: ListTile(
        title: Text("${_members[i].login}", style: _biggerFont),
        leading: CircleAvatar(
          backgroundColor: Colors.green,
          backgroundImage: NetworkImage(_members[i].avatarUrl)
      ),
    )
  );

这里是在 Flutter 中试用 Dart 2 的指南。有可能它和当前项目不兼容,如果想尝试 Dart 2,请用这个命令:

flutter analyze --preview-dart-2

以及这个命令:

flutter run --preview-dart-2

进行确认。

接下来去哪里?

你可以在本文头部或底部找到完整项目下载链接。

可以用 VSCode 或 Android Studio 打开项目。用 VSCode 打开只需要打开根文件夹。在打开项目时你需要下载 package。

如果用 Android Studio 打开这个项目,请用欢迎屏的 Open an existing Android Studio project,然后选择项目的根文件夹。然后选择 Open Flutter Settings 并选择你从 git repo 上克隆下来 Flutter SDK 所在路径。然后选择“‘Packages get’ has not been run “一行中的 Get dependencies。在 project 面板中选择 Project 视图,就可以看到你的文件。

如果你使用的是 macOS,你可以通过 Android Studio 将 app 运行到 Android 模拟器或 iOS 模拟器上。试一下,这真的很棒:]

关于 Flutter 和 Dart 还有很多内容需要学习。最好从这里开始学习:

  • flutter.io 的 Flutter 主页。你可以找到大量文档和信息。
  • 这里是可用的 widget。
  • 这里是一篇从 Android 开发者转为 Flutter 开发的很好的指南。
  • 这里是给 React Native 开发者的指南。

请继续关注其它 Flutter 教程和屏播!

请在下面或论坛中分享你的心得体会或者提问。希望你喜欢本教程!

Download Materials

阅读更多
换一批

没有更多推荐了,返回首页