flutter开篇

2020-04-24  本文已影响0人  就是这么简简单单

下一篇 Flutter进阶

Flutter英文网
Flutter中文网
Widgets 目录
Flutter SDK
Flutter插件
DartPad 的官方地址:dartpad.dev 和国内镜像地址 dartpad.cn

安装Flutter,配置环境(https://flutterchina.club/get-started/install/

image.png

Flutter是什么?

Flutter是一个UI框架,是Google 2017 年 5 月发布的。可以快速在iOS和Android上构建高质量的原生用户界面(这是官方说的)。现在也可以在Web,Linux,Windows,MacOs以及小程序上运行(不够成熟,Flutter + V8/JsCore 开发小程序引擎)。Flutter是完全免费、开源的。简单来说就是,Flutter一行代码运行多端。

为什么选择Flutter?

先了解下背景

跨平台开发方案的三个时代根据实现方式的不同,业内常见的观点是将主流的跨平台方案划分为三个时代。
1.Web 容器时代:基于 Web 相关技术通过浏览器组件来实现界面及功能,典型的框架包括 Cordova(PhoneGap)、Ionic ,APICloud和微信小程序。
2.泛 Web 容器时代:采用类 Web 标准进行开发,但在运行时把绘制和渲染交由原生系统接管的技术,代表框架有 React Native、Weex 和快应用,广义的还包括天猫的 Virtual View 等。
3.自绘引擎时代:自带渲染引擎,客户端仅提供一块画布即可获得从业务逻辑到功能呈现的多端高度一致的渲染体验。Flutter。


参考维度.png

1.在做技术选型时,可以参考以上维度,从开发效率、技术栈、性能表现、维护成本和社区生态来进行综合考虑。比如,是否必须支持动态化?是只解决 Android、iOS 的跨端问题,还是要包括 Web?对性能要求如何?对多端体验的绝对一致性和维护成本是否有强诉求?

2.跨平台的方案有很多,传统的嵌套H5,React-Native(Facebook),Weex(阿里),这些核心都是通过Javascript开发,执行时需要Javascript解释器。Flutter使用C、C ++、Dart和Skia(2D渲染引擎)构建(都是自己的)。在IOS上,Flutter引擎的C/C ++代码使用LLVM编译,在Android下,Flutter引擎的C/C ++代码是用Android的NDK编译的。任何Dart代码都是AOT编译成本地代码的,Flutter应用程序依然使用本机指令集运行(不涉及解释器)。最终程序运行是以原生的方式进行,iOS则会打包成FlutterViewController,Android则会打包成FlutterView,因此,Flutter能达到原生应用一样的性能。
简单来说,就是跨平台,
高保真(一整套包括底层渲染逻辑和上层开发语言的完整解决方案),
高性能(代码执行效率和渲染性能媲美原生App的体验)。

3.Flutter能跨多端,Dart也可以开发后端(在闲鱼,我们如何用Dart做高效后端开发?)。

4.看flutter社区 https://github.com/flutter/flutter

flutter GitHub.png

5.国内大厂加持,美团,闲鱼,今日头条。
https://github.com/alibaba/fish-redux start 5.7K
https://github.com/alibaba/flutter_boost start 3.1K
https://github.com/alibaba/flutter-go start 18.8K

6.Flutter 是构建 Google 物联网操作系统 Fuchsia 的 SDK。

目前国内跨平台开发方案 React Native 和 Flutter是最均衡的。

那么选RN还是Flutter?

1.RN 仅适用于中低复杂度的低交互类页面。面对稍微复杂一点儿的交互和动画需求,开发者都需要 case by case 地去 review,甚至还可能要通过原生代码去扩展才能实现。这些因素,也就导致了虽然跨平台开发从移动端诞生之初就已经被多次提及,但到现在也没有被很好地解决。
Flutter 的设计理念比较先进,解决方案也相对彻底,在渲染能力的一致性以及性能上。
Flutter 是重写了一整套包括底层渲染逻辑和上层开发语言的完整解决方案。这样不仅可以保证视图渲染在 Android 和 iOS 上的高度一致性(即高保真),在代码执行效率和渲染性能上也可以媲美原生 App 的体验(即高性能)。
2.React Native 之类的框架,只是通过 JavaScript 虚拟机扩展调用系统组件,由 Android 和 iOS 系统进行组件的渲染;
Flutter 则是自己完成了组件渲染的闭环。

Flutter为什么用Dart,而不是JavaScript?

1.Dart运行时和编译器支持Flutter的两个关键特性的组合:基于JIT的快速开发周期:允许使用类型的语言进行形状更改和有状态的热重载;以及AOT编译器,可生成高效的ARM代码,可以快速启动并拥有可预测的生产部署性能。简单来说,Dart 因为同时支持 AOT 和 JIT,所以具有运行速度快、执行性能好

2.Google自己研发的,不侵权(和甲骨文一直在打官司),沟通方便。对于 Flutter 需要的一些语言新特性,能够快速在语法层面落地实现。

3.而如果选择了 JavaScript,就必须经过各种委员会和浏览器提供商漫长的决议。

4.从语言特性上来说,Dart是单线程,Dart 避免了抢占式调度和共享内存,可以在没有锁的情况下进行对象分配和垃圾回收,在性能方面表现相当不错。

Flutter缺点

1.渲染UI方面性能确实高,但是像webView、音视频播放、第三方分享、数据持久化存储等功能需要大量的组件支持,flutter引擎并不支持。原生操作系统底层的能力像Push、视频、地图肯定还得依靠原生来实现。那Flutter能不能解决这个问题,给全接管了?答案是不能,在移动端包的体积是硬伤。所以Flutter很依赖社区和第三方库。

2.看提的issue,问题不少。


image.png

3.有一定的学习门槛,但是不高,有编程基础,半天搞定。

4.跨平台的局限就是真正的多端一致性很难完全保证。这是硬伤。

5.需要同时会Android和IOS开发。

6.Flutter嵌套式编码风格,硬伤。

关键技术

学习 Flutter 之前,我们有必要了解下构建 Flutter 的关键技术,即 SkiaDart
1.底层图像渲染引擎 Skia,Flutter 只关心如何向 GPU 提供视图数据,而 Skia 就是它向 GPU 提供视图数据的好帮手。
Skia 是一款用 C++ 开发的、性能彪悍的 2D 图像绘制引擎,其前身是一个向量绘图软件。2005 年被 Google 公司收购后,因为其出色的绘制表现被广泛应用在 Chrome 和 Android 等核心产品上。Skia 在图形转换、文字渲染、位图渲染方面都表现卓越,并提供了开发者友好的 API。因此,架构于 Skia 之上的 Flutter,也因此拥有了彻底的跨平台渲染能力。通过与 Skia 的深度定制及优化,Flutter 可以最大限度地抹平平台差异,提高渲染效率与性能。底层渲染能力统一了,上层开发接口和功能体验也就随即统一了,开发者再也不用操心平台相关的渲染特性了。也就是说,Skia 保证了同一套代码调用在 Android 和 iOS 平台上的渲染效果是完全一致的。

2.Dart 的诞生正是要解决 JavaScript 存在的、在语言本质上无法改进的缺陷。2011 年 10 月,在丹麦召开的 GOTO 大会上,Google 发布了一种新的编程语言 Dart。
Dart 是少数同时支持 JIT(Just In Time,即时编译)和 AOT(Ahead of Time,运行前编译)的语言之一。
语言在运行之前通常都需要编译,JITAOT 则是最常见的两种编译模式。

Flutter模板

image.png

Flutter 工程实际上就是一个同时内嵌了 Android 和 iOS 原生子工程的父工程

TargetPlatform.android
TargetPlatform.fuchsia
TargetPlatform.ios
image.png
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  int _counter = 0;

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.display1,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ), // This trailing comma makes auto-formatting nicer for build methods.
    );
  }
}

