Flutter学习之构建、布局及绘制三部曲

前言

学习Fullter也有些时间了,写过不少demo,对一些常用的widget使用也比较熟练,但是总觉得对Flutter的框架没有一个大致的了解,碰到有些细节的地方又没有文档可以查询,例如在写UI时总不知道为什么container添加了child就变小了;widget中key的作用,虽然官方有解释但是凭空来讲的话有点难理解。所以觉得深入一点的了解Flutter框架还是很有必要的。

构建

初次构建

flutter的入口main方法直接调用了runApp(Widget app)方法,app参数就是我们的根视图的Widget,我们直接跟进runApp方法

void runApp(Widget app) {
 WidgetsFlutterBinding.ensureInitialized()//此方法是对flutter的框架做一些必要的初始化
 ..attachRootWidget(app)
 ..scheduleWarmUpFrame();
}

runApp方法先调用了WidgetsFlutterBinding.ensureInitialized()方法,这个方法是做一些必要的初始化

class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {
	 static WidgetsBinding ensureInitialized() {
	 if (WidgetsBinding.instance == null)
	 WidgetsFlutterBinding();
	 return WidgetsBinding.instance;
	 }
}

WidgetsFlutterBinding混入了不少的其他的Binding

  • BindingBase 那些单一服务的混入类的基类
  • GestureBinding framework手势子系统的绑定,处理用户输入事件
  • ServicesBinding 接受平台的消息将他们转换成二进制消息,用于平台与flutter的通信
  • SchedulerBinding 调度系统,用于调用Transient callbacks(Window.onBeginFrame的回调)、Persistent callbacks(Window.onDrawFrame的回调)、Post-frame callbacks(在Frame结束时只会被调用一次,调用后会被系统移除,在Persistent callbacks后Window.onDrawFrame回调返回之前执行)
  • PaintingBinding 绘制库的绑定,主要处理图片缓存
  • SemanticsBinding 语义化层与Flutter engine的桥梁,主要是辅助功能的底层支持
  • RendererBinding 渲染树与Flutter engine的桥梁
  • WidgetsBinding Widget层与Flutter engine的桥梁

以上是这些Binding的主要作用,在此不做过多赘述,WidgetsFlutterBinding.ensureInitialized()返回的是WidgetsBinding对象,然后马上调用了WidgetsBinding的attachRootWidget(app)方法,将我们的根视图的Widget对象穿进去,我们继续看attachRootWidget方法

void attachRootWidget(Widget rootWidget) {
 _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
 container: renderView,
 debugShortDescription: '[root]',
 child: rootWidget
 ).attachToRenderTree(buildOwner, renderViewElement);
}

创建了一个RenderObjectToWidgetAdapter,让后直接调用它的attachToRenderTree方法,BuildOwner是Widget framework的管理类

RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [RenderObjectToWidgetElement<T> element]) {
 if (element == null) {
 owner.lockState(() {
 element = createElement();
 assert(element != null);
 element.assignOwner(owner);
 });
 owner.buildScope(element, () {
 element.mount(null, null);
 });
 } else {
 element._newWidget = this;
 element.markNeedsBuild();
 }
 return element;
}

element为空,owner先锁定状态,然后调用了RenderObjectToWidgetAdapter的createElement()返回了RenderObjectToWidgetElement对象,让后将owner赋值给element(assignOwner方法),让后就是owner调用buildScope方法

