Achitectural layers
Flutter被设计成一个可扩展的、分层的系统。它作为一系列独立的库存在,每个库都依赖于底层。任何一层都没有特权访问下面的一层,框架层的每一部分都被设计成可选择和可替换的。
对底层操作系统而言,Flutter应用程序与其他本地应用程序一样,以相同的方式进行打包。一个平台特定的嵌入器提供了一个入口点;与底层操作系统协调,以访问服务,如渲染表面、可访问性和输入;并管理消息事件循环。嵌入器是用适合平台的语言编写的:目前Android的Java和C++,iOS和macOS的Objective-C/Objective-C++,Windows和Linux的C++。使用嵌入器,Flutter代码可以作为一个模块集成到现有的应用程序中,也可以是应用程序的全部内容。Flutter包含了许多针对常见目标平台的嵌入器,但也存在其他嵌入器。
Flutter的核心是Flutter引擎,它主要用C++编写,支持所有Flutter应用所需的基元。每当需要绘制新的帧时,该引擎负责对合成场景进行光栅化。它提供了Flutter核心API的低层实现,包括图形(通过Skia)、文本布局、文件和网络I/O、可访问性支持、插件架构以及Dart运行时和编译工具链。
该引擎通过dart:ui暴露给Flutter框架,它将底层的C++代码封装在Dart类中。这个库暴露了最底层的基元,例如用于驱动输入、图形和文本渲染子系统的类。
通常情况下,开发人员通过Flutter框架与Flutter进行交互,Flutter框架提供了一个用Dart语言编写的现代、反应式框架。它包括一套丰富的平台、布局和基础库,由一系列的层组成。从底层到顶层,我们有:
Flutter框架相对较小;许多开发者可能会用到的更高级别的功能都是以包的形式实现的,包括像摄像头和webview这样的平台插件,以及像字符、http和动画这样的平台无关的功能,这些都是建立在核心Dart和Flutter库的基础上的。其中一些包来自更广泛的生态系统,涵盖应用内支付、苹果认证和动画等服务。
这篇综述的其余部分从UI开发的反应式范式开始,大致浏览了各个层次。然后,我们描述了如何将widget组合在一起,并将其转换为可作为应用程序的一部分进行渲染的对象。我们描述了Flutter如何在平台层面与其他代码进行交互,然后简要总结了Flutter的Web支持与其他目标的不同之处。
Reactive user interfaces
从表面上看,Flutter是一个被动的、伪声明式的UI框架,开发者提供一个从应用状态到界面状态的映射,当应用状态发生变化时,框架在运行时承担更新界面的任务。这种模式的灵感来自于Facebook为自己的React框架所做的工作,其中包括对很多传统设计原则的重新思考。
在大多数传统的UI框架中,用户界面的初始状态被描述一次,然后由用户代码在运行时响应事件单独更新。这种方法的一个挑战是,随着应用程序的复杂性增加,开发人员需要意识到状态变化如何在整个UI中级联。例如,考虑以下UI。
有很多地方可以改变状态:颜色框、色调滑块、单选按钮。当用户与用户界面交互时,变化必须反映在其他每个地方。更糟糕的是,除非小心翼翼,否则对用户界面的一个部分的微小改变可能会对看似不相关的代码产生涟漪效应。
一种解决方案是像MVC这样的方法,通过控制器将数据变化推送到模型,然后模型通过控制器将新的状态推送到视图。然而,这也是有问题的,因为创建和更新UI元素是两个独立的步骤,很容易不同步。
Flutter与其他反应式框架一样,采用了另一种方法来解决这个问题,通过明确地将用户界面与其底层状态解耦。使用React风格的API,你只需要创建UI描述,而框架则负责使用这一个配置来创建和/或适当更新用户界面。
在Flutter中,widget(类似于React中的组件)由不可变的类来表示,这些类用于配置对象树。这些widgets用于管理单独的对象树进行布局,然后用于管理单独的对象树进行合成。Flutter的核心是一系列机制,用于有效地行走树的修改部分,将对象树转换为低级对象树,并在这些树上传播变化。
一个widget通过覆盖build()方法来声明其用户界面,build()方法是一个将状态转换为UI的函数。
build()方法在设计上是快速执行的,并且应该没有副作用,允许框架在任何需要的时候都可以调用它(有可能每渲染一帧就调用一次)。
这种方法依赖于语言运行时的某些特性(特别是快速对象实例化和删除)。幸运的是,Dart特别适合这个任务。
Widgets
如前所述,Flutter强调widget是一个组成单位。Widget是Flutter应用的用户界面的构件,每个widget都是用户界面的一部分不可改变的声明。
小组件形成了一个基于组成的层次结构。每个widget都嵌套在它的父体内部,并且可以从父体接收上下文。这种结构一直延续到根widget(承载Flutter应用的容器,通常是MaterialApp或CupertinoApp),正如这个琐碎的例子所示。
在前面的代码中,所有实例化的类都是widget。
应用程序通过告诉框架用另一个widget替换层次结构中的一个widget来响应事件(如用户交互)更新用户界面。然后,框架会比较新旧widget,并有效地更新用户界面。
Flutter对每个UI控件都有自己的实现,而不是服从于系统提供的控件:例如,iOS Switch控件和Android对应的控件都有一个纯Dart的实现。
这种方法提供了几个好处:
Composition
小部件通常由许多其他小的、单一用途的小部件组成,这些小部件组合起来可以产生强大的效果。
在可能的情况下,设计概念的数量保持在最低限度,同时允许总词汇量很大。例如,在widgets层中,Flutter使用相同的核心概念(一个Widget)来表示绘制到屏幕上、布局(定位和大小)、用户交互性、状态管理、主题、动画和导航。在动画层,一对概念Animations和Tweens覆盖了大部分的设计空间。在渲染层中,RenderObjects用于描述布局、绘画、命中测试和可访问性。在每一种情况下,对应的词汇量最终都会很大:有数百个widgets和渲染对象,以及几十种动画和Tweens类型。
类的层次结构是刻意的浅而宽,以最大限度地增加可能的组合数量,专注于小型的、可组合的widgets,每个widgets都能做好一件事。核心功能是抽象的,即使是基本的功能,如padding和align,也是作为单独的组件实现的,而不是内置在核心中。(这也与传统的API形成了鲜明的对比,在传统的API中,像padding这样的功能是内置于每个布局组件的通用核心中的。)。所以,举例来说,要让一个小组件居中,而不是调整一个名义上的 Align 属性,你可以把它包裹在一个 Center 小组件中。
有用于填充、对齐、行、列和网格的小组件。这些布局部件没有自己的视觉表示。相反,它们的唯一目的是控制另一个部件的布局的某些方面。Flutter还包括利用这种组合方法的实用工具部件。
例如,Container,一个常用的widget,是由几个widget组成的,负责布局,绘画,定位和大小。具体来说,Container是由LimitedBox、ConstrainedBox、Align、Padding、DecoratedBox和Transform小组件组成的,你可以通过阅读它的源代码看到。Flutter的一个定义特性是,你可以钻进任何一个widget的源头并检查它。所以,你可以用新奇的方式将它和其他简单的widget组合起来,或者直接用Container作为灵感创建一个新的widget,而不是通过子类Container来产生自定义的效果。
Building widgets
如前所述,您通过重载build()函数来确定widget的视觉表现,以返回一个新的元素树。这个树以更具体的方式表示小组件在用户界面中的部分。例如,一个工具条小组件可能有一个构建函数,它返回一些文本和各种按钮的水平布局。根据需要,框架会递归地要求每个小组件进行构建,直到树完全由具体的可渲染对象来描述。然后,框架将这些可渲染对象缝合到一个可渲染对象树中。
一个widget的构建函数应该是没有副作用的。每当函数被要求构建时,widget应该返回一个新的widgets树1,不管widget之前返回的是什么。框架会做繁重的工作,根据渲染对象树来决定哪些构建方法需要被调用(后面会详细介绍)。关于这个过程的更多信息可以在Inside Flutter主题中找到。
在每个渲染帧上,Flutter可以通过调用该widget的build()方法,仅仅重新创建UI中状态已经改变的部分。因此,构建方法应该快速返回,重计算工作应该以某种异步方式完成,然后作为状态的一部分存储起来,供构建方法使用,这一点非常重要。
虽然这种自动对比的方法比较幼稚,但却相当有效,可以实现高性能、交互式的应用。而且,构建函数的设计通过专注于声明一个widget是由什么组成的,而不是将用户界面从一个状态更新到另一个状态的复杂性来简化你的代码。
Widget state
该框架引入了两大类widget:有状态和无状态widget。
许多widget没有可改变的状态:它们没有任何随时间变化的属性(例如,一个图标或一个标签)。这些widget是StatelessWidget的子类。
然而,如果一个小组件的独特特性需要根据用户交互或其他因素而改变,那么该小组件是有状态的。例如,如果一个小组件有一个计数器,每当用户点击一个按钮时就会递增,那么计数器的值就是该小组件的状态。当该值发生变化时,该小组件需要重新构建以更新其UI部分。这些widget是StatefulWidget的子类,(因为widget本身是不可变的)它们将可变的状态存储在一个单独的State子类中。StatefulWidgets没有构建方法;相反,它们的用户界面是通过State对象构建的。
每当你突变一个State对象时(例如,通过递增计数器),你必须调用setState()来向框架发出信号,通过再次调用State的构建方法来更新用户界面。
拥有独立的状态和widget对象,让其他widget以完全相同的方式对待无状态和有状态的widget,而不用担心丢失状态。父对象不需要紧紧抓住一个子对象来保存它的状态,而是可以在任何时候创建一个新的子对象实例而不会丢失子对象的持久化状态。框架会在适当的时候完成所有寻找和重用现有状态对象的工作。
State management
那么,如果许多widget可以包含状态,那么如何管理状态并在系统中传递呢?
和其他类一样,你可以在widget中使用构造函数来初始化它的数据,所以build()方法可以确保任何子widget被实例化时都有它需要的数据。
然而,随着小组件树的深入,在树的层次结构中上下传递状态信息变得很麻烦。因此,第三种小组件类型 InheritedWidget 提供了一种从共享祖先中抓取数据的简单方法。您可以使用 InheritedWidget 来创建一个状态小组件,该小组件在小组件树中包装一个共同的祖先,如本例所示。
每当一个 ExamWidget 或 GradeWidget 对象需要来自 StudentState 的数据时,它现在可以通过一个命令来访问它,例如
of(context)调用接收构建上下文(当前小组件位置的句柄),并返回树中与StudentState类型匹配的最近的祖先。InheritedWidgets还提供了一个updateShouldNotify()方法,Flutter调用该方法来决定状态变化是否应该触发使用它的子部件的重建。
Flutter本身广泛使用InheritedWidget作为共享状态框架的一部分,例如应用程序的视觉主题,其中包括颜色和类型样式等属性,这些属性在整个应用程序中是普遍存在的。MaterialApp build()方法在构建时,会在树中插入一个主题,然后在更深的层次结构中,一个widget可以使用.of()方法来查找相关的主题数据,例如。
这种方法也用于提供页面路由的Navigator和提供访问屏幕指标(如方向、尺寸和亮度)的MediaQuery。
随着应用程序的增长,更先进的状态管理方法,减少了创建和使用有状态小部件的仪式,变得更有吸引力。许多Flutter应用程序使用了像provider这样的实用程序包,它提供了一个围绕InheritedWidget的包装器。Flutter的分层架构也使其他方法能够实现状态到UI的转换,例如flutter_hooks包。
Rendering and layout
本节介绍了渲染管道,这是Flutter将小组件的层次结构转换为实际像素画到屏幕上的一系列步骤。
Flutter’s rendering model
你可能想知道:如果Flutter是一个跨平台框架,那么它怎么能提供与单平台框架相当的性能呢?
从传统的Android应用的工作方式开始思考是很有用的。绘图时,首先调用Android框架的Java代码。Android系统库提供了负责自己绘图的组件,将其转化为Canvas对象,然后Android可以用Skia渲染,Skia是一个用C/C++编写的图形引擎,调用CPU或GPU在设备上完成绘图。
跨平台框架的工作方式通常是在底层的原生 Android 和 iOS UI 库上创建一个抽象层,试图平滑每个平台表示方式的不一致。App代码通常是用JavaScript等解释语言编写的,而JavaScript又必须与基于Java的Android或基于Objective-C的iOS系统库进行交互以显示UI。所有这些都会增加大量的开销,特别是在UI和应用逻辑之间有大量交互的地方。
相比之下,Flutter最大限度地减少了这些抽象,绕过系统UI小部件库而使用自己的小部件集。绘制Flutter视觉效果的Dart代码被编译成本地代码,使用Skia进行渲染。Flutter还嵌入了自己的Skia副本作为引擎的一部分,允许开发者升级他们的应用程序,以保持最新的性能改进,即使手机还没有更新新的Android版本。其他原生平台上的Flutter也是如此,比如iOS、Windows或macOS。
From user input to the GPU
Flutter适用于其渲染管道的首要原则是:简单就是快。Flutter对于数据如何流向系统有一个简单明了的管道,如下顺序图所示。
让我们来看看这些阶段的一些细节。
Build: from Widget to Element
考虑这个简单的代码片段,它演示了一个简单的小组件层次结构。
当Flutter需要渲染这个片段时,它会调用build()方法,该方法会返回一个widgets的子树,根据当前应用状态渲染UI。在这个过程中,build()方法可以根据需要,根据其状态引入新的widgets。举个简单的例子,在前面的代码片段中,Container有颜色和子属性。通过查看Container的源码,可以看到,如果颜色不是null,它就会插入一个代表颜色的ColoredBox。
相应地,图像和文本小组件可能会在构建过程中插入子小组件,如 RawImage 和 RichText。因此,最终的小组件层次结构可能比代码所表示的更深,如本例2。
这就解释了为什么当你通过调试工具(如Dart DevTools的一部分Flutter检查器)检查这个树时,你可能会看到一个比你的原始代码更深的结构。
在构建阶段,Flutter将代码中表达的widget翻译成相应的元素树,每个widget都有一个元素。每个元素都代表了一个小组件在树层次结构的特定位置的具体实例。元素有两种基本类型。
RenderObjectElements是它们的widget类比和底层RenderObject之间的中介,我们稍后会提到。
任何widget的元素都可以通过它的BuildContext来引用,BuildContext是widget在树中位置的句柄。这是一个函数调用中的上下文,比如Theme.of(context),并作为参数提供给build()方法。
因为widget是不可改变的,包括节点之间的父/子关系,对widget树的任何改变(例如在前面的例子中把Text('A')改为Text('B'))都会导致返回一组新的widget对象。但这并不意味着必须重建底层表示。元素树从一帧到另一帧都是持久的,因此起着关键的性能作用,允许Flutter在缓存其底层表示时,就像小组件层次结构是完全可处置的一样。通过只走过发生变化的widget,Flutter可以只重建元素树中需要重新配置的部分。
Layout and rendering
这将是一个很少见的只画一个小部件的应用。因此,任何UI框架的一个重要部分都是能够有效地布局widget的层次结构,在屏幕上渲染之前确定每个元素的大小和位置。
渲染树中每个节点的基类是RenderObject,它定义了一个布局和绘画的抽象模型。这是极其通用的:它不承诺固定的尺寸数,甚至不承诺笛卡尔坐标系(通过这个极坐标系的例子来证明)。每个RenderObject都知道它的父体,但除了如何访问它们和它们的约束外,对它的子体几乎一无所知。这为RenderObject提供了足够的抽象性,能够处理各种用例。
在构建阶段,Flutter为元素树中的每个RenderObjectElement创建或更新一个继承自RenderObject的对象。RenderObjects是基元。RenderParagraph 渲染文本,RenderImage 渲染图像,RenderTransform 在绘制其子元素之前应用一个变换。
大多数Flutter widget都是由一个继承自RenderBox子类的对象来渲染的,RenderBox代表了一个在2D笛卡尔空间中固定大小的RenderObject。RenderBox提供了一个盒子约束模型的基础,为每个要渲染的widget建立了一个最小和最大的宽度和高度。
为了执行布局,Flutter以深度优先的遍历方式走过渲染树,并将尺寸约束从父级传递到子级。在确定其大小时,子代必须尊重其父代给它的约束。子对象在父对象建立的约束条件下,通过向上传递尺寸来做出响应。
在这一单次走过树的结束时,每个对象都在其父约束内有一个定义的大小,并准备好通过调用paint()方法来绘制。
箱子约束模型作为一种在O(n)时间内布局对象的方法是非常强大的。
关于约束和布局系统的更多信息,以及工作实例,可以在理解约束主题中找到。
所有RenderObjects的根是RenderView,它代表渲染树的总输出。当平台要求渲染一个新的帧时(例如,因为vsync或因为纹理解压/上传完成),会调用compositeFrame()方法,它是渲染树根部的RenderView对象的一部分。这将创建一个SceneBuilder来触发场景的更新。当场景完成后,RenderView对象将合成的场景传递给dart:ui中的Window.render()方法,该方法将控制权传递给GPU来渲染它。
管道的合成和光栅化阶段的进一步细节超出了本篇高级文章的范围,但更多的信息可以在这篇关于Flutter渲染管道的演讲中找到。
Platform embedding
正如我们所看到的,Flutter的用户界面不是被翻译成等价的操作系统小部件,而是由Flutter自己构建、布局、合成和绘制。根据该平台独特的关注点,获取纹理和参与底层操作系统的应用生命周期的机制不可避免地有所不同。该引擎是平台无关的,呈现了一个稳定的ABI(应用二进制接口),为平台嵌入者提供了一种设置和使用Flutter的方式。
平台嵌入器是承载所有Flutter内容的原生操作系统应用程序,并作为主机操作系统和Flutter之间的粘合剂。当你启动一个Flutter应用时,嵌入器提供入口点,初始化Flutter引擎,获取UI和光栅化的线程,并创建Flutter可以写入的纹理。嵌入器还负责应用程序的生命周期,包括输入手势(如鼠标、键盘、触摸)、窗口大小、线程管理和平台消息。Flutter包括Android、iOS、Windows、macOS和Linux的平台嵌入器;你也可以创建一个自定义的平台嵌入器,就像这个工作实例一样,支持通过VNC风格的framebuffer来遥控Flutter会话,或者这个工作实例用于Raspberry Pi。
每个平台都有自己的一套API和约束。一些针对平台的简要说明。
Integrating with other code
Flutter提供了多种互操作性机制,无论你是要访问用Kotlin或Swift等语言编写的代码或API,还是要调用基于C语言的原生API,在Flutter应用中嵌入原生控件,或者在现有应用中嵌入Flutter。
Platform channels
对于移动和桌面应用,Flutter允许你通过平台通道调用到自定义代码,这是一个简单的机制,用于在你的Dart代码和主机应用的平台特定代码之间进行通信。通过创建一个通用通道(封装名称和编解码器),你可以在Dart和用Kotlin或Swift等语言编写的平台组件之间发送和接收消息。数据从像Map这样的Dart类型序列化为标准格式,然后反序列化为Kotlin(如HashMap)或Swift(如Dictionary)中的等价表示。
以下是Kotlin(Android)或Swift(iOS)中Dart调用接收事件处理程序的一个简单平台通道示例。
更多使用平台渠道的例子,包括macOS的例子,可以在flutter/plugins资源库中找到3。另外,Flutter已经有数千个插件,涵盖了很多常见的场景,从Firebase到广告,再到摄像头和蓝牙等设备硬件。
Foreign Function Interface
对于基于C的API,包括那些可以为Rust或Go等现代语言编写的代码生成的API,Dart提供了一个直接的机制,使用dart:ffi库与本地代码绑定。外来函数接口(FFI)模型可以比平台通道快很多,因为不需要序列化来传递数据。相反,Dart运行时提供了在由Dart对象支持的堆上分配内存的能力,并对静态或动态链接的库进行调用。FFI适用于除web以外的所有平台,在这些平台上,js包具有同等的作用。
要使用FFI,你要为每个Dart和非托管方法签名创建一个typedef,并指示Dart VM在它们之间进行映射。作为一个简单的例子,这里有一个调用传统的Win32 MessageBox()API的代码片段。
Rendering native controls in a Flutter app
因为Flutter的内容是绘制在纹理上的,而且它的widget树完全是内部的,所以在Flutter的内部模型中没有像Android视图这样的东西存在的地方,也没有在Flutter widgets中交错渲染的地方。这对于那些希望在Flutter应用中包含现有平台组件的开发者来说是个问题,比如浏览器控件。
Flutter通过引入平台视图小部件(AndroidView和UiKitView)来解决这个问题,它可以让你在每个平台上嵌入这种内容。平台视图可以与其他Flutter内容集成4。这些小部件中的每一个都充当了底层操作系统的中介。例如,在Android上,AndroidView有三个主要功能。
不可避免的是,这种同步会带来一定的开销。因此,一般来说,这种方法最适合像Google地图这样的复杂控件,在Flutter中重新实现并不实用。
通常情况下,Flutter应用会根据平台测试在build()方法中实例化这些小部件。作为一个例子,从google_maps_flutter插件。
与AndroidView或UiKitView底层的原生代码通信通常使用平台通道机制,如前所述。
目前,平台视图还不能用于桌面平台,但这不是架构上的限制,未来可能会增加支持。
Hosting Flutter content in a parent app
前面场景的反义词是在现有的Android或iOS应用中嵌入Flutter小部件。如前一节所述,在移动设备上运行的新创建的Flutter应用程序被托管在Android活动或iOS UIViewController中。Flutter内容可以使用相同的嵌入API嵌入到现有的Android或iOS应用程序中。
Flutter模块模板是为了方便嵌入而设计的;你可以将其作为源码依赖嵌入到现有的Gradle或Xcode构建定义中,也可以将其编译到Android Archive或iOS Framework二进制中使用,而不需要每个开发者都安装Flutter。
Flutter引擎需要很短的时间来初始化,因为它需要加载Flutter共享库,初始化Dart运行时,创建和运行Dart隔离,并将渲染表面附加到UI。为了最大限度地减少呈现Flutter内容时的UI延迟,最好在整体应用初始化序列中初始化Flutter引擎,或者至少在第一个Flutter屏幕之前初始化,这样用户在加载第一个Flutter代码时就不会遇到突然的停顿。此外,分离Flutter引擎可以让它在多个Flutter屏幕上重复使用,并分担加载必要库所涉及的内存开销。
更多关于Flutter如何加载到现有的Android或iOS应用中的信息可以在加载顺序、性能和内存主题中找到。
Flutter web support
虽然一般的架构概念适用于Flutter支持的所有平台,但Flutter的Web支持有一些独特的特点值得评论。
Dart自从JavaScript语言存在以来就一直在编译成JavaScript,其工具链针对开发和生产目的进行了优化。许多重要的应用程序从Dart编译成JavaScript,并在今天的生产中运行,包括Google Ads的广告商工具。因为Flutter框架是用Dart编写的,所以编译成JavaScript相对简单。
然而,用C++编写的Flutter引擎被设计成与底层操作系统而非网络浏览器的接口。因此,需要采用不同的方法。在网络上,Flutter在标准浏览器API之上提供了引擎的重新实现。目前,我们有两种选择来渲染网络上的Flutter内容。HTML和WebGL。在HTML模式下,Flutter使用HTML、CSS、Canvas和SVG。为了渲染到WebGL,Flutter使用了一个编译成WebAssembly的Skia版本,称为CanvasKit。虽然HTML模式提供了最好的代码大小特性,但CanvasKit提供了最快的路径到浏览器的图形堆栈,并提供了一些更高的图形保真度与本地移动目标5。
网页版的架构层图如下。
也许与Flutter运行的其他平台相比,最显著的区别是,Flutter不需要提供Dart运行时。相反,Flutter框架(以及你编写的任何代码)被编译成JavaScript。值得注意的是,Dart在所有模式中很少有语言语义上的差异(JIT与AOT,native与web编译),大多数开发者永远不会写一行代码碰到这样的差异。
在开发的时候,Flutter web使用的是dartdevc,这是一个支持增量编译的编译器,因此允许应用程序的热重启(虽然目前还不能热重载)。相反,当你准备为web创建一个生产应用时,使用dart2js,Dart的高度优化的生产JavaScript编译器,将Flutter核心和框架与你的应用一起打包成一个最小化的源文件,可以部署到任何web服务器。代码可以在一个文件中提供,也可以通过延迟导入分割成多个文件。
Further information
对于那些对Flutter的内部信息感兴趣的人,Inside Flutter白皮书为框架的设计理念提供了有用的指导。
1 虽然构建函数会返回一棵新鲜的树,但你只需要返回一些不同的东西,如果有一些新的配置要加入。如果配置其实是一样的,你可以直接返回同样的widget。
2 这是为了方便阅读而略作的简化。在实践中,这棵树可能更复杂。
3 虽然Linux和Windows上的工作正在进行中,但这些平台的例子可以在Flutter桌面嵌入库中找到。随着在这些平台上的开发趋于成熟,这些内容将逐步迁移到Flutter主库中。
4 这种方法有一些局限性,例如,对于平台视图来说,透明度的合成方式和其他Flutter小部件的合成方式不一样。
5 一个例子是阴影,它必须用DOM等价的基元来近似,代价是牺牲一些保真度。
修仙
Flutter Dojo开源至今,受到了很多Flutter学习者和爱好者的喜爱,也有越来越多的人加入到Flutter的学习中来,所以我建了个Flutter修仙群,但是人数太多,所以分成了【Flutter修仙指南】【Flutter修仙指北】【Flutter修仙指东】三个群,对Flutter感兴趣的朋友,可以添加我的微信,注明加入Flutter修仙群,或者直接关注我的微信公众号【Android群英传】。
感兴趣的朋友可以加我微信【Tomcat_xu】,我拉你入群。
项目地址:
https://github.com/xuyisheng/flutter_dojo
本文分享自微信公众号 - Android群英传(android_heroes)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。