第一部分是应用入口、应用结构以及页面结构,可以帮助你理解构建 Flutter 程序的基本结构和套路;第二部分则是页面布局、交互逻辑及状态管理,能够帮你理解 Flutter 页面是如何构建、如何响应交互,以及如何更新的。
MyApp 类继承自 StatelessWidget 类 createState 方法返回
MaterialApp 类是对构建 material 设计风格应用的组件封装框架
MyHomePage 是应用的首页,继承自 StatefulWidget 类。这,代表着它是一个有状态的 Widget build 方法去返回。
AppBar 是页面的导航栏
body 则是一个 Text 组件,显示了一个根据 _counter 属性可变的文本:
floatingActionButton,则是页面右下角的带“+”的悬浮按钮
使用 setState 方法去自增状态属性 _counter。setState 方法是 Flutter 以数据驱动视图更新的关键函数,它会通知 Flutter 框架:我这儿有状态发生了改变,赶紧给我刷新界面吧。而 Flutter 框架收到通知后,会执行 Widget 的 build 方法,根据新的状态重新构建界面。这里需要注意的是:状态的更改一定要配合使用 setState。通过这个方法
这里需要注意的是:状态的更改一定要配合使用 setState,
通过这个方法的调用,Flutter 会在底层标记 Widget 的状态,随后触发重建。


image.png

对于 StatefulWidget 而言,当数据改变的时候,我们需要重新创建 Widget 去更新界面,这也就意味着 Widget 的创建销毁会非常频繁。
为此,Flutter 对这个机制做了优化,其框架内部会通过一个中间层去收敛上层 UI 配置对底层真实渲染的改动,从而最大程度降低对真实渲染视图的修改,提高渲染效率,而不是上层 UI 配置变了就需要销毁整个渲染视图树重建。
有原生 Android 和 iOS 框架开发经验的同学,可能更习惯命令式的 UI 编程风格:手动创建 UI 组件,在需要更改 UI 时调用其方法修改视觉属性。而 Flutter 采用声明式 UI 设计,我们只需要描述当前的 UI 状态(即 State)即可,不同 UI 状态的视觉变更由 Flutter 在底层完成。

Dart基础,从Helloword开始

main(){
  print("Hello Word");
}

和绝大多数编译型语言一样,Dart 要求以 main 函数作为执行的入口。
在 Dart 中,我们可以用 var 或者具体的类型来声明一个变量
在默认情况下,未初始化的变量的值都是 null

main(){
  var string1 ="这是var";
  String string2 = "这是具体类型";
  int i;
  print("string1 $string1"+" == string2 $string2");
  print("i=$i");
}

输出

string1 这是var == string2 这是具体类型
i=null

对于多行字符串的构建,你可以通过三个单引号或三个双引号的方式声明,这与 Python 是一致的:

main(){
  var s3 = """This is 
  amulti-line string.""";
  print("s3=$s3");
}

输出

s3=This is 
  amulti-line string.

Dart 是类型安全的语言,并且所有类型都是对象类型,都继承自顶层类型 Object,因此一切变量的值都是类的实例(即对象),甚至数字、布尔值、函数和 null 也都是继承自 Object 的对象(和java类似)

