Skip to main content

2 posts tagged with "flutter"

View All Tags

· 7 min read
i校长

简述

曾几何时,你有没有一个搭个人网站的冲动,我这个想法在我第一次开始写博客就有了,可就是没有搭起来,直到我看到flutter有了web支持,我就在想,是时候了,必须且一定要做,于是乎我就搭建了现在的网站 ibaozi.cn ,代码开源至 https://github.com/ibaozi-cn/ibaozi ,而这次我要做的并不是基于这个网站开发,我再次申请了一个新域名jetpack.net.cn,读过我之前的博客可能直到,我做了一个Android Jetpack模版项目在线生成工具,我申请这个就是为了将Android Jetpack整合进来,提供一个好记的域名,当然我还有另一项计划,就是做一个Flutter 生态的Jetpack,这次博客就是计划的第一步,完全开源给大家,让读我博客的同学们,跟我一起搭建一个网站,顺便学习Flutter技术,好了不多说了,接下来,让我们看看如何搭建这个网站。

环境要求

Flutter 项目创建

假装你已经搭建好环境

  • step 1 打开终端,切换Flutter 分支
flutter channel beta
flutter upgrade
flutter config --enable-web
flutter devices
Chrome • chrome • web-javascript • Google Chrome 78.0.3904.108

一行一行执行命令,最后看到Chrome,祝贺你成功了。

  • step 2

    打开Android Studio 项目名字、描述简单修改一下,next下一步 修改一下包名,然后Finish,需要等待一会儿。 项目创建成功了。这里就到这,后期博客慢慢介绍每次开发的细节。

Node 项目创建

我们直接打开Flutter项目的Terminal

mkdir node
mkdir server
cd node/server

进入server目录,现在你的node环境应该也可以了吧,好开始用Express 生成器生成项目

npm install express-generator -g //安装好了略过
express --view=pug myapp

修改myapp为你自己的项目名。执行完你会看到 接下来

cd myapp
npm i
npm start

浏览器试下http://localhost:3000看到如下就ok了

开始项目关联

  • step 1

    在Flutter项目中执行

flutter build web

构建web包,最终会在build文件夹下生成web包,web包下就是网站的相关文件。

  • step 2

    copy web包下的文件到node项目的public文件下 我创建了一个public_flutter_web,为了是以后文件区分,也建议你做一样的操作

  • step 3

    改造express,因为默认express是展示 views包下的网页的,而且默认不是html实现。 将下图中文件全部删除即可

    打开 app.js文件,删除delete标记部分,添加add标记部分

  • step 4

    保存修改,重新将服务npm start,再打开http://localhost:3000 看到如下:

    大功告成,这样就行了吗,nono,对于一个懒惰的人来说,我们要写一些脚本,辅助项目自动构建。

  • step 5

    由于node项目目录太深,在命令行运行也很麻烦,我们写个shell脚本,来帮我搞定。在flutter项目根目录创建bin文件夹,用来放置我们的脚本

    右键New File 命名为 test_start_node.sh,内容如下

#!/usr/bin/env bash
node node/server/bin/www

也很简单。这个脚本就是辅助我们开启node服务。当然我们还会有flutter项目构建的一些脚本,自动copy文件到指定目录等等,这些之后慢慢补全哦。

最后

将代码上传至github ibaozi-cn / flutter-jetpack

最后的最后

登上你的云服务器,通过git 将项目下载到服务器上,这里我们需要工具辅助我们服务部署 我选择pm2+nginx来将我的服务启动起来 pm2:环境搭建 nginx:环境搭建 这里不详细说了,网上有一片大海,需要你去浪。有问题的留言我,我可以协助你。 最终通过pm2 和 nginx ,项目完美运行 jetpack.net.cn,没错你看到的是jetpack.ibaozi.cn,哈哈,域名还没下来,先用了之前的ibaozi.cn,后面我们会迁移到jetpack.net.cn。

总结

下期我们就开发Flutter 主页,遇到什么,需要借助什么,怎么写,为什么这么写,我们将在未来的博客中,带你一步步实现一个完整的网站,随我写下去。如果你想参与开发,请私聊我,合作共赢,加油。

· 16 min read
i校长

背景