void buildScope(Element context, [VoidCallback callback]) {
 if (callback == null && _dirtyElements.isEmpty)
 return;
 Timeline.startSync('Build', arguments: timelineWhitelistArguments);
 try {
 _scheduledFlushDirtyElements = true;
 if (callback != null) {
 _dirtyElementsNeedsResorting = false;
 try {
 callback();
 } finally {}
 }
 ...
 }

省略了部分以及后续代码,可以看到buildScope方法首先就调用了callback(就是element.mount(null, null)方法),回到RenderObjectToWidgetElement的mount方法

@override
void mount(Element parent, dynamic newSlot) {
 assert(parent == null);
 super.mount(parent, newSlot);
 _rebuild();
}

首先super.mount(parent, newSlot)调用了RootRenderObjectElement的mount方法(只是判定parent和newSlot都为null),让后又继续向上调用了RenderObjectElement中的mount方法

@override
void mount(Element parent, dynamic newSlot) {
 super.mount(parent, newSlot);
 _renderObject = widget.createRenderObject(this);
 attachRenderObject(newSlot);
 _dirty = false;
}

RenderObjectElement中的mount方法又调用了Element的mount方法

@mustCallSuper
void mount(Element parent, dynamic newSlot) {
 _parent = parent;
 _slot = newSlot;
 _depth = _parent != null ? _parent.depth + 1 : 1;
 _active = true;
 if (parent != null) // Only assign ownership if the parent is non-null
 _owner = parent.owner;
 if (widget.key is GlobalKey) {
 final GlobalKey key = widget.key;
 key._register(this);
 }
 _updateInheritance();
}

Element的mount方法其实就是进行了一些赋值,以确认当前Element在整个树种的位置,让后回到RenderObjectElement中的mount方法,调用了widget.createRenderObject(this)方法,widget是RenderObjectToWidgetAdapter的实例,它返回的是RenderObjectWithChildMixin对象,让后调用attachRenderObject方法

@override
void attachRenderObject(dynamic newSlot) {
 assert(_ancestorRenderObjectElement == null);
 _slot = newSlot;
 _ancestorRenderObjectElement = _findAncestorRenderObjectElement();//获取此RenderObjectElement最近的RenderObjectElement对象
 _ancestorRenderObjectElement?.insertChildRenderObject(renderObject, newSlot);//将renderObject插入RenderObjectElement中
 final ParentDataElement<RenderObjectWidget> parentDataElement = _findAncestorParentDataElement();
 if (parentDataElement != null)
 _updateParentData(parentDataElement.widget);
}

///RenderObjectToWidgetElement中的insertChildRenderObject方法,简单将子RenderObject赋值给父RenderObject的child字段
@override
void insertChildRenderObject(RenderObject child, dynamic slot) {
 assert(slot == _rootChildSlot);
 assert(renderObject.debugValidateChild(child));
 renderObject.child = child;
}

Element的mount方法确定当前Element在整个树种的位置并插入,RenderObjectElement中的mount方法来创建RenderObject对象并将其插入到渲染树中,让后再回到RenderObjectToWidgetElement方法,mount之后调用_rebuild()方法, _rebuild()方法中主要是调用了Element的updateChild方法

@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
 if (newWidget == null) {//当子Widget没有的时候,直接将child deactivate掉
 if (child != null)
 deactivateChild(child);
 return null;
 }
 if (child != null) {//有子Element的时候
 if (child.widget == newWidget) {//Widget没有改变
 if (child.slot != newSlot)//再判断slot有没有改变,没有则不更新slot
 updateSlotForChild(child, newSlot);//更新child的slot
 return child;//返回child
 }
 if (Widget.canUpdate(child.widget, newWidget)) {//Widget没有改变,再判断Widget能否update,如果能还是重复上面的步骤
 if (child.slot != newSlot)
 updateSlotForChild(child, newSlot);
 child.update(newWidget);
 return child;
 }
 deactivateChild(child);//如果不能更新的话,直接将child deactivate掉,然后在inflateWidget(newWidget, newSlot)创建新的Element
 }
 return inflateWidget(newWidget, newSlot);//根据Widget对象以及slot创建新的Element
}

由于我们是第一次构建,child是null,所以就直接走到inflateWidget方法创建新的Element对象,跟进inflateWidget方法

@protected
Element inflatinflateWidgeteWidget(Widget newWidget, dynamic newSlot) {
 final Key key = newWidget.key;
 if (key is GlobalKey) {//newWidget的key是GlobalKey
 final Element newChild = _retakeInactiveElement(key, newWidget);//复用Inactive状态的Element
 if (newChild != null) {
 newChild._activateWithParent(this, newSlot);//activate 此Element(将newChild出入到Element树)
 final Element updatedChild = updateChild(newChild, newWidget, newSlot);//直接将newChild更新
 return updatedChild;//返回更新后的Element
 }
 }
 final Element newChild = newWidget.createElement();//调用createElement()进行创建
 newChild.mount(this, newSlot);//继续调用newChild Element的mount方法(如此就行一直递归下去,当递归完成,整个构建过程也就结束了)
 return newChild;//返回子Element
}

inflateWidget中其实就是通过Widget得到Element对象,让后继续调用子Element的mount的方将进行递归。

不同的Element,mount的实现会有所不同,我们看一下比较常用的StatelessElement、StatefulElement,他们的mount方法实现在ComponentElement中

@override
void mount(Element parent, dynamic newSlot) {
 super.mount(parent, newSlot);
 _firstBuild();
}