List

  //创建list
  var testList1 = List();
  print(testList1.length); //输出 0
  //固定长度List
  var testList = List(2);
  print(testList.length); //输出 2
  //泛型固定元素
  var testList3 = List<String>();
  testList3.add("哈哈哈");
  //testList3.add(1);//报错,1不是String类型

  //和js一样
  var testList4 = [123, 321, 111];
  print(testList4.length); //输出 3

  var testList5 = [true, "嘎嘎嘎", 1];
  testList5.add(1.6);
  print(testList5); //输出 [true, 嘎嘎嘎, 1, 1.6]

  //如果固定长度
  var testList6 = List(2);
  //testList6.add(0);//运行时 会报错“Unsupported operation: Cannot add to a fixed-length list”
  testList6[0] = 1;
  testList6[1] = true;
  print(testList6);

  //将满足条件的元素保留下来,不满足条件的元素删除
  var testList7 = ["学生", "老师", "军人", "学生", "学生"];
  testList7.retainWhere((obj) => obj.toString() == "学生");
  print(testList7.length); //输出 3

  var testList8 = ["学生", "老师", "军人", "学生", "学生"];
  bool any = testList8.any((element) => (element == "学生")); //只要有一个相等  true
  bool every = testList8.every((element) => (element == "学生")); //所有相等 true
  print("result:$any"); //输出 true
  print("result:$every"); //输出 false

  //list遍历
  testList7.forEach((item) => print("forEach: $item"));
  for (var item in testList7) {
    print("for: $item");
  }
 var list = testList7.map((value){
     print("map: $value");
  });
  list.toList();


  //遍历获取index
  for (int i = 0; i < testList7.length; i++) {
    print("for: $i");
  }

  var map1 = testList7.asMap();
  map1.forEach((index, v) => print('$index: $v'));

final 和 const区别

getData() {
  return 1;
}
image.png
 final List ls = [1, 2, 3]; //子元素可变
 ls[1] = 4;
 print(ls);

 const List ls1 = [1, 2, 3];//子元素不可变
 ls1[1] = 4; //运行报错  Cannot modify an unmodifiable list
 print(ls1);
final finalList1 = [1, 2];
final finalList2 = [1, 2];
print(identical(finalList1, finalList2)); //identical用于检查两个引用是否指向同一个对象

const constList1 = [1, 2]; //内存不会重建
const constList2 = [1, 2];
print(identical(constList1, constList2)); //identical用于检查两个引用是否指向同一个对象

函数可以为变量,参数

  //函数是一段用来独立地完成某个功能的代码。在 Dart 中,所有类型都是对象类型,函数也是对象,它的类型叫作 Function。
  // 这意味着函数也可以被定义为变量,甚至可以被定义为参数传递给另一个函数。
  //  bool isZero(int number) {
  //    //判断整数是否为0
  //    return number == 0;
  //  }

  bool isZero(int number) => number == 0;

  void printInfo(int number, Function isZero) {
    //用isZero函数来判断整数是否为0
    print("$number is Zero: ${isZero(number)}");
  }

  Function f = isZero; //函数
  int x = 10;
  int y = 0;
  printInfo(x, f); // 输出 10 is Zero:false
  printInfo(y, f); // 输出 0 is Zero: true

构造函数

  //C++ 与 Java 的做法是,提供函数的重载,即提供同名但参数不同的函数。
  // 但 Dart 认为重载会导致混乱,因此从设计之初就不支持重载,而是提供了可选命名参数和可选参数
  //在 Flutter 中会大量用到可选命名参数的方式

  //要达到可选命名参数的用法,那就在定义函数的时候给参数加上 {}
  void enable1Flags({bool bold, bool hidden}) => print("$bold , $hidden");
  enable1Flags(bold: true, hidden: false); //true, false
  enable1Flags(bold: true); //true, null

  //定义可选命名参数时增加默认值
  void enable2Flags({bool bold = true, bool hidden = false}) => print("$bold ,$hidden");
  enable2Flags(bold: false); //false, false

  //可忽略的参数在函数定义时用[]符号指定
  void enable3Flags(bool bold, [bool hidden]) => print("$bold ,$hidden");
  enable3Flags(true, false); //true, false
  enable3Flags(
    true,
  ); //true, null

  //定义可忽略参数时增加默认值
  void enable4Flags(bool bold, [bool hidden = false]) => print("$bold ,$hidden");
  enable4Flags(true); //true, false
  enable4Flags(true, true); // true, true

  //Dart 中并没有 public、protected、private 这些关键字,我们只要在声明变量与方法时,
  // 在前面加上“_”即可作为 private 方法使用。如果不加“_”,则默认为 public

}

//构造函数
class Point {
  int x, y;
  String s;
  Point(this.x, this.y): s = "";//初始化变量s

//  Point(var x, var y) {
//    this.x = x;
//    this.y = y;
//  }

  Point.ff(this.s);//重命名构造
  Point.kk(num k):this(k,0);// 重定向构造函数
}

判空

main(){
  //判空
  var s;
  print(s?.getData());//输出 null
  print(s?.getData()??6);//输出 6
}

getData(){
  return 1;
}

多继承(mixin) 关键字 with

class Point {
  num x = 10, y = 0;
  void printInfo() => print('($x,$y)');
}

class Point2 {
  void printInfo() => print("这是Point2");
}

//Vector继承自Point
class Vector extends Point {
  num z = 0;
  @override
  void printInfo() => print('($x,$y,$z)'); //覆写了printInfo实现
}

//Coor是对Point的接口实现
class Coor implements Point {
  //  num x = 0, y = 0; //成员变量需要重新声明
  //  void printInfo() => print('($x,$y)'); //成员函数需要重新声明实现
  @override
  num x;

  @override
  num y;

  @override
  void printInfo() {
    // TODO: implement printInfo
  }
}

class Demo extends Point with Point2 {
  //输出 ??
}

Flutter基础

flutter核心是一切皆Widget。

