Flutter基础

535 阅读37分钟

1基础介绍

1.1简洁

Flutter是谷歌的移动UI框架,可以快速在iOS和Android上构建高质量的原生用户界面。 Flutter可以与现有的代码一起工作。在全世界,Flutter正在被越来越多的开发者和组织使用,并且Flutter是完全免费、开源的。

1.2特点

  • 快速开发:Flutter的热重载可帮助您快速地进行测试、构建UI、添加功能并更快地修复错误。在iOS和Android模拟器或真机上可以在亚秒内重载,并且不会丢失状态。

  • 富有表现力和灵活的UI:使用Flutter内置美丽的Material Design和Cupertino(iOS风格)widget、丰富的motion API、平滑而自然的滑动效果和平台感知,为用户带来全新体验。

  • 现代的响应式框架:使用Flutter的现代、响应式框架,和一系列基础widget,轻松构建您的用户界面。使用功能强大且灵活的API(针对2D、动画、手势、效果等)解决艰难的UI挑战。

  • 访问本地功能和SDK:通过平台相关的API、第三方SDK和原生代码让应用变得强大易用。 Flutter允许复用现有的Java、Swift或ObjC代码,访问iOS和Android上的原生系统功能和系统SDK。

  • 统一的应用开发体验:Flutter拥有丰富的工具和库,可以帮助您轻松地同时在iOS和Android系统中实现您的想法和创意。 如果您没有任何移动端开发体验,Flutter是一种轻松快捷的方式来构建漂亮的移动应用程序。 如果您是一位经验丰富的iOS或Android开发人员,则可以使用Flutter作为视图(View)层, 并可以使用已经用Java / ObjC / Swift完成的部分(Flutter支持混合开发)。

1.3 跨平台解决方案

  • Flutter跨平台最核心的部分,是它的高性能引擎渲染(Flutter Engine)。Flutter不使用浏览器技术,也不使用Native的原生控件,它使用自己的渲染引擎来绘制widget。

  • widget是Flutter应用程序用户界面的基本构建块。每个widget都是用户界面一部分的不可变声明。与其他将视图、控制器、布局和其他属性分离的框架不同,Flutter具有一致的统一对象模型:widget。在更新widget的时候,框架能够更加的高效。 对于Android平台,Flutter引擎的C/C++代码是由NDK编译,在iOS平台,则是由LLVM编译,两个平台的Dart代码都是AOT编译为本地代码,Flutter应用使用本机指令集运行。

  • Flutter正是通过使用相同的渲染器、框架和一组widget,来同时构建iOS和Android应用,而无需维护两套独立的代码。

  • Flutter将UI和渲染器从平台移动到应用程序中,这使得它们可以自定义和可扩展。Flutter唯一要求系统提供的是canvas,以便定制的UI组件可以出现在设备的屏幕上。

1.4 环境搭建

flutterchina.club/setup-windo…

2第一个 Flutter App

2.1功能

为一个创业公司生成建议的名称。用户可以选择和取消选择的名称、保存(收藏)喜欢的名称。该代码一次生成十个名称,当用户滚动时,会生成一新批名称。用户可以点击导航栏右边的列表图标,以打开到仅列出收藏名称的新页面。

2.2你能学到什么

  • Flutter应用程序的基本结构.

  • 查找和使用packages来扩展功能.

  • 使用热重载加快开发周期.

  • 如何实现有状态的widget.

  • 如何创建一个无限的、延迟加载的列表.

  • 如何创建并导航到第二个页面.

  • 如何使用主题更改应用程序的外观.

2.3代码

2.3.1 创建 Flutter app

替换 lib/main.dart

import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Welcome to Flutter',
      home: new Scaffold(
        appBar: new AppBar(
          title: new Text('Welcome to Flutter'),
        ),
        body: new Center(
          child: new Text('Hello World'),
        ),
      ),
    );
  }
}

分析

  • 本示例创建一个Material APP。Material是一种标准的移动端和web端的视觉设计语言。 Flutter提供了一套丰富的Material widgets。

  • main函数使用了(=>)符号, 这是Dart中单行函数或方法的简写。

  • 该应用程序继承了 StatelessWidget,这将会使应用本身也成为一个widget。 在Flutter中,大多数东西都是widget,包括对齐(alignment)、填充(padding)和布局(layout)

  • Scaffold 是 Material library 中提供的一个widget, 它提供了默认的导航栏、标题和包含主屏幕widget树的body属性。widget树可以很复杂。

  • widget的主要工作是提供一个build()方法来描述如何根据其他较低级别的widget来显示自己。

  • 本示例中的body的widget树中包含了一个Center widget, Center widget又包含一个 Text 子widget。 Center widget可以将其子widget树对其到屏幕中心。

2.3.2使用外部包(package)

  1. pubspec文件管理Flutter应用程序的assets(资源,如图片、package等)。 在pubspec.yaml中,将english_words(3.1.0或更高版本)添加到依赖项列表,如下所示:

  1. 在Android Studio的编辑器视图中查看pubspec时,单击右上角的 Packages get,这会将依赖包安装到项目。可以在控制台中看到以下内容:

  1. 在 lib/main.dart 中, 引入 english_words, 如高亮显示的行所示:

  1. 使用 English words 包生成文本来替换字符串“Hello World”.

  1. 如果应用程序正在运行,请使用热重载按钮 (lightning bolt icon) 更新正在运行的应用程序。每次单击热重载或保存项目时,都会在正在运行的应用程序中随机选择不同的单词对。 这是因为单词对是在 build 方法内部生成的。每次MaterialApp需要渲染时或者在Flutter Inspector中切换平台时 build 都会运行.