void _firstBuild() {
 rebuild();//调用了Element的rebuild()方法
}

//Element的rebuild方法,通常被三处地方调用
//1.当BuildOwner.scheduleBuildFor被调用标记此Element为dirty时
//2.当Element第一次构建由mount方法去调用
//3.当Widget改变时,被update方法调用
void rebuild() {
 if (!_active || !_dirty)
 return;
 performRebuild();//调用performRebuild方法(抽象方法)
}

//ComponentElement的performRebuild实现
@override
void performRebuild() {
 Widget built;
 try {
 built = build();//构建Widget(StatelessElement直接调用build方法,StatefulElement直接调用state.build方法)
 } catch (e, stack) {
 built = ErrorWidget.builder(_debugReportException('building $this', e, stack));//有错误的化就创建一个ErrorWidget
 } finally {
 _dirty = false;
 }
 try {
 _child = updateChild(_child, built, slot);//让后还是根据Wdiget来更新子Element
 } catch (e, stack) {
 built = ErrorWidget.builder(_debugReportException('building $this', e, stack));
 _child = updateChild(null, built, slot);
 }
}

再看一看MultiChildRenderObjectElement的mount方法

@override
void mount(Element parent, dynamic newSlot) {
 super.mount(parent, newSlot);
 _children = List<Element>(widget.children.length);
 Element previousChild;
 for (int i = 0; i < _children.length; i += 1) {
 final Element newChild = inflateWidget(widget.children[i], previousChild);//遍历children直接inflate根据Widget创建新的Element
 _children[i] = newChild;
 previousChild = newChild;
 }
}

可以看到不同的Element构建方式会有些不同,Element(第一层Element)的mount方法主要是确定当前Element在整个树种的位置并插入;ComponentElement(第二层)的mount方法先构建Widget树,让后再递归更新(包括重用,更新,直接创建inflate)其Element树;RenderObjectElement(第二层)中的mount方法来创建RenderObject对象并将其插入到渲染树中。MultiChildRenderObjectElement(RenderObjectElement的子类)在RenderObjectElement还要继续创建children Element。

总结:首先是由WidgetBinding创建RenderObjectToWidgetAdapter然后调用它的attachToRenderTree方法,创建了RenderObjectToWidgetElement对象,让后将它mount(调用mount方法),mount方法中调用的_rebuild,继而调用updateChild方法,updateChild会进行递归的更新Element树,若child没有则需要重新创建新的Element,让后将其mount进Element树中(如果是RenderobjectElement的化,mount的过程中会去创建RenderObject对象,并插入到RenderTree)。

通过setState触发构建

通常我们在应用中要更新状态都是通过State中的setState方法来触发界面重绘,setState方法就是先调用了callback让后调用该State的Element对象的markNeedsBuild方法,markNeedsBuild中将Element标记为dirty并通过BuildOwner将其添加到dirty列表中并调用onBuildScheduled回调(在WidgetsBinding初始化时设置的,它回去调用window.scheduleFrame方法),让后window的onBeginFrame,onDrawFrame回调(在SchedulerBinding初始化时设置的,这两个回调会执行一些callback)会被调用,SchedulerBinding通过persisterCallbacks来调用到BuildOwner中buildScope方法。上面我们只看了buildScope的一部分,当通过setState方法来触发界面重绘时,buildScope的callBack为null

void buildScope(Element context, [VoidCallback callback]) {
 if (callback == null && _dirtyElements.isEmpty)
 return;
 Timeline.startSync('Build', arguments: timelineWhitelistArguments);
 try {
 _scheduledFlushDirtyElements = true;
 if (callback != null) {
 Element debugPreviousBuildTarget;
 _dirtyElementsNeedsResorting = false;
 try {
 callback();//调用callback
 } finally {}
 }
 _dirtyElements.sort(Element._sort);
 _dirtyElementsNeedsResorting = false;
 int dirtyCount = _dirtyElements.length;
 int index = 0;
 while (index < dirtyCount) {
 try {
 _dirtyElements[index].rebuild();//遍历dirtyElements并执行他们的rebuild方法来使这些Element进行rebuild
 } catch (e, stack) {}
 index += 1;
 if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting) {
 _dirtyElements.sort(Element._sort);
 _dirtyElementsNeedsResorting = false;
 dirtyCount = _dirtyElements.length;
 while (index > 0 && _dirtyElements[index - 1].dirty) {
 index -= 1;
 }
 }
 }
 } finally {
 for (Element element in _dirtyElements) {//最后解除Element的dirty标记,以及清空dirtyElements
 assert(element._inDirtyList);
 element._inDirtyList = false;
 }
 _dirtyElements.clear();
 _scheduledFlushDirtyElements = false;
 _dirtyElementsNeedsResorting = null;
 Timeline.finishSync();
 }
}