image.png
abstract class RenderObjectWidget extends Widget {
  @override
  RenderObjectElement createElement();
  @protected
  RenderObject createRenderObject(BuildContext context);
  @protected
  void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }
  ...
}
abstract class RenderObjectElement extends Element {
  RenderObject _renderObject;

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

其中,Widget 是 Flutter 世界里对视图的一种结构化描述,里面存储的是有关视图渲染的配置信息;Element 则是 Widget 的一个实例化对象,将 Widget 树的变化做了抽象,能够做到只将真正需要修改的部分同步到真实的 Render Object 树中,最大程度地优化了从结构化的配置信息到完成最终渲染的过程;而 RenderObject,则负责实现视图的最终呈现,通过布局、绘制完成界面的展示。

StatefulWidget与StatelessWidget

StatefulWidget 应对有交互、需要动态变化视觉效果的场景,而 StatelessWidget 则用于处理静态的、无状态的视图展示。

简单来说就是,当你所要构建的用户界面不随任何状态信息的变化而变化时,需要选择使用 StatelessWidget,反之则选用 StatefulWidget。

命令式与声明式

// Android设置某文本控件展示文案为Hello World
TextView textView = (TextView) findViewById(R.id.txt);
textView.setText("Hello World");

// iOS设置某文本控件展示文案为Hello World
UILabel *label = (UILabel *)[self.view viewWithTag:1234];
label.text = @"Hello World";

// 原生JavaScript设置某文本控件展示文案为Hello World
document.querySelector("#demo").innerHTML = "Hello World!";

总结来说,命令式编程强调精确控制过程细节;而声明式编程强调通过意图输出结果整体。

生命周期

image.png

State 的生命周期可以分为 3 个阶段:创建(插入视图树)、更新(在视图树中存在)、销毁(从视图树中移除)
State 初始化时会依次执行 :构造方法 -> initState -> didChangeDependencies -> build,随后完成页面渲染。
构造方法是 State 生命周期的起点,Flutter 会通过调用 StatefulWidget.createState() 来创建一个 State。我们可以通过构造方法,来接收父 Widget 传递的初始化 UI 配置数据。这些配置数据,决定了 Widget 最初的呈现效果。initState,会在 State 对象被插入视图树的时候调用。这个函数在 State 的生命周期中只会被调用一次,所以我们可以在这里做一些初始化工作,比如为状态变量设定默认值。didChangeDependencies 则用来专门处理 State 对象依赖关系变化,会在 initState() 调用结束后,被 Flutter 调用。build,作用是构建视图。经过以上步骤,Framework 认为 State 已经准备好了,于是调用 build。我们需要在这个函数中,根据父 Widget 传递过来的初始化配置数据,以及 State 的当前状态,创建一个 Widget 然后返回。

当组件的可见状态发生变化时,deactivate 函数会被调用
当 State 被永久地从视图树中移除时,Flutter 会调用 dispose 函数


image.png
abstract class WidgetsBindingObserver {
  //页面pop
  Future<bool> didPopRoute() => Future<bool>.value(false);
  //页面push
  Future<bool> didPushRoute(String route) => Future<bool>.value(false);
  //系统窗口相关改变回调,如旋转
  void didChangeMetrics() { }
  //文本缩放系数变化
  void didChangeTextScaleFactor() { }
  //系统亮度变化
  void didChangePlatformBrightness() { }
  //本地化语言变化
  void didChangeLocales(List<Locale> locale) { }
  //App生命周期变化
  void didChangeAppLifecycleState(AppLifecycleState state) { }
  //内存警告回调
  void didHaveMemoryPressure() { }
  //Accessibility相关特性回调
  void didChangeAccessibilityFeatures() {}
}

-- 生命周期回调
didChangeAppLifecycleState 回调函数中,有一个参数类型为 AppLifecycleState 的枚举类,这个枚举类是 Flutter 对 App 生命周期状态的封装。它的常用状态包括 resumed、inactive、paused 这三个。resumed:可见的,并能响应用户的输入。inactive:处在不活动状态,无法处理用户响应。paused:不可见并不能响应用户的输入,但是在后台继续活动中。
dispose 时把监听器移除


image.png

-- 帧绘制回调
WidgetsBinding 提供了单次 Frame 绘制回调,以及实时 Frame 绘制回调两种机制,来分别满足不同的需求:
单次 Frame 绘制回调,通过 addPostFrameCallback 实现。它会在当前 Frame 绘制完成后进行进行回调,并且只会回调一次,如果要再次监听则需要再设置一次。

WidgetsBinding.instance.addPostFrameCallback((_){
    print("单次Frame绘制回调");//只回调一次
  });

实时 Frame 绘制回调,则通过 addPersistentFrameCallback 实现。这个函数会在每次绘制 Frame 结束后进行回调,可以用做 FPS 监测。

WidgetsBinding.instance.addPersistentFrameCallback((_){
  print("实时Frame绘制回调");//每帧都回调
});

Flutter控件从Hello Word说起

文本控件

Android 里的 TextView、iOS 中的 UILabel,小程序里的text。
Text 支持两种类型的文本展示,一个是默认的展示单一样式的文本 Text,另一个是支持多种混合样式的富文本 Text.rich,TextSpan。
如何使用单一样式的文本 Text。

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Center(child: Text("Hello Word",textAlign: TextAlign.center,),),
      ),
      body: Text(
        '文本是视图系统中的常见控件,用来显示一段特定样式的字符串,就比如Android里的TextView,或是iOS中的UILabel,或是小程序里的text', textAlign: TextAlign.center, //居中显示
        style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: Colors.red), //20号红色粗体展示
      ),
    );
  }
}
image.png

如何使用富文本Text.rich,TextSpan
混合展示样式与单一样式的关键区别在于分片,即如何把一段字符串分为几个片段来管理,给每个片段单独设置样式。

class MyHomePage extends StatelessWidget {

  @override
  Widget build(BuildContext context) {
    TextStyle blackStyle = TextStyle(fontWeight: FontWeight.normal, fontSize: 20, color: Colors.black); //黑色样式
    TextStyle redStyle = TextStyle(fontWeight: FontWeight.bold, fontSize: 20, color: Colors.red); //红色样式
    return Scaffold(
      appBar: AppBar(
        title: Center(child: Text("Hello Word",textAlign: TextAlign.center,),),
      ),
      body: Text.rich(
          TextSpan(
            children: <TextSpan>[
              TextSpan(text:'文本是视图系统中常见的控件,它用来显示一段特定样式的字符串,类似', style: redStyle), //第1个片段,红色样式
              TextSpan(text:'Android', style: blackStyle), //第1个片段,黑色样式
              TextSpan(text:'中的', style:redStyle), //第1个片段,红色样式
              TextSpan(text:'TextView', style: blackStyle) //第1个片段,黑色样式
            ]
          ),
        textAlign: TextAlign.center,
      ),
    );
  }
}
image.png