2.3.3添加一个 有状态的部件(Stateful widget)

  • Stateless widgets 是不可变的, 这意味着它们的属性不能改变 - 所有的值都是最终的.

  • Stateful widgets 持有的状态可能在widget生命周期中发生变化. 实现一个 stateful widget 至少需要两个类:

    • 一个 StatefulWidget类。
    • 一个 State类。 StatefulWidget类本身是不变的,但是 State类在widget生命周期中始终存在.

在这一步中,将添加一个有状态的widget-RandomWords,它创建其State类RandomWordsState。State类将最终为widget维护建议的和喜欢的单词对。

  1. 添加有状态的 RandomWords widget 到 main.dart。 它也可以在MyApp之外的文件的任何位置使用,但是本示例将它放到了文件的底部。RandomWords widget除了创建State类之外几乎没有其他任何东西

  2. 添加 RandomWordsState 类.该应用程序的大部分代码都在该类中, 该类持有RandomWords widget的状态。这个类将保存随着用户滚动而无限增长的生成的单词对, 以及喜欢的单词对,用户通过重复点击心形 ❤️ 图标来将它们从列表中添加或删除。

一步一步地建立这个类。首先,通过添加高亮显示的代码创建一个最小类

  1. 在添加状态类后,IDE会提示该类缺少build方法。接下来,将添加一个基本的build方法,该方法通过将生成单词对的代码从MyApp移动到RandomWordsState来生成单词对。

将build方法添加到RandomWordState中,如下面高亮代码所示

  1. 通过下面高亮显示的代码,将生成单词对代的码从MyApp移动到RandomWordsState中

应用程序应该像之前一样运行,每次热重载或保存应用程序时都会显示一个单词对。

2.3.4创建一个无限滚动ListView

在这一步中,将扩展(继承)RandomWordsState类,以生成并显示单词对列表。 当用户滚动时,ListView中显示的列表将无限增长。 ListView的builder工厂构造函数允许按需建立一个懒加载的列表视图。

  1. 向RandomWordsState类中添加一个_suggestions列表以保存建议的单词对。 该变量以下划线(_)开头,在Dart语言中使用下划线前缀标识符,会强制其变成私有的。

另外,添加一个biggerFont变量来增大字体大小

  1. 向RandomWordsState类添加一个 _buildSuggestions() 函数. 此方法构建显示建议单词对的ListView。

ListView类提供了一个builder属性,itemBuilder 值是一个匿名回调函数, 接受两个参数- BuildContext和行迭代器i。迭代器从0开始, 每调用一次该函数,i就会自增1,对于每个建议的单词对都会执行一次。该模型允许建议的单词对列表在用户滚动时无限增长。

添加如下高亮的行:

  1. 对于每一个单词对,_buildSuggestions函数都会调用一次_buildRow。 这个函数在ListTile中显示每个新词对,在下一步中可以生成更漂亮的显示行

在RandomWordsState中添加一个_buildRow函数 :

  1. 更新RandomWordsState的build方法以使用_buildSuggestions(),而不是直接调用单词生成库。 更改后如下面高亮部分:

  1. 更新MyApp的build方法。从MyApp中删除Scaffold和AppBar实例。 这些将由RandomWordsState管理,这使得用户在下一步中从一个屏幕导航到另一个屏幕时, 可以更轻松地更改导航栏中的的路由名称。

用下面高亮部分替换最初的build方法:

重新启动应用程序。你应该看到一个单词对列表。尽可能地向下滚动,将继续看到新的单词对。

2.3.5添加交互

在这一步中,将为每一行添加一个可点击的心形 ❤️ 图标。当用户点击列表中的条目,切换其“收藏”状态时,将该词对添加到或移除出“收藏夹”。

  1. 添加一个 _saved Set(集合) 到RandomWordsState。这个集合存储用户喜欢(收藏)的单词对。 在这里,Set比List更合适,因为Set中不允许重复的值。

  1. 在 _buildRow 方法中添加 alreadySaved来检查确保单词对还没有添加到收藏夹中。

  1. 同时在 _buildRow()中, 添加一个心形 ❤️ 图标到 ListTiles以启用收藏功能。接下来,你就可以给心形 ❤️ 图标添加交互能力了。

添加下面高亮的行:

  1. 重新启动应用。你现在可以在每一行看到心形❤️图标️,但它们还没有交互。
  2. 在 _buildRow中让心形❤️图标变得可以点击。如果单词条目已经添加到收藏夹中, 再次点击它将其从收藏夹中删除。当心形❤️图标被点击时,函数调用setState()通知框架状态已经改变。

提示: 在Flutter的响应式风格的框架中,调用setState() 会为State对象触发build()方法,从而导致对UI的更新