很明显就是对dirtyElements中的元素进行遍历并且对他们进行rebuild。

布局

window通过scheduleFrame方法会让SchedulerBinding来执行handleBeginFrame方法(执行transientCallbacks)和handleDrawFrame方法(执行persistentCallbacks,postFrameCallbacks),在RendererBinding初始化时添加了_handlePersistentFrameCallback,它调用了核心的绘制方法drawFrame。

@protected
void drawFrame() {
 assert(renderView != null);
 pipelineOwner.flushLayout();//布局
 pipelineOwner.flushCompositingBits();//刷新dirty的renderobject的数据
 pipelineOwner.flushPaint();//绘制
 renderView.compositeFrame(); // 将二进制数据发送给GPU
 pipelineOwner.flushSemantics(); // 将语义发送给系统
}

flushLayout触发布局,将RenderObject树的dirty节点通过调用performLayout方法进行逐一布局,我们先看一下RenderPadding中的实现

@override
void performLayout() {
 _resolve();//解析padding参数
 if (child == null) {//如果没有child,直接将constraints与padding综合计算得出自己的size
 size = constraints.constrain(Size(
  _resolvedPadding.left + _resolvedPadding.right,
  _resolvedPadding.top + _resolvedPadding.bottom
 ));
 return;
 }
 final BoxConstraints innerConstraints = constraints.deflate(_resolvedPadding);//将padding减去,生成新的约束innerConstraints
 child.layout(innerConstraints, parentUsesSize: true);//用新的约束去布局child
 final BoxParentData childParentData = child.parentData;
 childParentData.offset = Offset(_resolvedPadding.left, _resolvedPadding.top);//设置childParentData的offset值(这个值是相对于parent的绘制偏移值,在paint的时候传入这个偏移值)
 size = constraints.constrain(Size(//将constraints与padding以及child的sieze综合计算得出自己的size
 _resolvedPadding.left + child.size.width + _resolvedPadding.right,
 _resolvedPadding.top + child.size.height + _resolvedPadding.bottom
 ));
}

可以看到RenderPadding中的布局分两种情况。如果没有child,那么就直接拿parent传过来的约束以及padding来确定自己的大小;否则就先去布局child,让后再拿parent传过来的约束和padding以及child的size来确定自己的大小。

RenderPadding是典型的单child的RenderBox,我们看一下多个child的RenderBox。例如RenderFlow

@override
void performLayout() {
 size = _getSize(constraints);//直接先确定自己的size
 int i = 0;
 _randomAccessChildren.clear();
 RenderBox child = firstChild;
 while (child != null) {//遍历孩子
 _randomAccessChildren.add(child);
 final BoxConstraints innerConstraints = _delegate.getConstraintsForChild(i, constraints);//获取child的约束,此方法为抽象
 child.layout(innerConstraints, parentUsesSize: true);//布局孩子
 final FlowParentData childParentData = child.parentData;
 childParentData.offset = Offset.zero;
 child = childParentData.nextSibling;
 i += 1;
 }
}

可以看到RenderFlow的size直接就根据约束来确定了,并没去有先布局孩子,所以RenderFlow的size不依赖与孩子,后面依旧是对每一个child依次进行布局。

还有一种比较典型的树尖类型的RenderBox,LeafRenderObjectWidget子类创建的RenderObject对象都是,他们没有孩子,他们才是最终需要渲染的对象,例如

@override
void performLayout() {
 size = _sizeForConstraints(constraints);
}

非常简单就通过约束确定自己的大小就结束了。所以performLayout过程就是两点,确定自己的大小以及布局孩子。我们上面提到的都是RenderBox的子类,这些RenderObject约束都是通过BoxConstraints来完成,但是RenderSliver的子类的约束是通过SliverConstraints来完成,虽然他们对child的约束方式不同,但他们在布局过程需要执行的操作都是一致的。

绘制

布局完成了,PipelineOwner就通过flushPaint来进行绘制