图片

https://blog.csdn.net/poorkick/article/details/80458707

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();

}

class _MyHomePageState extends State<MyHomePage>{
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Center(child: Text("Hello Word"),),),
      body: FadeInImage.assetNetwork(
        placeholder: 'images/loading.gif', //gif占位
        image: 'https://images.unsplash.com/photo-1576247628507-d93ee4557ea4?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1050&q=80',
        fit: BoxFit.cover, //图片拉伸模式
        width: 200,
        height: 200,
      ),
    );
  }
}
图片加载.gif

Image 控件需要根据图片资源异步加载的情况,决定自身的显示效果,因此是一个 StatefulWidget。

按钮

Flutter 提供了三个基本的按钮控件,即 FloatingActionButton、FlatButton 和 RaisedButton

FloatingActionButton(onPressed: () => print('FloatingActionButton pressed'),child: Text('Btn'),);
FlatButton(onPressed: () => print('FlatButton pressed'),child: Text('Btn'),);
RaisedButton(onPressed: () => print('RaisedButton pressed'),child: Text('Btn'),);
image.png

onPressed 参数用于设置点击回调,告诉 Flutter 在按钮被点击时通知我们。如果 onPressed 参数为空,则按钮会处于禁用状态,不响应用户点击。
child 参数用于设置按钮的内容,告诉 Flutter 控件应该长成什么样,也就是控制着按钮控件的基本样式。child 可以接收任意的 Widget,比如我们在上面的例子中传入的 Text,除此之外我们还可以传入 Image 等控件。

Dialog

RaisedButton(
            child: Text("SimpleDialog"),
            onPressed: () {
              showDialog(
                  context: context,
                  builder: (_) {
                    return SimpleDialog(
                      title: Text("标题SimpleDialog"),
                      children: <Widget>[
                        Container(
                          height: 100,
                          child: Text('这里是内容'),
                        ),
                        RaisedButton(
                          child: Text('取消'),
                          onPressed: () {
                            Navigator.of(context).pop();
                          },
                        ),
                        RaisedButton(
                          child: Text('确认'),
                          onPressed: () {
                            Navigator.of(context).pop();
                          },
                        ),
                      ],
                    );
                  });
            },
          ),
SimpleDialog.png
RaisedButton(
            child: Text("AlertDialog"),
            onPressed: () {
              showDialog(
                  context: context,
                  builder: (_) {
                    return AlertDialog(
                      title: Text("标题AlertDialog"),
                      content: Text("这里是内容"),
                      actions: <Widget>[
                        RaisedButton(
                          child: Text('取消'),
                          onPressed: () {
                            Navigator.of(context).pop();
                          },
                        ),
                        RaisedButton(
                          child: Text('确定'),
                          onPressed: () {
                            Navigator.of(context).pop();
                          },
                        ),
                      ],
                    );
                  });
            },
          ),
AlertDialog.png
RaisedButton(
            child: Text("GeneralDialog"),
            onPressed: () {
              showGeneralDialog(
                  context: context,
                  barrierDismissible: false,
                  barrierLabel: 'barrierLabel',
                  transitionDuration: Duration(milliseconds: 400),
                  pageBuilder: (context, animation, secondaryAnimation) {
                    return AlertDialog(
                      title: Text("标题GeneralDialog"),
                      content: Text("这里是内容"),
                      actions: <Widget>[
                        RaisedButton(
                          child: Text('取消'),
                          onPressed: () {
//                            Navigator.of(context).pop();
                            Navigator.of(context, rootNavigator: true).pop();
                          },
                        ),
                        RaisedButton(
                          child: Text('确定'),
                          onPressed: () {
                            Navigator.of(context).pop();
                          },
                        ),
                      ],
                    );
                  });
            },
          ),
GeneralDialog.png
RaisedButton(
            child: Text("CupertinoAlertDialog"),
            onPressed: (){
              showCupertinoDialog(
                  context: context,
                  builder: (context) {
                    return  CupertinoAlertDialog(
                      title:  Text("标题 CupertinoAlertDialog"),
                      content:  Text("内容内容内容内容内容内容内容内容内容内容内容"),
                      actions: <Widget>[
                        new FlatButton(
                          onPressed: () {
                            Navigator.of(context).pop("点击了确定");
                          },
                          child:  Text("确认"),
                        ),
                        new FlatButton(
                          onPressed: () {
                            Navigator.of(context).pop("点击了取消");
                          },
                          child: Text("取消"),
                        ),
                      ],
                    );
                  });
            },
          ),
CupertinoAlertDialog.png
         ///fix error overflow
          RaisedButton(
            child: Text("DialogWithColumn"),
            onPressed: (){
              showDialogWithColumn(context);
            },
          ),