热重载你的应用。你就可以点击任何一行收藏或移除。请注意,点击一行时会生成从心形 ❤️ 图标发出的水波动画

2.3.6导航到新页面

在这一步中,将添加一个显示收藏夹内容的新页面(在Flutter中称为路由(route))。将学习如何在主路由和新路由之间导航(切换页面)。

在Flutter中,导航器管理应用程序的路由栈。将路由推入(push)到导航器的栈中,将会显示更新为该路由页面。 从导航器的栈中弹出(pop)路由,将显示返回到前一个路由。

  1. 在RandomWordsState的build方法中为AppBar添加一个列表图标。当用户点击列表图标时,包含收藏夹的新路由页面入栈显示。

提示: 某些widget属性需要单个widget(child),而其它一些属性,如action,需要一组widgets(children),用方括号[]表示。

将该图标及其相应的操作添加到build方法中:

  1. 向RandomWordsState类添加一个 _pushSaved() 方法.

  1. 当用户点击导航栏中的列表图标时,建立一个路由并将其推入到导航管理器栈中。此操作会切换页面以显示新路由。 新页面的内容在在MaterialPageRoute的builder属性中构建,builder是一个匿名函数。

    添加Navigator.push调用,这会使路由入栈(以后路由入栈均指推入到导航管理器的栈)

  1. 添加MaterialPageRoute及其builder。 现在,添加生成ListTile行的代码。ListTile的divideTiles()方法在每个ListTile之间添加1像素的分割线。 该 divided 变量持有最终的列表项。

  1. builder返回一个Scaffold,其中包含名为“Saved Suggestions”的新路由的应用栏。 新路由的body由包含ListTiles行的ListView组成; 每行之间通过一个分隔线分隔。

    添加如下高亮的代码:

  1. 热重载应用程序。收藏一些选项,并点击应用栏中的列表图标,在新路由页面中显示收藏的内容。 请注意,导航器会在应用栏中添加一个“返回”按钮。你不必显式实现Navigator.pop。点击后退按钮返回到主页路由。

2.3.7使用主题更改UI

在这最后一步中,将会使用主题。主题控制应用程序的外观和风格。可以使用默认主题,该主题取决于物理设备或模拟器,也可以自定义主题以适应品牌。

  1. 可以通过配置ThemeData类轻松更改应用程序的主题。 应用程序目前使用默认主题,下面将更改primary color颜色为白色。

通过如下高亮部分代码,将应用程序的主题更改为白色:

  1. 热重载应用。 请注意,整个背景将会变为白色,包括应用栏。
  2. 作为一个练习,使用 ThemeData 来改变UI的其他方面。 Material library中的 Colors类提供了许多可以使用的颜色常量, 你可以使用热重载来快速简单地尝试、实验。

3项目管理

3.1路由管理

路由(Route)在移动开发中通常指页面(Page),这跟web开发中单页应用的Route概念意义是相同的,Route在Android中通常指一个Activity,在iOS中指一个ViewController。所谓路由管理,就是管理页面之间如何跳转,通常也可被称为导航管理。Flutter中的路由管理和原生开发类似,无论是Android还是iOS,导航管理都会维护一个路由栈,路由入栈(push)操作对应打开一个新页面,路由出栈(pop)操作对应页面关闭操作,而路由管理主要是指如何来管理路由栈。

3.1.1一个简单示例

  1. 创建一个新路由,命名“NewRoute”

新路由继承自StatelessWidget,界面很简单,在页面中间显示一句"This is new route"。

  1. 在_MyHomePageState.build方法中的Column的子widget中添加一个按钮(FlatButton)

我们添加了一个打开新路由的按钮,并将按钮文字颜色设置为蓝色,点击该按钮后就会打开新的路由页面,效果如图2-2和2-3所示。

3.1.2MaterialPageRoute

MaterialPageRoute继承自PageRoute类,PageRoute类是一个抽象类,表示占有整个屏幕空间的一个模态路由页面,它还定义了路由构建及切换时过渡动画的相关接口及属性。MaterialPageRoute 是Material组件库提供的组件,它可以针对不同平台,实现与平台页面切换动画风格一致的路由切换动画:

对于Android,当打开新页面时,新的页面会从屏幕底部滑动到屏幕顶部;当关闭页面时,当前页面会从屏幕顶部滑动到屏幕底部后消失,同时上一个页面会显示到屏幕上。 对于iOS,当打开页面时,新的页面会从屏幕右侧边缘一致滑动到屏幕左边,直到新页面全部显示到屏幕上,而上一个页面则会从当前屏幕滑动到屏幕左侧而消失;当关闭页面时,正好相反,当前页面会从屏幕右侧滑出,同时上一个页面会从屏幕左侧滑入。 下面我们介绍一下MaterialPageRoute 构造函数的各个参数的意义:

  • builder 是一个WidgetBuilder类型的回调函数,它的作用是构建路由页面的具体内容,返回值是一个widget。我们通常要实现此回调,返回新路由的实例。

  • settings 包含路由的配置信息,如路由名称、是否初始路由(首页)。

  • maintainState:默认情况下,当入栈一个新路由时,原来的路由仍然会被保存在内存中,如果想在路由没用的时候释放其所占用的所有资源,可以设置maintainState为false。

  • fullscreenDialog表示新的路由页面是否是一个全屏的模态对话框,在iOS中,如果fullscreenDialog为true,新页面将会从屏幕底部滑入(而不是水平方向)。