void flushPaint() {
 try {
 final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
 _nodesNeedingPaint = <RenderObject>[];
 // 对dirty nodes列表进行排序,最深的在第一位
 for (RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
  assert(node._layer != null);
  if (node._needsPaint && node.owner == this) {
  if (node._layer.attached) {
   PaintingContext.repaintCompositedChild(node);
  } else {
   node._skippedPaintingOnLayer();
  }
  }
 }
 } finally {}
}

PaintingContext.repaintCompositedChild(node)会调用到child._paintWithContext(childContext, Offset.zero)方法,进而调用到child的paint方法,我们来看一下第一次绘制的情况,dirty的node就应该是RenderView,跟进RenderView的paint方法

@override
void paint(PaintingContext context, Offset offset) {
 if (child != null)
 context.paintChild(child, offset);//直接绘制child
}

自己没有什么绘制的内容,直接绘制child,再看一下RenderShiftedBox

@override
void paint(PaintingContext context, Offset offset) {
 if (child != null) {
 final BoxParentData childParentData = child.parentData;
 context.paintChild(child, childParentData.offset + offset);//直接绘制child
 }
}

好像没有绘制内容就直接递归的进行绘制child,那找一个有绘制内容的吧,我们看看RenderDecoratedBox

@override
void paint(PaintingContext context, Offset offset) {//Offset由parent去paintChild的时候传入,该值存放在child的parentdata字段中,该字段是BoxParentData或以下实例
 _painter ??= _decoration.createBoxPainter(markNeedsPaint);//获取painter画笔
 final ImageConfiguration filledConfiguration = configuration.copyWith(size: size);
 if (position == DecorationPosition.background) {//画背景
 _painter.paint(context.canvas, offset, filledConfiguration);//绘制过程,具体细节再painter中
 if (decoration.isComplex)
  context.setIsComplexHint();
 }
 super.paint(context, offset);//画child,里面直接调用了paintChild
 if (position == DecorationPosition.foreground) {//画前景
 _painter.paint(context.canvas, offset, filledConfiguration);
 if (decoration.isComplex)
  context.setIsComplexHint();
 }
}

如果自己有绘制内容,paint方法中的实现就应该包括绘制自己以及绘制child,如果没有孩子就只绘制自己的内容,看一下RenderImage

@override
void paint(PaintingContext context, Offset offset) {
 if (_image == null)
 return;
 _resolve();
 paintImage(//直接绘制Image,具体细节再此方法中
 canvas: context.canvas,
 rect: offset & size,
 image: _image,
 scale: _scale,
 colorFilter: _colorFilter,
 fit: _fit,
 alignment: _resolvedAlignment,
 centerSlice: _centerSlice,
 repeat: _repeat,
 flipHorizontally: _flipHorizontally,
 invertColors: invertColors,
 filterQuality: _filterQuality
 );
}

所以基本上绘制需要完成的流程就是,如果自己有绘制内容,paint方法中的实现就应该包括绘制自己以及绘制child,如果没有孩子就只绘制自己的内容,流程比较简单。

以上是自己学习的一些总结,如有错误之处请指出,大家共同探讨,觉得不错的话,点个赞呗!

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。

(0)