void showDialogWithColumn(BuildContext context) {
    showDialog(
        context: context,
        builder: (context) {
          return new AlertDialog(
            title: new Text("title"),
            content: new SingleChildScrollView(
              child: new Column(
                children: <Widget>[
                  new SizedBox(
                    height: 100,
                    child: new Text("1"),
                  ),
                  new SizedBox(
                    height: 100,
                    child: new Text("1"),
                  ),
                  new SizedBox(
                    height: 100,
                    child: new Text("1"),
                  ),
                  new SizedBox(
                    height: 100,
                    child: new Text("1"),
                  ),
                  new SizedBox(
                    height: 100,
                    child: new Text("1"),
                  ),
                  new SizedBox(
                    height: 100,
                    child: new Text("1"),
                  ),
                  new SizedBox(
                    height: 100,
                    child: new Text("1"),
                  ),
                  new SizedBox(
                    height: 100,
                    child: new Text("1"),
                  ),
                  new SizedBox(
                    height: 100,
                    child: new Text("1"),
                  ),
                  new SizedBox(
                    height: 100,
                    child: new Text("1"),
                  ),
                  new SizedBox(
                    height: 100,
                    child: new Text("1"),
                  ),
                  new SizedBox(
                    height: 100,
                    child: new Text("1"),
                  ),
                ],
              ),
            ),
            actions: <Widget>[
              new FlatButton(
                onPressed: () {Navigator.of(context).pop();},
                child: Text("确认"),
              ),
              new FlatButton(
                onPressed: () {Navigator.of(context).pop();},
                child: Text("取消"),
              ),
            ],
          );
        });
  }
DialogWithColumn.png
 RaisedButton(
            child: Text("DialogWithListView"),
            onPressed: (){
              showDialogWithListView(context);
            },
          ),
 void showDialogWithListView(BuildContext context) {
    showDialog(
      context: context,
      builder: (BuildContext context) {
        return AlertDialog(
          title: Text("Listview"),
            content: Container(
              /*fix:
              要将ListView包装在具有特定宽度和高度的Container中
              如果Container没有定义这两个属性的话,会报错,无法显示ListView
               */
              width: MediaQuery.of(context).size.width * 0.9,
              height: MediaQuery.of(context).size.height * 0.9,
              child: ListView.builder(
                itemBuilder: (context, index) {
                  return  SizedBox(
                    height: 100,
                    child:  Text("1"),
                  );
                },
                itemCount: 10,
                shrinkWrap: true,
              ),
            ));
      },
    );
}
DialogWithListView.png
//State 刷新
RaisedButton(
            child: Text("showDialogWithStateBuilder"),
            onPressed: (){
              showDialogWithStateBuilder(context);
            },
          ),
void showDialogWithStateBuilder(BuildContext context) {
    showDialog(
        context: context,
        builder: (context) {
          bool selected = false;
          return AlertDialog(
            title: Text("StatefulBuilder"),
            content: StatefulBuilder(builder: (context, StateSetter setState) {
              return Container(
                child:  CheckboxListTile(
                    title: Text("选项"),
                    value: selected,
                    onChanged: (bool) {
                      setState(() {
                        selected = !selected;
                      });
                    }),
              );
            }),
          );
        });
  }
showDialogWithStateBuilder.png
 RaisedButton(
            child: Text("自定义Dialog"),
            onPressed: (){
              showCustomLoadingDialog(context);
            },
          ),
void showCustomLoadingDialog(BuildContext context) {
    showDialog(
        context: context,
        barrierDismissible: false,
        builder: (context) {
          return CustomLoadingDialog();
        });
  }

import 'package:flutter/material.dart';

class CustomLoadingDialog extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    Duration insetAnimationDuration = const Duration(milliseconds: 100);
    Curve insetAnimationCurve = Curves.decelerate;

    RoundedRectangleBorder _defaultDialogShape = RoundedRectangleBorder(
        borderRadius: BorderRadius.all(Radius.circular(2.0)));

    return AnimatedPadding(
      padding: MediaQuery.of(context).viewInsets +
          const EdgeInsets.symmetric(horizontal: 40.0, vertical: 24.0),
      duration: insetAnimationDuration,
      curve: insetAnimationCurve,
      child: MediaQuery.removeViewInsets(
        removeLeft: true,
        removeTop: true,
        removeRight: true,
        removeBottom: true,
        context: context,
        child: Center(
          child: SizedBox(
            width: 120,
            height: 120,
            child: Material(
              elevation: 24.0,
              color: Theme.of(context).dialogBackgroundColor,
              type: MaterialType.card,
              ///在这里修改成想要显示的widget,外部的属性跟其他Dialog保持一致
              child:  Column(
                mainAxisSize: MainAxisSize.min,
                crossAxisAlignment: CrossAxisAlignment.center,
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                   CircularProgressIndicator(),
                  Padding(
                    padding: const EdgeInsets.only(top: 20),
                    child:  Text("加载中"),
                  ),
                ],
              ),
              shape: _defaultDialogShape,
            ),
          ),
        ),
      ),
    );
  }
}
自定义Dialog.png
 ///许可证 dialog 样式固定
          RaisedButton(
            child: Text("AboutDialog"),
            onPressed: (){
              showAboutDialog(context: context,children: [
                Padding(padding: EdgeInsets.only(top: 10.0), child: Text('1. AboutDialog!')),
                Padding(padding: EdgeInsets.only(top: 10.0), child: Text('2. SimpleDialog!')),
                Padding(padding: EdgeInsets.only(top: 10.0), child: Text('3. AlertDialog!'))
              ]);
//              showDialog(context: context,
//                  barrierDismissible: false,
//                  builder: (context) {
//                    return AboutDialog(
//                        applicationIcon: Container(child: FlutterLogo()),
//                        applicationName: 'Flutter Dialog',
//                        applicationLegalese: '所有解释权归本人所有!',
//                        applicationVersion: 'V1.5.2',
//                        children: <Widget>[
//                          Padding(padding: EdgeInsets.only(top: 10.0), child: Text('1. AboutDialog!')),
//                          Padding(padding: EdgeInsets.only(top: 10.0), child: Text('2. SimpleDialog!')),
//                          Padding(padding: EdgeInsets.only(top: 10.0), child: Text('3. AlertDialog!'))
//                        ]);
//                  });
            },
          ),