为什么说Flutter万物皆Widget?首先你要知道,Flutter是什么,它是一个现代的响应式框架、一个2D渲染引擎、现成的widget和开发工具,基于Skia,一个性能彪悍的2D图像绘制引擎,2005年被Google收购,被广泛应用于Chrome和Android之上,等等吧,说白一点,Flutter就是一个UI框架,所以说,万物皆Widget,而Widget的中文意思是小部件,它为什么不能像Android或者Ios一样叫做View呢?因为widget既可以是一个结构元素(如按钮或菜单)、也可以是一个文本样式元素(如字体或颜色方案)、布局的一个方面(如填充)等等,我们可以统筹它们为wiget,而不是view,根据基本的命名规范,这就是一种合理的命名抽象。那么接下来我们学什么?

  • Widget是什么
  • Widget类结构
  • 跟着我实现一个widget(直接继承widget抽象类)
  • Element类结构
  • 深入理解Element

Widget是什么

其实上面说了,一切皆Widget,那我们可不可以认为,在flutter的框架中,用到的东西都是Widget呢,当然不是哈,由于它是基于Dart,所以有很多Dart的库,还是可以使用的,比如AES,RSA加密解密,Json序列化等等,但你可以这么说,一切构建图形相关的东西都是Widget,这就是Widget

Widget类结构

为什么说下类结构呢?类结构可以很清晰帮助我们梳理逻辑,从全局的角度看待整个结构

image

  • RenderObjectWidget 看名字我们判断,它是持有RenderObject对象的Widget,而通过其他通道了解到,RenderObject实际上是完成界面的布局、测量与绘制,像Padding,Table,Align都是它的子类
  • StatefulWidget 多了一个State状态的Widget,子类都是可以动态改变的如CheckBox,Switch
  • StatelessWidget 就是一个普通的Widget,不可变如Icon,Text。
  • ProxyWidget InheritedWidget就是它的子类,我们暂且认为它是子类能从父类拿数据的关键,以后再研究,大多数的主题都是继承自ProxyWidget

跟我一起实现一个Widget

我不想和别人的教程思路一样,既然万物皆Widget,那我们就从实现一个Widget开始,然后一步步深入,看到什么就去了解什么?来上代码

class TestWidget extends Widget{
@override
Element createElement() {
// TODO: implement createElement
throw UnimplementedError();
}
}

创建一个TestWidget然后继承Widget,然后会让你重写函数createElement,返回一个Element,通过这个我们看的出,其实我们创建的Widget,最终肯定是创建了一个Element,那Element到底是什么呢?同样的思路,我们继承Element看一下

class TestElement extends Element{

TestElement(Widget widget) : super(widget);

@override
bool get debugDoingBuild => throw UnimplementedError();

@override
void performRebuild() {
}

}

多了一个构造函数,传递Widget对象,get函数debugDoingBuild,还有performRebuild函数,都是干嘛的呢?

abstract class Element extends DiagnosticableTree implements BuildContext 

abstract class BuildContext {

/// Whether the [widget] is currently updating the widget or render tree.
///
/// For [StatefulWidget]s and [StatelessWidget]s this flag is true while
/// their respective build methods are executing.
/// [RenderObjectWidget]s set this to true while creating or configuring their
/// associated [RenderObject]s.
/// Other [Widget] types may set this to true for conceptually similar phases
/// of their lifecycle.
///
/// When this is true, it is safe for [widget] to establish a dependency to an
/// [InheritedWidget] by calling [dependOnInheritedElement] or
/// [dependOnInheritedWidgetOfExactType].
///
/// Accessing this flag in release mode is not valid.
bool get debugDoingBuild;

经过代码的跟踪我们发现一些注解:

  • Element继承自DiagnosticableTree,并实现BuildContext
  • DiagnosticableTree是个“诊断树”,主要作用是提供调试信息。
  • BuildContext类似原生系统的上下文,它定义了debugDoingBuild,通过注解我们知道,它应该就是一个debug用的一个标志位。
  • performRebuild 经过源码查看后发现,由rebuild()调用如下
  void rebuild() {
if (!_active || !_dirty)
return;
performRebuild();
}

@override
void update(ProxyWidget newWidget) {
rebuild();
}

首先说明下,这个并不是Element的源码,我摘自StatelessElement,是Element的子类,这说明在update函数后,Element就会直接执行performRebuild函数,那我们完善下自定义的Element逻辑

class TestElement extends Element {

TestElement(Widget widget) : super(widget);

@override
bool get debugDoingBuild => throw UnimplementedError();

@override
void performRebuild() {
}

@override
void update(Widget newWidget) {
super.update(newWidget);
print("TestWidget update");
performRebuild();
}

@override
TestWidget get widget => super.widget as TestWidget;

Widget build() => widget.build(this);
}

在update的时候执行performRebuild(),但是performRebuild执行什么呢?我们结合一下StatelessElement的实现,发现,它调用了传递进来的Widget参数build函数,那么我们就在TestWidget中添加函数,并完善下逻辑后是这样的

class TestWidget extends Widget {

@override
Element createElement() {
/// 将自己传递进去,让Element调用下面的build函数
return TestElement(this);
}
/// 这个context其实就是Element
Widget build(BuildContext context) {
print("TestWidget build");
return Text("TestWidget");
}
}

class TestElement extends Element {

Element _child;

TestElement(Widget widget) : super(widget);

@override
bool get debugDoingBuild => throw UnimplementedError();

@override
void performRebuild() {
///调用build函数
var _build = build();
///更新子视图
_child = updateChild(_child, _build, slot);
}

@override
void update(Widget newWidget) {
super.update(newWidget);
print("TestWidget update");
///更新
performRebuild();
}

///将widget强转成TestWidget
@override
TestWidget get widget => super.widget as TestWidget;
/// 调用TestWidget的build函数
Widget build() => widget.build(this);
}

然后将其放入main.dart中如图

image

最终效果展示,如图

[图片上传失败...(image-72eee6-1600853501724)]

展示出来了,我们简单总结一下,到目前你学到了什么?