如果想自定义路由切换动画,可以自己继承PageRoute来实现,我们将在后面介绍动画时,实现一个自定义的路由组件。

3.1.3Navigator

Navigator是一个路由管理的组件,它提供了打开和退出路由页方法。Navigator通过一个栈来管理活动路由集合。通常当前屏幕显示的页面就是栈顶的路由。Navigator提供了一系列方法来管理路由栈,在此我们只介绍其最常用的两个方法:

  • Future push(BuildContext context, Route route)

    将给定的路由入栈(即打开新的页面),返回值是一个Future对象,用以接收新路由出栈(即关闭)时的返回数据。

  • bool pop(BuildContext context, [ result ])

    将栈顶路由出栈,result为页面关闭时返回给上一个页面的数据。

实例方法

Navigator类中第一个参数为context的静态方法都对应一个Navigator的实例方法, 比如Navigator.push(BuildContext context, Route route)等价于Navigator.of(context).push(Route route) ,下面命名路由相关的方法也是一样的。

3.1.4路由传值

很多时候,在路由跳转时我们需要带一些参数,比如打开商品详情页时,我们需要带一个商品id,这样商品详情页才知道展示哪个商品信息;又比如我们在填写订单时需要选择收货地址,打开地址选择页并选择地址后,可以将用户选择的地址返回到订单页等等。下面我们通过一个简单的示例来演示新旧路由如何传参。

下面是打开新路由TipRoute的代码:

运行上面代码,点击RouterTestRoute页的“打开提示页”按钮,会打开TipRoute页,运行效果如图所示:

需要说明:

  1. 提示文案“我是提示xxxx”是通过TipRoute的text参数传递给新路由页的。我们可以通过等待Navigator.push(…)返回的Future来获取新路由的返回数据。
  2. 在TipRoute页中有两种方式可以返回到上一页;第一种方式时直接点击导航栏返回箭头,第二种方式是点击页面中的“返回”按钮。这两种返回方式的区别是前者不会返回数据给上一个路由,而后者会。下面是分别点击页面中的返回按钮和导航栏返回箭头后,RouterTestRoute页中print方法在控制台输出的内容:

3.1.5 命名路由

所谓“命名路由”(Named Route)即有名字的路由,我们可以先给路由起一个名字,然后就可以通过路由名字直接打开新的路由了,这为路由管理带来了一种直观、简单的方式。

  • 路由表

要想使用命名路由,我们必须先提供并注册一个路由表(routing table),这样应用程序才知道哪个名字与哪个路由组件相对应。其实注册路由表就是给路由起名字,路由表的定义如下:

它是一个Map,key为路由的名字,是个字符串;value是个builder回调函数,用于生成相应的路由widget。我们在通过路由名字打开新路由时,应用会根据路由名字在路由表中查找到对应的WidgetBuilder回调函数,然后调用该回调函数生成路由widget并返回。

  • 注册路由表

路由表的注册方式很简单,我们回到之前“计数器”的示例,然后在MyApp类的build方法中找到MaterialApp,添加routes属性,代码如下:

现在我们就完成了路由表的注册。上面的代码中home路由并没有使用命名路由,如果我们也想将home注册为命名路由应该怎么做呢?其实很简单,直接看代码:

可以看到,我们只需在路由表中注册一下MyHomePage路由,然后将其名字作为MaterialApp的initialRoute属性值即可,该属性决定应用的初始路由页是哪一个命名路由。

  • 通过路由名打开新路由页

要通过路由名称来打开新路由,可以使用Navigator 的pushNamed方法:

Navigator 除了pushNamed方法,还有pushReplacementNamed等其他管理命名路由的方法,可以自行查看API文档。接下来我们通过路由名来打开新的路由页,修改FlatButton的onPressed回调代码,改为:

  • 命名路由参数传递

在Flutter最初的版本中,命名路由是不能传递参数的,后来才支持了参数;下面展示命名路由如何传递并获取路由参数:

我们先注册一个路由:

在路由页通过RouteSetting对象获取路由参数:

在打开路由时传递参数

  • 适配

假设我们也想将上面路由传参示例中的TipRoute路由页注册到路由表中,以便也可以通过路由名来打开它。但是,由于TipRoute接受一个text 参数,我们如何在不改变TipRoute源码的前提下适配这种情况?其实很简单:

3.1.6 路由生成钩子

假设我们要开发一个电商APP,当用户没有登录时可以看店铺、商品等信息,但交易记录、购物车、用户个人信息等页面需要登录后才能看。为了实现上述功能,我们需要在打开每一个路由页前判断用户登录状态!如果每次打开路由前我们都需要去判断一下将会非常麻烦,那有什么更好的办法吗?答案是有!

MaterialApp有一个onGenerateRoute属性,它在打开命名路由时可能会被调用,之所以说可能,是因为当调用Navigator.pushNamed(...)打开命名路由时,如果指定的路由名在路由表中已注册,则会调用路由表中的builder函数来生成路由组件;如果路由表中没有注册,才会调用onGenerateRoute来生成路由。onGenerateRoute回调签名如下:

有了onGenerateRoute回调,要实现上面控制页面权限的功能就非常容易:我们放弃使用路由表,取而代之的是提供一个onGenerateRoute回调,然后在该回调中进行统一的权限控制,如:

注意:onGenerateRoute只会对命名路由生效。

3.1.7路由总结

本章先介绍了Flutter中路由管理、传参的方式,然后又着重介绍了命名路由相关内容。在此需要说明一点,由于命名路由只是一种可选的路由管理方式,在实际开发中,可能心中会犹豫到底使用哪种路由管理方式。在此,根据笔者经验,建议最好统一使用命名路由的管理方式,这将会带来如下好处:

  • 语义化更明确。

  • 代码更好维护;如果使用匿名路由,则必须在调用Navigator.push的地方创建新路由页,这样不仅需要import新路由页的dart文件,而且这样的代码将会非常分散。

  • 可以通过onGenerateRoute做一些全局的路由跳转前置处理逻辑。 综上所述,笔者比较建议使用命名路由,当然这并不是什么金科玉律,可以根据自己偏好或实际情况来决定。

另外,还有一些关于路由管理的内容我们没有介绍,比如路由MaterialApp中还有navigatorObservers和onUnknownRoute两个回调属性,前者可以监听所有路由跳转动作,后者在打开一个不存在的命名路由时会被调用,由于这些功能并不常用,而且也比较简单,我们便不再花费篇幅来介绍了,可以自行查看API文档。

3.2 包管理

在软件开发中,很多时候有一些公共的库或SDK可能会被很多项目用到,因此,将这些代码单独抽到一个独立模块,然后哪个项目需要使用时再直接集成这个模块,便可大大提高开发效率。很多编程语言或开发工具都支持这种“模块共享”机制,如Java语言中这种独立模块会被打成一个jar包,Android中的aar包,Web开发中的npm包等。为了方便表述,我们将这种可共享的独立模块统一称为“包”( Package)。

一个APP在实际开发中往往会依赖很多包,而这些包通常都有交叉依赖关系、版本依赖等,如果由开发者手动来管理应用中的依赖包将会非常麻烦。因此,各种开发生态或编程语言官方通常都会提供一些包管理工具,比如在Android提供了Gradle来管理依赖,iOS用Cocoapods或Carthage来管理依赖,Node中通过npm等。而在Flutter开发中也有自己的包管理工具。本节我们主要介绍一下flutter如何使用配置文件pubspec.yaml(位于项目根目录)来管理第三方依赖包。

YAML是一种直观、可读性高并且容易被人类阅读的文件格式,它和xml或Json相比,它语法简单并非常容易解析,所以YAML常用于配置文件,Flutter也是用yaml文件作为其配置文件。Flutter项目默认的配置文件是pubspec.yaml,我们看一个简单的示例:

下面,我们逐一解释一下各个字段的意义:

  • name:应用或包名称。

  • description: 应用或包的描述、简介。

  • version:应用或包的版本号。

  • dependencies:应用或包依赖的其它包或插件。

  • dev_dependencies:开发环境依赖的工具包(而不是flutter应用本身依赖的包)。

  • flutter:flutter相关的配置选项。

如果我们的Flutter应用本身依赖某个包,我们需要将所依赖的包添加到dependencies 下,接下来我们通过一个例子来演示一下如何添加、下载并使用第三方包。

3.2.1Pub仓库

Pub(pub.dev/ )是Google官方的Dart Packages仓库,类似于node中的npm仓库,android中的jcenter。我们可以在Pub上面查找我们需要的包和插件,也可以向Pub发布我们的包和插件。我们将在后面的章节中介绍如何向Pub发布我们的包和插件。

3.2.2示例

接下来,我们实现一个显示随机字符串的widget。有一个名为“english_words”的开源软件包,其中包含数千个常用的英文单词以及一些实用功能。我们首先在pub上找到english_words这个包(如图2-5所示),确定其最新的版本号和是否支持Flutter。

我们看到“english_words”包最新的版本是3.1.3,并且支持flutter,接下来:

  1. 将“english_words”(3.1.3版本)添加到依赖项列表,如下:

  1. 下载包。在Android Studio的编辑器视图中查看pubspec.yaml时(图2-6),单击右上角的 Packages get 。

这会将依赖包安装到项目。我们可以在控制台中看到以下内容:

我们也可以在控制台,定位到当前工程目录,然后手动运行flutter packages get 命令来下载依赖包。另外,需要注意dependencies和dev_dependencies的区别,前者的依赖包将作为APP的源码的一部分参与编译,生成最终的安装包。而后者的依赖包只是作为开发阶段的一些工具包,主要是用于帮助我们提高开发、测试效率,比如flutter的自动化测试包等。

  1. 引入english_words包。

在输入时,Android Studio会自动提供有关库导入的建议选项。导入后该行代码将会显示为灰色,表示导入的库尚未使用。

  1. 使用english_words包来生成随机字符串。