AboutDialog.png
//fix:Scaffold.of() called with a context that does not contain a Scaffold.
floatingActionButton: Builder(builder: (BuildContext context) {
        return FloatingActionButton(onPressed: () {
          showBottomSheet(context: context, builder: (_){
            return Container(
              height: 200,
              width: 200,
              color: Colors.red,
              child: Column(
                children: <Widget>[
                  Text("相册1"),
                  Text("拍照1"),
                  Text("取消1"),
                ],
              ),
            );
          });

//          Scaffold.of(context).showBottomSheet((_){
//            return Container(
//              height: 200,
//              width: 200,
//              child: Column(
//                children: <Widget>[
//                  Text("相册"),
//                  Text("拍照"),
//                  Text("取消"),
//                ],
//              ),
//            );
//          });
        },);
      },)
sheetDialog.png

ListView

Android 中是由ListView 或 RecyclerView ,iOS 中是UITableView 。
Flutter中的ListView有2中创建方式

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage>{
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Center(child: Text("Hello Word"),),),
      body: ListView(
          children: <Widget>[
            //设置ListTile组件的标题与图标
            ListTile(leading: Icon(Icons.map),  title: Text('Map')),
            ListTile(leading: Icon(Icons.mail), title: Text('Mail')),
            ListTile(leading: Icon(Icons.message), title: Text('Message')),
          ])
    );
  }
}
image.png

这是水平的

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage>{
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Center(child: Text("Hello Word"),),),
      body: ListView(
          scrollDirection: Axis.horizontal,
          itemExtent: 100, //列表项高度,一般固定高度,不固定会动态计算,消耗性能
          children: <Widget>[
            //设置ListTile组件的标题与图标
            ListTile(leading: Icon(Icons.map),  title: Text('Map')),
            ListTile(leading: Icon(Icons.mail), title: Text('Mail')),
            ListTile(leading: Icon(Icons.message), title: Text('Message')),
          ])
    );
  }
}
class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage>{
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Center(child: Text("Hello Word"),),),
      body: ListView.builder(
          itemCount: 100, //元素个数,为空则表示 ListView 为无限列表
          itemExtent: 50.0, //列表项高度,一般固定高度,不固定会动态计算,消耗性能
          itemBuilder: (BuildContext context, int index) => ListTile(title: Text("title $index"), subtitle: Text("body $index"))
      )
    );
  }
}
ListView.builder(
            itemCount: 100, //元素个数,为空则表示 ListView 为无限列表
            itemExtent: 50.0*2, //列表项高度,一般固定高度,不固定会动态计算,消耗性能
            itemBuilder: (BuildContext context, int index) => Column(
                  children: <Widget>[
                    ListTile(title: Text("title $index"), subtitle: Text("body $index")),
                    index.isOdd?Divider(height: 1,color: Colors.red,):Divider(height: 1,color: Colors.lightBlue,)//是奇数
                  ],
                )));
 ListView.separated(
            itemCount: 100,
            separatorBuilder: (BuildContext context, int index) => index %2 ==0? Divider(color: Colors.green) : Divider(color: Colors.red),//index为偶数,创建绿色分割线;index为奇数,则创建红色分割线
            itemBuilder: (BuildContext context, int index) => ListTile(title: Text("title $index"), subtitle: Text("body $index"))//创建子Widget
        )
image.png

Listview 下拉加载 上拉更多

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  ScrollController _scrollController = ScrollController(); //滚动监听

  @override
  void initState() {
    super.initState();
    _scrollController.addListener((){
      if (_scrollController.position.pixels == _scrollController.position.maxScrollExtent){
        print('滑动到了最底部,加载更多');
//        _getMore();
      }
    });
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: Center(
            child: Text("Hello Word"),
          ),
        ),
        body: RefreshIndicator(
          onRefresh: _onRefresh,
          child: ListView.separated(
              itemCount: 20,
              controller:_scrollController,
              separatorBuilder: (BuildContext context, int index) => index %2 ==0? Divider(color: Colors.green) : Divider(color: Colors.red),//index为偶数,创建绿色分割线;index为奇数,则创建红色分割线
              itemBuilder: (BuildContext context, int index) => ListTile(title: Text("title $index"), subtitle: Text("body $index"))//创建子Widget
          ),
        )
    );
  }

  //下拉刷新
  Future<void> _onRefresh() async{
    await Future.delayed(Duration(seconds: 3), () {
      print('refresh');
      setState(() {
      });
    });
  }

  @override
  void dispose() {
    _scrollController.dispose();
    super.dispose();
  }
}
下拉刷新上拉加载.gif

CustomScrollView

用来处理多个需要自定义滚动效果的 Widget

CustomScrollView(
            slivers: <Widget>[
              SliverAppBar(//SliverAppBar作为头图控件
                title: Text('CustomScrollView Demo',style: TextStyle(color: Colors.red),),//标题
                floating: true,//设置悬浮样式
                flexibleSpace: Image.network("https://images.unsplash.com/photo-1576247628507-d93ee4557ea4?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=crop&w=1050&q=80",fit:BoxFit.cover),//设置悬浮头图背景
                expandedHeight: 300,//头图控件高度
              ),
              SliverList(//SliverList作为列表控件
                delegate: SliverChildBuilderDelegate(
                      (context, index) => ListTile(title: Text('Item #$index')),//列表项创建方法
                  childCount: 100,//列表元素个数
                ),
              ),
            ])
CustomScrollView demo.gif

布局

一共31种布局

单子 Widget 布局:Container、Padding 与 Center

Center(
          child: Padding(
            padding: const EdgeInsets.all(28.0),
            child: Container(
              child: Text('Container(容器)在UI框架中是一个很常见的概念。'),
              padding: EdgeInsets.all(18.0),// 内边距
              width: double.infinity,
              height: 240,
              alignment: Alignment.center, // 子Widget居中对齐
              decoration: BoxDecoration(
                //Container样式
                color: Colors.red, // 背景色
                borderRadius: BorderRadius.circular(10.0), // 圆角边框
              ),
            ),
          ),
        ));
image.png

多子 Widget 布局:Row、Column 与 Expanded

Row水平排列
Column 垂直排列
Expanded 负责分配这些子 Widget 在布局方向(行 / 列)中剩余空间的