  • Widget会创建Element对象(调用createElement并不是Widget,而是Framework)
  • Widget并没有实际的操控UI
  • Element是在update的时候重新调用Widget的build函数来构建子Widget
  • updateChild会根据传入的Widget生成新的Element
  • Widget的函数build,传入的context其实就是它创建的Element对象,那么为什么这么设计呢?一方面它可以隔离掉一些Element的细节,避免Widget频繁调用或者误操作带来的不确定问题,一方面context上下文可以存储树的结构,来从树种查找元素。

其实可以很简单的理解为,Widget就是Element的配置信息,在Dart虚拟机中会频繁的创建和销毁,由于量比较大,所以抽象一层Element来读取配置信息,做一层过滤,最终再真实的绘制出来,这样做的好处就是避免不必要的刷新。接下来我们深入了解下Element

Element类结构

在深入了解Element之前我们也从全局看下它的结构

image

可以看到,Element最主要的两个抽象:

  • ComponentElement
  • RenderObjectElement

都是干嘛的呢?经过看源码,发现ComponentElement,其实做了一件事情就是在mount函数中,判断Element是第一次创建,然后调用_firstBuild,最终通过rebuild调用performRebuild,通过上面我们也知道performRebuild最终调用updateChild来绘制UI 而RenderObjectElement就比较复杂一点,它创建了RenderObject,通过RenderObjectWidget的createRenderObject方法,通过以前的学习,我们也知道RenderObject其实是真正绘制UI的对象,所以我们暂且认为RenderObjectElement其实就是可以直接操控RenderObject,一种更直接的方式来控制UI。

深入理解Element

为什么要深入理解Element呢,由于大多数情况下,我们开发者并不会直接操作Element,但对于想要全局了解FlutterUI框架至关重要,特别实在一些状态管理的框架中,如Provider,他们都定制了自己的Element实现,那么这么重要,我们需要从哪方面了解呢?一个很重要的知识点就是生命周期,只有了解了正确的生命周期,你才能在合适的时间做合适的操作

image

为了验证该图,我们加入日志打印下,代码如下:

/// 创建LifecycleElement 实现生命周期函数
class LifecycleElement extends TestElement{

LifecycleElement(Widget widget) : super(widget);

@override
void mount(Element parent, newSlot) {
print("LifecycleElement mount");
super.mount(parent, newSlot);
}

@override
void unmount() {
print("LifecycleElement unmount");
super.unmount();
}

@override
void activate() {
print("LifecycleElement activate");
super.activate();
}

@override
void rebuild() {
print("LifecycleElement rebuild");
super.rebuild();
}

@override
void deactivate() {
print("LifecycleElement deactivate");
super.deactivate();
}

@override
void didChangeDependencies() {
print("LifecycleElement didChangeDependencies");
super.didChangeDependencies();
}

@override
void update(Widget newWidget) {
print("LifecycleElement update");
super.update(newWidget);
}

@override
Element updateChild(Element child, Widget newWidget, newSlot) {
print("LifecycleElement updateChild");
return super.updateChild(child, newWidget, newSlot);
}

@override
void deactivateChild(Element child) {
print("LifecycleElement deactivateChild");
super.deactivateChild(child);
}

}

class TestWidget extends Widget {

@override
Element createElement() {
/// 将自己传递进去,让Element调用下面的build函数
/// 更新TestElement为LifecycleElement
return LifecycleElement(this);
}
/// 这个context其实就是Element
Widget build(BuildContext context) {
return Text("TestWidget");
}
}

然后改造下main.dart, 如下

///添加变量
bool isShow = true;
/// 加入变量控制
isShow ? TestWidget() : Container(),
/// 将floatingActionButton改为这样的实现
onPressed: () {
setState(() {
isShow = !isShow;
});
},

运行一下项目查看日志

image