我们将RandomWordsWidget 添加到 _MyHomePageState.build 的Column的子widget中。

  1. 如果应用程序正在运行,请使用热重载按钮(⚡️图标) 更新正在运行的应用程序。每次单击热重载或保存项目时,都会在正在运行的应用程序中随机选择不同的单词对。 这是因为单词对是在 build 方法内部生成的。每次热更新时,build方法都会被执行,运行效果如图所示。

3.2.3其它依赖方式

上文所述的依赖方式是依赖Pub仓库的。但我们还可以依赖本地包和git仓库。

  • 依赖本地包

如果我们正在本地开发一个包,包名为pkg1,我们可以通过下面方式依赖:

路径可以是相对的,也可以是绝对的。

  • 依赖Git:你也可以依赖存储在Git仓库中的包。如果软件包位于仓库的根目录中,请使用以下语法

上面假定包位于Git存储库的根目录中。如果不是这种情况,可以使用path参数指定相对位置,例如:
上面介绍的这些依赖方式是Flutter开发中常用的,但还有一些其它依赖方式,完整的内容可以自行查看:www.dartlang.org/tools/pub/d…

3.3 资源管理

Flutter APP安装包中会包含代码和 assets(资源)两部分。Assets是会打包到程序安装包中的,可在运行时访问。常见类型的assets包括静态数据(例如JSON文件)、配置文件、图标和图片(JPEG,WebP,GIF,动画WebP / GIF,PNG,BMP和WBMP)等。

3.3.1指定 assets

和包管理一样,Flutter也使用pubspec.yaml文件来管理应用程序所需的资源,举个例子:

assets指定应包含在应用程序中的文件, 每个asset都通过相对于pubspec.yaml文件所在的文件系统路径来标识自身的路径。asset的声明顺序是无关紧要的,asset的实际目录可以是任意文件夹(在本示例中是assets文件夹)。

在构建期间,Flutter将asset放置到称为 asset bundle 的特殊存档中,应用程序可以在运行时读取它们(但不能修改)。

3.3.2Asset 变体(variant)

构建过程支持“asset变体”的概念:不同版本的asset可能会显示在不同的上下文中。 在pubspec.yaml的assets部分中指定asset路径时,构建过程中,会在相邻子目录中查找具有相同名称的任何文件。这些文件随后会与指定的asset一起被包含在asset bundle中。

例如,如果应用程序目录中有以下文件:

  • …/pubspec.yaml

  • …/graphics/my_icon.png

  • …/graphics/background.png

  • …/graphics/dark/background.png

  • …etc.

然后pubspec.yaml文件中只需包含:

那么这两个graphics/background.png和graphics/dark/background.png 都将包含在asset bundle中。前者被认为是main asset (主资源),后者被认为是一种变体(variant)。

在选择匹配当前设备分辨率的图片时,Flutter会使用到asset变体(见下文),将来,Flutter可能会将这种机制扩展到本地化、阅读提示等方面。

3.3.3加载 assets

应用可以通过AssetBundle对象访问其asset 。有两种主要方法允许从Asset bundle中加载字符串或图片(二进制)文件。

3.3.4加载文本assets

  • 通过rootBundle 对象加载:每个Flutter应用程序都有一个rootBundle对象, 通过它可以轻松访问主资源包,直接使用package:flutter/services.dart中全局静态的rootBundle对象来加载asset即可。
  • 通过 DefaultAssetBundle 加载:建议使用 DefaultAssetBundle 来获取当前BuildContext的AssetBundle。 这种方法不是使用应用程序构建的默认asset bundle,而是使父级widget在运行时动态替换的不同的AssetBundle,这对于本地化或测试场景很有用。

通常,可以使用DefaultAssetBundle.of()在应用运行时来间接加载asset(例如JSON文件),而在widget上下文之外,或其它AssetBundle句柄不可用时,可以使用rootBundle直接加载这些asset,例如:

3.3.5加载图片

类似于原生开发,Flutter也可以为当前设备加载适合其分辨率的图像。

  • 声明分辨率相关的图片 assets

AssetImage 可以将asset的请求逻辑映射到最接近当前设备像素比例(dpi)的asset。为了使这种映射起作用,必须根据特定的目录结构来保存asset:

…/image.png
…/Mx/image.png
…/Nx/image.png
…etc.

其中M和N是数字标识符,对应于其中包含的图像的分辨率,也就是说,它们指定不同设备像素比例的图片。

主资源默认对应于1.0倍的分辨率图片。看一个例子:

…/my_icon.png
…/2.0x/my_icon.png
…/3.0x/my_icon.png

在设备像素比率为1.8的设备上,.../2.0x/my_icon.png 将被选择。对于2.7的设备像素比率,.../3.0x/my_icon.png将被选择。

如果未在Image widget上指定渲染图像的宽度和高度,那么Image widget将占用与主资源相同的屏幕空间大小。 也就是说,如果.../my_icon.png是72px乘72px,那么.../3.0x/my_icon.png应该是216px乘216px; 但如果未指定宽度和高度,它们都将渲染为72像素×72像素(以逻辑像素为单位)。

