Flutter 布局 - UI - 官方文档 - 学习速记(一)

  • 设置crossAxisAlignment属性值为CrossAxisAlignment.start,这会将该列中的子项左对齐。
  • 获取主题色:Theme.of(context).primaryColor;
     // The theme depends on the BuildContext because different parts of the tree
        // can have different themes.  The BuildContext indicates where the build is
        // taking place and therefore which theme to use.
  • 在行的主轴方向通过 MainAxisAlignment.spaceEvenly 平均的分配每个列占据的行空间
  • softwrap属性表示文本是否应在软换行符(例如句点或逗号)之间断开。
  • 将Column(列)放入Expanded中会拉伸该列以使用该行中的所有剩余空闲空间。
  • Container也是一个widget,允许您自定义其子widget。如果要添加填充,边距,边框或背景色,请使用Container来设置
  • Flutter布局机制的核心就是widget。在Flutter中,几乎所有东西都是一个widget - 甚至布局模型都是widget。您在Flutter应用中看到的图像、图标和文本都是widget。 甚至你看不到的东西也是widget,例如行(row)、列(column)以及用来排列、约束和对齐这些可见widget的网格(grid)。
  • 对于Material应用程序,您可以将widget直接添加到body属性中;(Scafford等)
  • 默认情况下,非Material应用程序不包含AppBar,标题或背景颜色。 如果您想在非Material应用程序中使用这些功能,您必须自己构建它们。此应用程序将背景颜色更改为白色,将文本更改为深灰色以模仿Material应用程序。
  • 您可以控制行或列如何使用mainAxisAlignmentcrossAxisAlignment属性来对齐其子项。 对于行(Row)来说,主轴是水平方向,横轴垂直方向。对于列(Column)来说,主轴垂直方向,横轴水平方向。
  • Expanded widget具有一个flex属性,它是一个整数,用于确定widget的弹性系数,默认弹性系数是1。
  • 默认情况下,行或列沿着其主轴会尽可能占用尽可能多的空间,但如果要将孩子紧密聚集在一起,可以将mainAxisSize设置为MainAxisSize.min。显示效果 --> [    ****    ]
 // DefaultTextStyle.merge可以允许您创建一个默认的文本样式,该样式会被其
    // 所有的子节点继承
    var iconList = DefaultTextStyle.merge(
      style: descTextStyle,
      child: new Container(
        padding: new EdgeInsets.all(20.0),
        child: new Row(
          mainAxisAlignment: MainAxisAlignment.spaceEvenly,
          children: [
            new Column(
              children: [
                new Icon(Icons.kitchen, color: Colors.green[500]),
                new Text('PREP:'),
                new Text('25 min'),
              ],
            ),
            new Column(
              children: [
                new Icon(Icons.timer, color: Colors.green[500]),
                new Text('COOK:'),
                new Text('1 hr'),
              ],
            ),
            new Column(
              children: [
                new Icon(Icons.restaurant, color: Colors.green[500]),
                new Text('FEEDS:'),
                new Text('4-6'),
              ],
            ),
          ],
        ),
  • widget分为两类:widgets library中的标准widget和Material Components library中的专用widget 。 任何应用程序都可以使用widgets library中的widget,但只有Material应用程序可以使用Material Components库。
  • 以使用SizedBox来限制Card的大小
  • Widget描述了他们的视图在给定其当前配置和状态时应该看起来像什么。当widget的状态发生变化时,widget会重新构建UI,Flutter会对比前后变化的不同, 以确定底层渲染树从一个状态转换到下一个状态所需的最小更改(译者语:类似于React/Vue中虚拟DOM的diff算法)。
void main() {
  runApp(
    new Center(
      child: new Text(
        'Hello, world!',
        textDirection: TextDirection.ltr,
      ),
    ),
  );
}
该runApp函数接受给定的Widget并使其成为widget树的根。 在此示例中,widget树由两个widget:Center(及其子widget)和Text组成。框架强制根widget覆盖整个屏幕,这意味着文本“Hello, world”会居中显示在屏幕上。
  • 一个widget通常由一些较低级别widget组成。Flutter框架将依次构建这些widget,直到构建到最底层的子widget时,这些最低层的widget通常为RenderObject,它会计算并描述widget的几何形状。
  • Stack允许子 widget 堆叠, 你可以使用 Positioned 来定位他们相对于Stack的上下左右四条边的位置。
  • container 可以装饰为一个BoxDecoration, 如 background、一个边框、或者一个阴影。 Container 也可以具有边距(margins)、填充(padding)和应用于其大小的约束(constraints)。另外, Container可以使用矩阵在三维空间中对其进行变换。
  • 请确保在pubspec.yaml文件中,将flutter的值设置为:uses-material-design: true。这允许我们可以使用一组预定义Material icons。
name: my_app
flutter:
  uses-material-design: true
  • 每个Flutter应用必须有一个main函数,作为入口;
  • Material应用程序以MaterialApp widget开始, 该widget在应用程序的根部创建了一些有用的widget,其中包括一个Navigator, 它管理由字符串标识的Widget栈(即页面路由栈)。Navigator可以让您的应用程序在页面之间的平滑的过渡。 是否使用MaterialApp完全是可选的,但是使用它是一个很好的做法。
  • 许多widget都会使用一个GestureDetector为其他widget提供可选的回调。 例如,IconButton、 RaisedButton、 和FloatingActionButton ,它们都有一个onPressed回调,它会在用户点击该widget时被触发。
  • 无状态widget从它们的父widget接收参数, 它们被存储在final型的成员变量中。 当一个widget被要求构建时,它使用这些存储的值作为参数来构建widget。
class CounterDisplay extends StatelessWidget {
  CounterDisplay({this.count});

  final int count;

  @override
  Widget build(BuildContext context) {
    return new Text('Count: $count');
  }
}
  • 为了更有趣的方式对用户输入做出反应 - 应用程序通常会携带一些状态。 Flutter使用StatefulWidgets来满足这种需求。StatefulWidgets是特殊的widget,它知道如何生成State对象,然后用它来保持状态。 
  • 和RN的方法类似,Flutter也是通过setState方法,通知FrameWork进行页面的重新渲染&刷新; - StatefulWidgets;
  • 为什么StatefulWidget和State是单独的对象。
在Flutter中,这两种类型的对象具有不同的生命周期: Widget是临时对象,用于构建当前状态下的应用程序,而State对象在多次调用build()之间保持不变,允许它们记住信息(状态)。
  • 在Flutter中,事件流是“向上”传递的,而状态流是“向下”传递的(译者语:这类似于React/Vue中父子组件通信的方式:子widget到父widget是通过事件通信,而父到子是通过状态),重定向这一流程的共同父元素是State。
class CounterDisplay extends StatelessWidget {
  CounterDisplay({this.count});

  final int count;

  @override
  Widget build(BuildContext context) {
    return new Text('Count: $count');
  }
}

class CounterIncrementor extends StatelessWidget {
  CounterIncrementor({this.onPressed});

  final VoidCallback onPressed;

  @override
  Widget build(BuildContext context) {
    return new RaisedButton(
      onPressed: onPressed,
      child: new Text('Increment'),
    );
  }
}

class Counter extends StatefulWidget {
  @override
  _CounterState createState() => new _CounterState();
}

class _CounterState extends State {
  int _counter = 0;

  void _increment() {
    setState(() {
      ++_counter;
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Row(children: [
      new CounterIncrementor(onPressed: _increment),
      new CounterDisplay(count: _counter),
    ]);
  }
}
  • Flutter框架会将新构建的widget与先前构建的widget进行比较,并仅将差异部分应用于底层RenderObject
  •   // The framework calls createState the first time a widget appears at a given
      // location in the tree. If the parent rebuilds and uses the same type of
      // widget (with the same key), the framework will re-use the State object
      // instead of creating a new State object.
  • 在StatefulWidget调用createState之后,框架将新的状态对象插入树中,然后调用状态对象的initState。 子类化State可以重写initState,以完成仅需要执行一次的工作。 例如,您可以重写initState以配置动画或订阅platform services。initState的实现中需要调用super.initState

    当一个状态对象不再需要时,框架调用状态对象的dispose。 您可以覆盖该dispose方法来执行清理工作。例如,您可以覆盖dispose取消定时器或取消订阅platform services。 dispose典型的实现是直接调用super.dispose

  • 您可以使用key来控制框架将在widget重建时与哪些其他widget匹配。默认情况下,框架根据它们的runtimeType和它们的显示顺序来匹配。 使用key时,框架要求两个widget具有相同的keyruntimeType

    Key在构建相同类型widget的多个实例时很有用。

  • stateless widget 没有内部状态. Icon、 IconButton, 和Text 都是无状态widget, 他们都是 StatelessWidget的子类。

    stateful widget 是动态的. 用户可以和其交互 (例如输入一个表单、 或者移动一个slider滑块),或者可以随时间改变 (也许是数据改变导致的UI更新). Checkbox, Radio, Slider,InkWell, Form, and TextField 都是 stateful widgets, 他们都是 StatefulWidget的子类。

  • StatefulWidget:当widget的状态改变时,状态对象调用setState(),告诉框架重绘widget。

  • 以下是管理状态的最常见的方法:

  1. widget管理自己的state
  2. 父widget管理 widget状态(类似RN的状态提升)
  3. 混搭管理(父widget和widget自身都管理状态))
  • 如何决定使用哪种管理方法?以下原则可以帮助您决定:

  1. 如果状态是用户数据,如复选框的选中状态、滑块的位置,则该状态最好由父widget管理
  2. 如果所讨论的状态是有关界面外观效果的,例如动画,那么状态最好由widget本身来管理.

如果有疑问,首选是在父widget中管理状态。

// ParentWidget 为 TapboxB 管理状态.

//------------------------ ParentWidget --------------------------------

class ParentWidget extends StatefulWidget {
  @override
  _ParentWidgetState createState() => new _ParentWidgetState();
}

class _ParentWidgetState extends State {
  bool _active = false;

  void _handleTapboxChanged(bool newValue) {
    setState(() {
      _active = newValue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return new Container(
      child: new TapboxB(
        active: _active,
        onChanged: _handleTapboxChanged,
      ),
    );
  }
}

//------------------------- TapboxB ----------------------------------

class TapboxB extends StatelessWidget {
  TapboxB({Key key, this.active: false, @required this.onChanged})
      : super(key: key);

  final bool active;
  final ValueChanged onChanged;

  void _handleTap() {
    onChanged(!active);
  }

  Widget build(BuildContext context) {
    return new GestureDetector(
      onTap: _handleTap,
      child: new Container(
        child: new Center(
          child: new Text(
            active ? 'Active' : 'Inactive',
            style: new TextStyle(fontSize: 32.0, color: Colors.white),
          ),
        ),
        width: 200.0,
        height: 200.0,
        decoration: new BoxDecoration(
          color: active ? Colors.lightGreen[700] : Colors.grey[600],
        ),
      ),
    );
  }
}
  • 在Android中,View是屏幕上显示的所有内容的基础, 按钮、工具栏、输入框等一切都是View。 在Flutter中,View相当于是Widget。然而,与View相比,Widget有一些不同之处。 首先,Widget仅支持一帧,并且在每一帧上,Flutter的框架都会创建一个Widget实例树(译者语:相当于一次性绘制整个界面)。 相比之下,在Android上View绘制结束后,就不会重绘,直到调用invalidate时才会重绘。

    与Android的视图层次系统不同(在framework改变视图),而在Flutter中的widget是不可变的,这允许widget变得超级轻量。

  • 这里要注意的重要一点是无状态和有状态 widget的核心特性是相同的。每一帧它们都会重新构建,不同之处在于StatefulWidget有一个State对象,它可以跨帧存储状态数据并恢复它。

  • 记住这个规则:如果一个widget发生了变化(例如用户与它交互),它就是有状态的。但是,如果一个子widget对变化做出反应,而其父widget对变化没有反应,那么包含的父widget仍然可以是无状态的widget。

  • 在Android中,您通过XML编写布局,但在Flutter中,您可以使用widget树来编写布局。

  • 在Android中,您可以从父级控件调用addChild或removeChild以动态添加或删除View。 在Flutter中,因为widget是不可变的,所以没有addChild。相反,您可以传入一个函数,该函数返回一个widget给父项,并通过布尔值控制该widget的创建。

      _getToggleChild() {
        if (toggle) {
          return new Text('Toggle One');
        } else {
          return new MaterialButton(onPressed: () {}, child: new Text('Toggle Two'));
        }
      }
  • React Native:【Es5/6 + React(理解JSX/State/props)】;Flutter:【Dart + 一切皆Widget】;两者有相似之处;
  • Flutter有两个类可以帮助您绘制画布,CustomPaint和CustomPainter,它们实现您的算法以绘制到画布。

  • 在Android中,您通常会继承View或已经存在的某个控件,然后覆盖其绘制方法来实现自定义View。

    在Flutter中,一个自定义widget通常是通过组合其它widget来实现的,而不是继承。

  • Intent在Flutter中等价于什么?Flutter不具有Intents的概念,但如果需要的话,Flutter可以通过Native整合来触发Intents。

    要在Flutter中切换屏幕,您可以访问路由以绘制新的Widget。 管理多个屏幕有两个核心概念和类:Route 和 Navigator。Route是应用程序的“屏幕”或“页面”的抽象(可以认为是Activity), Navigator是管理Route的Widget。Navigator可以通过push和pop route以实现页面切换。和Android相似,您可以在AndroidManifest.xml中声明您的Activities,在Flutter中,您可以将具有指定Route的Map传递到顶层MaterialApp实例;

    void main() {
      runApp(new MaterialApp(
        home: new MyAppHome(), // becomes the route named '/'
        routes:  {
          '/a': (BuildContext context) => new MyPage(title: 'page A'),
          '/b': (BuildContext context) => new MyPage(title: 'page B'),
          '/c': (BuildContext context) => new MyPage(title: 'page C'),
        },
      ));
    }

然后,您可以通过Navigator来切换到命名路由的页面。

Navigator.of(context).pushNamed('/b');
  • Dart是单线程执行模型,支持Isolates(在另一个线程上运行Dart代码的方式)、事件循环和异步编程。 除非您启动一个Isolate,否则您的Dart代码将在主UI线程中运行,并由事件循环驱动(译者语:和JavaScript一样)。例如,您可以在UI线程上运行网络请求代码而不会导致UI挂起(译者语:因为网络请求是异步的):
    loadData() async {
      String dataURL = "https://jsonplaceholder.typicode.com/posts";
      http.Response response = await http.get(dataURL);
      setState(() {
        widgets = JSON.decode(response.body);
      });
    }
    要更新UI,您可以调用setState,这会触发build方法再次运行并更新数据。
    
    由于Flutter是单线程的,运行一个事件循环(如Node.js),所以您不必担心线程管理或者使用AsyncTasks、IntentServices。
    
    要异步运行代码,可以将函数声明为异步函数,并在该函数中等待这个耗时任务
  • 在Android上,当您继承AsyncTask时,通常会覆盖3个方法,OnPreExecute、doInBackground和onPostExecute。 在Flutter中没有这种模式的等价物,因为您只需等待一个长时间运行的函数,而Dart的事件循环将负责其余的事情。

    但是,有时您可能需要处理大量数据,导致UI可能会挂起。

    在这种情况下,与AsyncTask一样,在Flutter中,可以利用多个CPU内核来执行耗时或计算密集型任务。这是通过使用Isolates来完成的。

    是一个独立的执行线程,它运行时不会与主线程共享任何内存。这意味着你不能从该线程访问变量或通过调用setState来更新你的UI。

  • okhttp在Flutter中的等价物是什么?“http”package - https://pub.dartlang.org/packages/http ,虽然“http” package 没有实现OkHttp的所有功能,但“http” package 抽象出了许多常用的API,可以简单有效的发起网络请求。可以通过dart:convert库进行JSON的解析;

  • 在Android中,您可以在Gradle文件来添加依赖项。

    在Flutter中,虽然在Flutter项目中的Android文件夹下有Gradle文件,但只有在添加平台相关所需的依赖关系时才使用这些文件。 否则,应该使用pubspec.yaml声明用于Flutter的外部依赖项。

  • 在Android中,您可以覆盖Activity的方法来捕获Activity的生命周期回调。

    在Flutter中您可以通过挂接到WidgetsBinding观察并监听didChangeAppLifecycleState更改事件来监听生命周期事件

    您可以监听到的生命周期事件是

  1. resumed - 应用程序可见并响应用户输入。这是来自Android的onResume
  2. inactive - 应用程序处于非活动状态,并且未接收用户输入。此事件在Android上未使用,仅适用于iOS
  3. paused - 应用程序当前对用户不可见,不响应用户输入,并在后台运行。这是来自Android的暂停
  4. suspending - 该应用程序将暂时中止。这在iOS上未使用
  • 开发依赖项是不包含在我们的应用程序源代码中的依赖项。
  • 技术类型 UI渲染方式 性能 开发效率 动态化 框架代表
    H5+原生 WebView渲染 一般 ✔️ Cordova、Ionic
    JavaScript+原生渲染 原生控件渲染 ✔️ RN、Weex
    自绘UI+原生 调用系统API渲染 Flutter高, QT低 默认不支持 QT、Flutter

    上表中开发语言主要指UI的开发语言,动态化主要指是否支持动态下发代码和是否支持热更新。值得注意的是Flutter的Release包默认是使用Dart AOT模式编译的,所以不支持动态化,但Dart还有JIT或snapshot运行方式,这些模式都是支持动态化的

  • Flutter:Skia + 基于JIT的快速开发周期 + 基于AOT的发布包;

  • 可以简单认为Stateful widget 和Stateless widget有两点不同:

Stateful widget可以拥有状态,这些状态在widget生命周期中是可以变的,而Stateless widget是不可变的。

Stateful widget至少由两个类组成:

  1. 一个StatefulWidget类。
  2. 一个 State类; StatefulWidget类本身是不变的,但是 State类中持有的状态在widget生命周期中可能会发生变化。
  • setState方法的作用是通知Flutter框架,有状态发生了改变,Flutter框架收到通知后,会执行build方法来根据新的状态重新构建界面, Flutter 对此方法做了优化,使重新执行变的部分,所以你可以重新构建任何需要更新的东西,而无需分别去修改各个widget。
  • 路由相关
 Navigator.push( context,
           new MaterialPageRoute(builder: (context) {
                  return new NewRoute();
             }));
          },
         )
MaterialPageRoute继承自PageRoute类,PageRoute类是一个抽象类,表示占有整个屏幕空间的一个模态路由页面,它还定义了路由构建及切换时过渡动画的相关接口及属性。MaterialPageRoute 是Material组件库的一个Widget,它可以针对不同平台,实现与平台页面切换动画风格一致的路由切换动画。

 MaterialPageRoute({
    WidgetBuilder builder,
    RouteSettings settings,
    bool maintainState = true,
    bool fullscreenDialog = false,
  })
Navigator是一个路由管理的widget,它通过一个栈来管理一个路由widget集合。
- Future push(BuildContext context, Route route)
将给定的路由入栈(即打开新的页面),返回值是一个Future对象,用以接收新路由出栈(即关闭)时的返回数据。【可以理解为Android中的startActivityForResult】
- bool pop(BuildContext context, [ result ])
将栈顶路由出栈,result为页面关闭时返回给上一个页面的数据。
  1. 命名路由:即给路由起一个名字,然后可以通过路由名字直接打开新的路由。
要想使用命名路由,我们必须先提供并注册一个路由表(routing table),这样应用程序才知道哪个名称与哪个路由Widget对应。路由表的定义如下:

Map routes;

示例:
return new MaterialApp(
  title: 'Flutter Demo',
  theme: new ThemeData(
    primarySwatch: Colors.blue,
  ),
  //注册路由表
  routes:{
   "new_page":(context)=>NewRoute(),
  } ,
  home: new MyHomePage(title: 'Flutter Demo Home Page'),
);

通过pushNamed进行路由跳转:
Future pushNamed(BuildContext context, String routeName,{Object arguments})

示例:
- 路由,并传递参数
Navigator.of(context).pushNamed("new_page", arguments: "hi");
- 获取参数
    var args=ModalRoute.of(context).settings.arguments
  • pubspec.yaml相关配置说明
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ^0.1.2

dev_dependencies:
  flutter_test:
    sdk: flutter

flutter:
  uses-material-design: true

dependencies:应用或包依赖的其它包或插件。
dev_dependencies:开发环境依赖的工具包(而不是flutter应用本身依赖的包)。
flutter:flutter相关的配置选项。
--------------------
除了可以添加最常用的pub仓库依赖,还可以添加本地及git依赖
--------------------
flutter:
  assets:
    - images/my_icon.png
    - images/background.png
assets指定应包含在应用程序中的文件。 每个asset都通过相对于pubspec.yaml文件所在位置的显式路径进行标识。asset的声明顺序是无关紧要的。asset的实际目录可以是任意文件夹(在本示例中是images)。
  •  Dart异常捕获

Flutter 框架为我们在很多关键的方法进行了异常捕获。

如果我们想上报异常,只需要提供一个自定义的错误处理回调即可,如:

void main() {
  FlutterError.onError = (FlutterErrorDetails details) {
    reportError(details);
  };
 ...
}

 这样我们就可以处理那些Flutter为我们捕获的异常了。

在Flutter中,还有一些Flutter没有为我们捕获的异常,如调用空对象方法异常、Future中的异常。在Dart中,异常分两类:同步异常和异步异常,同步异常可以通过try/catch捕获,而异步异常则比较麻烦,如下面的代码是捕获不了Future的异常的:

try{
    Future.delayed(Duration(seconds: 1)).then((e) => Future.error("xxx"));
}catch (e){
    print(e)
}

 Dart中有一个runZoned(...) 方法,可以给执行对象指定一个Zone。Zone表示一个代码执行的环境范围,为了方便理解,读者可以将Zone类比为一个代码执行沙箱,不同沙箱的之间是隔离的,沙箱可以捕获、拦截或修改一些代码行为,如Zone中可以捕获日志输出、Timer创建、微任务调度的行为,同时Zone也可以捕获所有未处理的异常。

R runZoned(R body(), {
    Map zoneValues, // Zone 的私有数据,可以通过实例zone[key]获取,可以理解为每个“沙箱”的私有数据。
    ZoneSpecification zoneSpecification,// Zone的一些配置,可以自定义一些代码行为,比如拦截日志输出行为等
    Function onError,// Zone中未捕获异常处理回调,如果开发者提供了onError回调或者通过ZoneSpecification.handleUncaughtError指定了错误处理回调,那么这个zone将会变成一个error-zone,该error-zone中发生未捕获异常(无论同步还是异步)时都会调用开发者提供的回调
})

 这样一来,结合上面的FlutterError.onError我们就可以捕获我们Flutter应用中全部错误了!需要注意的是,error-zone内部发生的错误是不会跨越当前error-zone的边界的,如果想跨越error-zone边界去捕获异常,可以通过共同的“源”zone来捕获,如:

var future = new Future.value(499);
runZoned(() {
    var future2 = future.then((_) { throw "error in first error-zone"; });
    runZoned(() {
        var future3 = future2.catchError((e) { print("Never reached!"); });
    }, onError: (e) { print("unused error handler"); });
}, onError: (e) { print("catches error of first error-zone."); });

 

你可能感兴趣的:(跨平台相关)