  • 调用 element.mount(parentElement,newSlot)
  • 调用 update(Widget newWidget)
  • 调用 updateChild(Element child, Widget newWidget, newSlot)

然后我们点击下按钮

image

  • 调用 deactivate()
  • 调用 unmount()

我们再点击下按钮

image

这次只有mount,为什么?由于Widget本身不可变,我判断是因为这个导致的,那如何判断呢?下面介绍一个小技巧,其实flutter的framework层是可以加入调试代码的,我们加入日志看下,如下:

/// widget 基类其实有一个canUpdate函数,我们猜测肯定是这里导致的,加入日志如下
static bool canUpdate(Widget oldWidget, Widget newWidget) {

if(oldWidget.toString()=="TestWidget") {
print("canUpdate${oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key}");
}

return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}

是个静态函数,肯定是在Element中被调用的,我们找下

@mustCallSuper
void update(covariant Widget newWidget) {

if (newWidget.toString() == "TestWidget") {
print("TestWidget update start");
}

assert(_debugLifecycleState == _ElementLifecycle.active
&& widget != null
&& newWidget != null
&& newWidget != widget
&& depth != null
&& _active
&& Widget.canUpdate(widget, newWidget));

assert(() {
_debugForgottenChildrenWithGlobalKey.forEach(_debugRemoveGlobalKeyReservation);
_debugForgottenChildrenWithGlobalKey.clear();
return true;
}());
if (newWidget.toString() == "TestWidget") {
print("TestWidget:${newWidget.hashCode}");
}
_widget = newWidget;
}

如上代码是Element的源码,这里调用了canUpdate函数,如果不需要更新的话,就直接中断了执行,我们重新运行下demo,并在加一个print来验证一下newWidget是什么样子的,这里加入newWidget.toString() == "TestWidget",主要是为了过滤垃圾日志,重新运行项目。如图

image

点击后按钮

image

再点击

image

发现并没有调用canUpdate,那我们如何让它重新加载回来呢?我们查查资料,改造下例子

  @override
void mount(Element parent, newSlot) {
print("LifecycleElement mount");
super.mount(parent, newSlot);
assert(_child == null);
print("LifecycleElement firstBuild");
performRebuild();
}

mount函数加入performRebuild()函数,最终会触发updateChild,加assert断言是防止后面再加载进来的时候多次触发updateChild,然后改造下main.dart

@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: isShow ? TestWidget() : Container(),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
isShow = !isShow;
});
},
tooltip: 'Increment',
child: Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}

去掉Column,这里是由于我们没有处理widget的index逻辑,导致在Column里不正常,后续我们再研究为什么,先来看下生命周期的回调

第一次运行

[图片上传失败...(image-be90d-1600853501724)]

点击按钮

image

又发现一个问题,为什么我们的断言没生效呢?怎么又出现了firstBuild?哈哈,这里不要纠结,由于TestWidget并非const,导致setState后,又重新被创建了,而对应的Element也同样是创建了新的值,最终导致被重新执行。其实这个TestWidget已经不是上一个了,那我们加入 const修饰再看看

/// 改成const
const TestWidget()

/// 加入当前widget hashcode输出,用来判断两次是否一致
@override
void mount(Element parent, newSlot) {
print("LifecycleElement widget hashcode${widget.hashCode}");
print("LifecycleElement hashcode${this.hashCode}");
print("LifecycleElement mount");
super.mount(parent, newSlot);
assert(_child == null);
print("LifecycleElement firstBuild");
performRebuild();
}

最终(启动,点击按钮两次的效果)运行效果如下:

image

两次运行Widget保持一致,这就避免了Widget的重建

小结

经过测试我们发现:

  • Widget的创建可以做到复用,通过const修饰
  • Element并没有复用,其实原因应该是在于isShow为false的时候导致其被deactivate 然后unmount,从Element树种被移除掉。
  • 有的人肯定有些疑问,怎么全程没看到activate呢?它不应该属于生命周期的一部分吗?这个就需要用到Key了,在接下来的课程里,讲到Key的时候,我们再详细的学习。

总结

本期我们对Widget,Element有了一个详细的认知,但其实它还有一个State类(StatefulWidget的核心实现)和RenderObject类,这两个下期我再分析。