相关推荐

  • flutter窗口初始和绘制流程详析

    前言 环境: flutter sdk v1.7.8+hotfix.3@stable 对应 flutter engine: 54ad777f 这里关注的是C++层面的绘制流程,平台怎样驱动和响应绘制与渲染的过程,并不是Dart部分的渲染. 结合之前的分析,在虚拟机实例的构造函数中调用了一个重要方法DartUI::InitForGlobal() , 调用流程再罗列一下: DartVMRef::Create DartVMRef::DartVMRef DartVM::Create DartVMData:

  • Flutter学习之构建、布局及绘制三部曲

    前言 学习Fullter也有些时间了,写过不少demo,对一些常用的widget使用也比较熟练,但是总觉得对Flutter的框架没有一个大致的了解,碰到有些细节的地方又没有文档可以查询,例如在写UI时总不知道为什么container添加了child就变小了:widget中key的作用,虽然官方有解释但是凭空来讲的话有点难理解.所以觉得深入一点的了解Flutter框架还是很有必要的. 构建 初次构建 flutter的入口main方法直接调用了runApp(Widget app)方法,app参数就是

  • BootStrap学习系列之布局组件(下拉,按钮组[toolbar],上拉)

    布局组件 下拉按钮 <div class="dropdown"> <button class="btn dropdown-toggle" id='drop1' data-toggle="dropdown"> 帮助<span class="caret"></span> </button> <ul class="dropdown-menu" ro

  • Unreal学习之简单三角形的绘制详解

    目录 1. 概述 2. 详论 2.1 代码实现 2.2 解析:Component 2.3 解析:材质 2.4 解析:包围盒 2.5 解析:Section 3. 其他 1. 概述 之所以写这个绘制简单三角形的实例其实是想知道如何在Unreal中通过代码绘制自定义Mesh,如果你会绘制一个三角形,那么自然就会绘制复杂的Mesh了.所以这是很多图形工作者的第一课. 2. 详论 2.1 代码实现 Actor是Unreal的基本显示对象,有点类似于Unity中的GameObject或者OSG中的Node.

  • Flutter学习之创建一个内嵌的navigation详解

    目录 简介 搭建主Navigator 构建子路由 总结 简介 我们在flutter中可以使用Navigator.push或者Navigator.pushNamed方法来向Navigator中添加不同的页面,从而达到页面调整的目的. 一般情况下这样已经足够了,但是有时候我们有多个Navigator的情况下,上面的使用方式就不够用了.比如我们有一个主页面app的Navigator,然后里面有一个匹配好友的功能,这个功能有多个页面,因为匹配好友功能的多个页面实际上是一个完整的流程,所以这些页面需要被放

  • 如何在Flutter中嵌套Android布局

    效果 本文具体demo效果如下 开发 1.首先创建flutter项目,在项目中定义好flutter需要展示布局: @override Widget build(BuildContext context) { return Scaffold( body: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Expanded( child: Center( child: Text( 'Android按钮

  • R语言学习之线图的绘制详解

    目录 线图 单线图 多线图 横轴文本线图 线图 线图是反映趋势变化的一种方式,其输入数据一般也是一个矩阵. 单线图 假设有这么一个矩阵,第一列为转录起始位点及其上下游5 kb的区域,第二列为H3K27ac修饰在这些区域的丰度,想绘制一张线图展示. profile="Pos;H3K27ac -5000;8.7 -4000;8.4 -3000;8.3 -2000;7.2 -1000;3.6 0;3.6 1000;7.1 2000;8.2 3000;8.4 4000;8.5 5000;8.5"

  • Flutter学习LogUtil封装与实现实例详解

    目录 一. 为什么要封装打印类 二. 需要哪些类 三. 打印输出的抽象类 四. 格式化日志内容 格式化堆栈 堆栈裁切工具类 格式化堆栈信息 格式化JSON 五. 需要用到的常量 六. 为了控制多个打印器的设置做了一个配置类 七. Log的管理类 九. 调用LogUtil 十. 定义一个Flutter 控制台打印输出的方法 十一. 现在使用前初始化log打印器一次 使用 一. 为什么要封装打印类 虽然 flutter/原生给我们提供了日志打印的功能,但是超出一定长度以后会被截断 Json打印挤在一

  • Flutter学习之实现自定义themes详解

    目录 简介 MaterialApp中的themes 自定义themes的使用 总结 简介 一般情况下我们在flutter中搭建的app基本上都是用的是MaterialApp这种设计模式,MaterialApp中为我们接下来使用的按钮,菜单等提供了统一的样式,那么这种样式能不能进行修改或者自定义呢? 答案是肯定的,一起来看看吧. MaterialApp中的themes MaterialApp也是一种StatefulWidget,在MaterialApp中跟theme相关的属性有这样几个: fina

  • Flutter学习笔记(二)创建一个flutter项目

    目录 开发环境 实践 运行 网络环境配置 (1)进入packages\flutter_tools\gradle 文件夹,然后打开flutter.gradle文件.目录如下图所示: (2)进入 flutter\packages\flutter_tools\gradle 文件夹,然后打开resolve_dependencies.gradle文件,目录如下: 本文就是利用androidstudio创建一个flutter项目并且成功运行起来.其中运行的过程,可能涉及到网络环境配置的问题.觉得过于简单的朋

  • Flutter学习笔记(一)配置环境

    目录 背景 主题 当前环境 配置过程 下载flutter sdk 配置flutter sdk环境 背景 在日益内卷的开发圈子,多一项技能就意味着竞争力的提升,受到大环境的各种因素,所以有了这篇文章. 主题 本文主题,就是介绍如何配置flutter 当前环境 win10as2022.1.1版本jdk11 配置过程 下载flutter sdk 首先,从官网下载一个flutter的sdk,下载地址 博主当前使用版本为–flutter_windows_3.7.8-stable 配置flutter sdk

随机推荐