pubspec.yaml中asset部分中的每一项都应与实际文件相对应,但主资源项除外。当主资源缺少某个资源时,会按分辨率从低到高的顺序去选择 ,也就是说1x中没有的话会在2x中找,2x中还没有的话就在3x中找。

  • 加载图片 要加载图片,可以使用 AssetImage类。例如,我们可以从上面的asset声明中加载背景图片:
    注意,AssetImage 并非是一个widget, 它实际上是一个ImageProvider,有些时候你可能期望直接得到一个显示图片的widget,那么你可以使用Image.asset()方法,如:

使用默认的 asset bundle 加载资源时,内部会自动处理分辨率等,这些处理对开发者来说是无感知的。 (如果使用一些更低级别的类,如 ImageStream或 ImageCache 时你会注意到有与缩放相关的参数)

  • 依赖包中的资源图片 要加载依赖包中的图像,必须给AssetImage提供package参数。

例如,假设应用程序依赖于一个名为“my_icons”的包,它具有如下目录结构:

…/pubspec.yaml
…/icons/heart.png
…/icons/1.5x/heart.png
…/icons/2.0x/heart.png
…etc.

然后加载图像,使用:

 new AssetImage('icons/heart.png', package: 'my_icons')

new Image.asset('icons/heart.png', package: 'my_icons')

注意:包在使用本身的资源时也应该加上package参数来获取。

  • 打包包中的 assets 如果在pubspec.yaml文件中声明了期望的资源,它将会打包到相应的package中。特别是,包本身使用的资源必须在pubspec.yaml中指定。

包也可以选择在其lib/文件夹中包含未在其pubspec.yaml文件中声明的资源。在这种情况下,对于要打包的图片,应用程序必须在pubspec.yaml中指定包含哪些图像。 例如,一个名为“fancy_backgrounds”的包,可能包含以下文件:

…/lib/backgrounds/background1.png
…/lib/backgrounds/background2.png
…/lib/backgrounds/background3.png

要包含第一张图像,必须在pubspec.yaml的assets部分中声明它:

flutter:
  assets:
    - packages/fancy_backgrounds/backgrounds/background1.png

lib/是隐含的,所以它不应该包含在资产路径中。

3.3.6特定平台 assets

上面的资源都是flutter应用中的,这些资源只有在Flutter框架运行之后才能使用,如果要给我们的应用设置APP图标或者添加启动图,那我们必须使用特定平台的assets。

设置APP图标

更新Flutter应用程序启动图标的方式与在本机Android或iOS应用程序中更新启动图标的方式相同。

  • Android 在Flutter项目的根目录中,导航到.../android/app/src/main/res目录,里面包含了各种资源文件夹(如mipmap-hdpi已包含占位符图像“ic_launcher.png”,见图2-8)。 只需按照Android开发人员指南中的说明, 将其替换为所需的资源,并遵守每种屏幕密度(dpi)的建议图标大小标准。

注意: 如果重命名.png文件,则还必须在AndroidManifest.xml的标签的android:icon属性中更新名称。

  • iOS

在Flutter项目的根目录中,导航到.../ios/Runner。该目录中Assets.xcassets/AppIcon.appiconset已经包含占位符图片(见图2-9), 只需将它们替换为适当大小的图片,保留原始文件名称。

更新启动页

在Flutter框架加载时,Flutter会使用本地平台机制绘制启动页。此启动页将持续到Flutter渲染应用程序的第一帧时。

注意:

这意味着如果不在应用程序的main()方法中调用runApp 函数 (或者更具体地说,如果不调用window.render去响应window.onDrawFrame)的话, 启动屏幕将永远持续显示。

  • Android

要将启动屏幕(splash screen)添加到Flutter应用程序, 请导航至.../android/app/src/main。在res/drawable/launch_background.xml,通过自定义drawable来实现自定义启动界面(你也可以直接换一张图片)。

  • iOS

要将图片添加到启动屏幕(splash screen)的中心,请导航至.../ios/Runner。在Assets.xcassets/LaunchImage.imageset, 拖入图片,并命名为LaunchImage.png、LaunchImage@2x.png、LaunchImage@3x.png。 如果你使用不同的文件名,那还必须更新同一目录中的Contents.json文件,图片的具体尺寸可以查看苹果官方的标准。

可以通过打开Xcode完全自定义storyboard。在Project Navigator中导航到Runner/Runner然后通过打开Assets.xcassets拖入图片,或者通过在LaunchScreen.storyboard中使用Interface Builder进行自定义,如图所示。

4混合开发

4.1 混合开发集成

4.1.1使用Android Studio

4.1.2手动集成

  1. 创建一个Flutter module

  1. 配置Java 8 特性

  1. 添加Flutter module依赖

    1. 源码依赖

    1. aar依赖

4.2 flutter和native通信

4.2.1通信方式简介

  • MethodChannel

    用于传递方法调用(method invocation)一次性通信:如flutter调用Native拍照

  • EventChannel

    用于数据流(event stream)的通信,持续通信,收到消息后无法回复此次消息,通过长用于Nativie向flutter的通信,如:手机电量变化,网络连接变化,陀螺仪,传感器等;

  • BasicMessageChannel

    用于传递字符串和半结构化的信息,持续通信,收到消息后可以回复此次消息,如:Native将遍历到的文件信息陆续传递到dart,在比如:flutter将从服务端陆续获取到的信息交给Native加工,Native处理完返回等。