//Row的用法示范
Row(
  children: <Widget>[
    Container(color: Colors.yellow, width: 60, height: 80,),
    Container(color: Colors.red, width: 100, height: 180,),
    Container(color: Colors.black, width: 60, height: 80,),
    Container(color: Colors.green, width: 60, height: 80,),
  ],
);
image.png
//Column的用法示范
Column(
  children: <Widget>[
    Container(color: Colors.yellow, width: 60, height: 80,),
    Container(color: Colors.red, width: 100, height: 180,),
    Container(color: Colors.black, width: 60, height: 80,),
    Container(color: Colors.green, width: 60, height: 80,),
  ],
);
image.png
Row(
  children: <Widget>[
    Expanded(flex: 1, child: Container(color: Colors.yellow, height: 60)), //设置了flex=1,因此宽度由Expanded来分配
    Container(color: Colors.red, width: 100, height: 180,),
    Container(color: Colors.black, width: 60, height: 80,),
    Expanded(flex: 1, child: Container(color: Colors.green,height: 60),)/设置了flex=1,因此宽度由Expanded来分配
  ],
);
image.png image.png

我们可以根据主轴与纵轴,设置子 Widget 在这两个方向上的对齐规则 mainAxisAlignment 与 crossAxisAlignment。比如,主轴方向 start 表示靠左对齐、center 表示横向居中对齐、end 表示靠右对齐、spaceEvenly 表示按固定间距对齐;而纵轴方向 start 则表示靠上对齐、center 表示纵向居中对齐、end 表示靠下对齐。


Row主轴.png
Row纵轴.png

层叠 Widget 布局:Stack 与 Positioned

Stack 让一个控件叠加在另一个控件的上面,比如在一张图片上放置一段文字,又或者是在图片的某个区域放置一个按钮。
Stack 提供了层叠布局的容器,而 Positioned 则提供了设置子 Widget 位置的能力。

Stack(
  children: <Widget>[
    Container(color: Colors.yellow, width: 300, height: 300),//黄色容器
    Positioned(
      left: 18.0,
      top: 18.0,
      child: Container(color: Colors.green, width: 50, height: 50),//叠加在黄色容器之上的绿色控件
    ),
    Positioned(
      left: 18.0,
      top:70.0,
      child: Text("Stack提供了层叠布局的容器"),//叠加在黄色容器之上的文本
    )
  ],
)
image.png

Stack 控件允许其子 Widget 按照创建的先后顺序进行层叠摆放,而 Positioned 控件则用来控制这些子 Widget 的摆放位置。需要注意的是,Positioned 控件只能在 Stack 中使用,在其他容器中使用会报错。

assets项目资源

image.png
rootBundle.loadString('assets/result.json').then((msg)=>print(msg));//加载json

更换启动图标得去android和ios原生系统去更换。

依赖第三方库

Flutter插件

image.png

路由

页面之间的跳转是通过 Route 和 Navigator 来管理的
Route 是页面的抽象,主要负责创建对应的界面,接收参数,响应 Navigator 打开和关闭;
Navigator 则会维护一个路由栈管理 Route,Route 打开即入栈,Route 关闭即出栈,还可以直接替换栈内的某一个 Route
基本路由。无需提前注册,在页面切换时需要自己构造页面实例。
命名路由。需要提前注册页面标识符,在页面切换时通过标识符直接打开新的路由。

基本路由

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("First Screen"),
      ),
      body: RaisedButton(
        onPressed: () {
          Navigator.push(context, MaterialPageRoute(builder: (context) => SecondPage()));
        },
        child: Text("Push Second Page"),
      ),
    );
  }
}
class SecondPage extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("Second Screen"),
      ),
      body: RaisedButton(
        onPressed: () {
          Navigator.pop(context);
        },
        child: Text("goBack First Page"),
      ),
    );
  }
}
基本路由.gif

命名路由

class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      routes: {
        "second_page": (context) => SecondPage(),
      },
      home: MyHomePage(),
    );
  }
}

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("First Screen"),
      ),
      body: RaisedButton(
        onPressed: () {
          //          Navigator.push(context, MaterialPageRoute(builder: (context) => SecondPage()));
          //使用名字打开页面
          Navigator.pushNamed(context, "second_page");
        },
        child: Text("Push Second Page"),
      ),
    );
  }
}

命名路由只能通过字符串名字来初始化固定目标页面,这个字符串写错了怎么办?

 //错误路由处理,统一返回UnknownPage
 onUnknownRoute: (RouteSettings setting) => MaterialPageRoute(builder: (context) => UnknownPage()),

传参

Navigator.push(context, MaterialPageRoute(builder: (context) => SecondPage(firstTitle: "this is first page",firstList: list,)));
  final String firstTitle;
  final List firstList;
  const SecondPage({Key key, this.firstTitle,this.firstList}) : super(key: key);

理解构造函数中的key

Navigator.pushNamed(context, "second_page",arguments:"这是FirstPage传递的参数");
//取出路由参数
String msg = ModalRoute.of(context).settings.arguments as String;

Flutter 也提供了返回参数的机制,即A->B,B关闭需要传递参数告知A页面处理结果。

Navigator.pushNamed(context, "second_page",arguments:"这是FirstPage传递的参数").then((msg)=>print("msg:$msg"));
Navigator.pop(context,"第second_page关闭返回的");

路由具体代码 https://github.com/TWBfly/flutter_demo/tree/master/%E8%B7%AF%E7%94%B1

MaterialApp的title是个啥?

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {

    return MaterialApp(
        //????
        title: 'Flutter Demo',
        debugShowCheckedModeBanner: false,
        //主题颜色
        theme: new ThemeData(
          primarySwatch: Colors.purple,
        ),
        home: MyHomePage(),
    );
  }
}
image.png
上一篇下一篇

猜你喜欢

热点阅读