这三种类型的Channel都是全双工通信,即A<=>B,flutter可以主动发送消息给Native端,并且Native接收到消息后可以做出回应,同样,Native端可以主动发送消息给flutter端,flutter端接收数据后给Native端。

注意: 为了保证UI的响应,通过Platform Channels传递的消息都是异步的

Flutter官方出的获取手机电量的Demo。相关源代码可以从Github下载。

4.2.2 MethodChannel

  • MethodChannel-Native 端

为简单起见,本例的Android端代码都直接写在MainActivity中。Android平台下获取电量是通过调用BatteryManager来获取的,所以我们先在MainActivity中增加一个获取电量的函数:

这个函数需要能被Flutter app调用,此时就需要通过MethodChannel来建立这个通道了。 在configureFlutterEngine创建一个新的MethodChannel

注意: 每个MethodChannel需要有唯一的字符串作为标识,用以互相区分,这个名称建议使用package.module...这样的模式来命名。因为所有的MethodChannel都是保存在以通道名为Key的Map中。所以你要是设了两个名字一样的channel,只有后设置的那个会生效。

接下来我们来填充onMethodCall。

onMethodCall有两个入参,MethodCall里包含要调用的方法名称和参数。Result是给Flutter的返回值。方法名是两端协商好的。通过if语句判断MethodCall.method来区分不同的方法,在我们的例子里面我们只会处理名为“getBatteryLevel”的调用。在调用本地方法获取到电量以后通过result.success(batteryLevel)调用把电量值返回给Flutter。

  • MethodChannel-Flutter 端

首先在 State中创建Flutter端的MethodChannel

channel的名称要和Native端的一致。 然后是通过MethodChannel调用的代码

final int result = await platform.invokeMethod('getBatteryLevel');这行代码就是通过通道来调用Native方法了。注意这里await关键字。前面我们说过MethodChannel是异步的,所以这里必须要使用await关键字。 在上面Native代码中我们把获取到的电量通过result.success(batteryLevel);返回给Flutter。这里await表达式执行完成以后电量就直接赋值给result变量了。剩下的就是怎么展示的问题了,就不再细说了,具体可以去看代码。 需要注意的是,这里我们只介绍了从Flutter调用Native方法,其实通过MethodChannel,Native也能调用Flutter的方法,这是一个双向的通道

举个例子,我们想从Native端请求Flutter端的一个getName方法获取一个字符串。在Flutter端你需要给MethodChannel设置一个MethodCallHandler

在Native端,只需要让对应的的channel调用invokeMethod就行了

至此,MethodChannel的用法就介绍完了。可以发现,通过MethodChannelNative和Flutter方法互相调用还是蛮直接的。这里只是做了个大概的介绍,具体细节和一些复杂用法还有待大家的探索。MethodChannel提供了方法调用的通道,那如果Native有数据流需要传送给Flutter该怎么办呢?这时候就要用到EventChannel了。

4.2.3 EventChannel

EventChannel的使用我们也以官方获取电池电量的demo为例,手机的电池状态是不停变化的。我们要把这样的电池状态变化由Native及时通过EventChannel来告诉Flutter。这种情况用之前讲的MethodChannel办法是不行的,这意味着Flutter需要用轮询的方式不停调用getBatteryLevel来获取当前电量,显然是不正确的做法。而用EventChannel的方式,则是将当前电池状态"推送"给Flutter.

  • EventChannel - Native端

首先Native端创建EventChannel

和MethodChannel类似,我们也是直接new一个EventChannel实例,并给它设置了一个StreamHandler类型的回调。其中onCancel代表对面不再接收,这里我们应该做一些clean up的事情。而 onListen则代表通道已经建好,Native可以发送数据了。注意onListen里带的EventSink这个参数,后续Native发送数据都是经过EventSink的。看代码:

在onReceive函数内,系统发来电池状态广播以后,在Native这里转化为约定好的字符串,然后通过调用events.success();发送给Flutter。Native端的代码就是这样,接下来看Flutter端。

  • EventChannel - Flutter端

首先还是在State内创建EventChannel

然后在initState的时候打开这个channel:

收到event以后的处理是在_onEvent函数里:

从Native端传过来的"charging"/"discharging"字符串直接就是入参event。

4.2.4 总结

Flutter的出发点就是跨平台,而真正要做到跨平台则取决于Flutter是否能通过简单的方式与Native高效通信。Platform Channels能否实现这个目标还有待大规模应用的检验。对于Flutter开发者来讲,由于众多的Native平台API需要暴露给Flutter,还有很多用Native实现的组件/业务逻辑也可能需要暴露给Flutter。这需要写大量的通道代码,也就是说我们必须掌握使用Platform Channels的技能,才能体会到Flutter真正的跨平台能力。本文中对Platform Channels的应用只是非常简单的demo。在大型app中还存在两大挑战,一个是大量的通道我们如何组织,如何维护。另一个是通道协议如何设计才能抹平Android和iOS之间的平台差异,这就需要开发这对两个平台都非常熟悉,这个貌似更加困难。

4.3混合开发后的包大小

  • debug:so库打入了x86_64、x86、arm64-v8a,总共22.28M

  • release,so库只有armeabi-v7a,总共3.46M