flutter专栏--深入剖析你的第一个flutter应用

使用fvm管理flutter版本

如果你有使用多版本flutter的需求,那么fvm将会给你提供较大的帮助。下面我列举一下mac + flutter3.35.2的版本的操作命令,完成之后,你将可以随意切换flutter版本

复制代码
# 下载fvm相关的依赖
brew tap leoafarias/fvm
brew install fvm

# 检查版本号
fvm --version
# 3.2.1

# 下载flutter, 并使用
fvm install 3.35.2 #或者直接下载具体版本号也行
fvm use 3.35.2

# 检查flutter版本号
fvm flutter --version
# 3.35.2

这里由于笔者已经全局安装一个公司私域的flutter, 并且配置了全局的path, fvm目前识别不到已经安装的flutter,由于不想影响已经安装的全局私域flutter,这里可以在flutter命令前直接加fvm,即可使用到fvm安装的官方flutter,如果不是我这种特殊情况的话,直接使用flutter命令理论是可以切到fvm设置的global源的。

创建你的第一flutter应用

前置步骤

创建flutter之前,你需要配置你的开发环境。具体的步骤官网做了较为详细的概括,这里将不再赘述,主要分为几个步骤。

  1. 下载flutter sdk, vscode配置flutter相关插件
  2. 下载配置android studio,下载jdk
  3. 下载配置xcode
  4. 测试手机开启授权

创建flutter应用

复制代码
fvm flutter create test_app
cd test_app
fvm flutter run

选择浏览器打开,第一个flutter demo就创建完成了

源代码剖析

结构列表总结

在test_app这个根目录下,大约有十几个子目录,如下。

名称 类型 主要作用
test_app 目录 项目根目录
.dart_tool 目录 Dart 工具链配置与缓存
.idea 目录 IDE 配置
android 目录 Android 平台原生代码
build 目录 构建产物(可删除)
ios 目录 iOS 平台原生代码
lib 目录 核心 Dart 应用代码
linux 目录 Linux 桌面端原生代码
macos 目录 macOS 桌面端原生代码
test 目录 测试代码
web 目录 Web 平台代码
windows 目录 Windows 桌面端原生代码
.gitignore 文件 Git 忽略规则
.metadata 文件 Flutter 工具元数据
analysis_options.yaml 文件 静态代码分析规则配置
pubspec.lock 文件 依赖包精确版本锁定
pubspec.yaml 文件 项目依赖与元数据配置(非常重要)
README.md 文件 项目说明文档
test_app.iml 文件 IntelliJ 模块配置

作为入门,我们可以重点关注pubspec.yaml、web、和lib

pubspec.yaml目录

去除了多余的代码之后,整体的结果如下

复制代码
name: test_app
description: "A new Flutter project."
publish_to: 'none'
version: 1.0.0+1

environment:
  sdk: ^3.9.0

dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.8

dev_dependencies:
  flutter_test:
    sdk: flutter

  flutter_lints: ^5.0.0

flutter:

  uses-material-design: true
name字段

表示当前flutter应用的包名,是一个最基本的字段

我们在导入其他文件时,就需要使用如下方式,如果包名发生变化的话,相应的路径也需要发生变化

复制代码
import 'package:flutter_demo/listview_demo/listview_demo.dart';
publish_to

此属性意为包发布到哪里去

  • none:表示此包不发布;
  • 也可以指定发布的服务器,如果删除此项配置,那么默认发布到pub.dev
version

此属性表示当前工程的版本,分为应用程序的版本内部版本号,格式为x.x.x+x,比如1.0.0+1,称为语义版本号

  • +号前面的叫做version number
  • +号后面的叫做build number

在test_app/android/app/build.gradle.kts这个文件里可以看到安卓打包定版本的具体逻辑

environment

可以配置FlutterDart版本

dependencies
复制代码
dependencies:
  flutter:
    sdk: flutter

  cupertino_icons: ^1.0.8

添加我们用到的第三方的sdk

  • sdk: flutter意为默认获取flutter的最新版本,也就是我们机器上的flutter版本,我们也可以在此处添加version来指定flutter的版本;
  • cupertino_icons:给应用程序添加Cupertino图标的,一般用于iOS;

其实这个本质上跟js项目的生产依赖是一样的

dev_dependencies

开发依赖,只有运行时才会用到

flutter

Flutter相关的配置

复制代码
# 确保我们的应用程序中包含Material Icons字体,以使我们能够使用material Icons类中的图标;
uses-material-design: true

我们当资源的配置也是在这个配置下进行设置:

  • assets:配置图片;
  • fonts:配置字体;
  • plugin:该配置只存在于插件项目中,用来配置适配的平台,一般不要修改;如需添加新平台,直接添加即可;
web目录

主要包含了一些生成web页面的静态资源和模版信息

lib目录

flutter项目的源代码

main.dart整个应用的入口文件

完整的代码信息如下

复制代码
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

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

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }
}
导入依赖
复制代码
import 'package:flutter/material.dart';

导入了 Material UI 组件库。Material(opens new window)是一种标准的移动端和web端的视觉设计语言, Flutter 默认提供了一套丰富的 Material 风格的UI组件。

应用入口
复制代码
void main() {
  runApp(const MyApp());
}

Flutter 应用中 main 函数为应用程序的入口。main 函数中调用了runApp 方法,它的功能是启动Flutter应用。runApp它接受一个 Widget参数,在本示例中它是一个MyApp对象,MyApp()是 Flutter 应用的根组件。

也可以简写成单行函数

复制代码
void main() => runApp(MyApp());
应用代码结构
复制代码
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
      ),
      home: const MyHomePage(title: 'Flutter Demo Home Page'),
    );
  }
}
  • MyApp类代表 Flutter 应用,它继承了 StatelessWidget类,这也就意味着应用本身也是一个widget。
  • 在 Flutter 中,大多数东西都是 widget,包括对齐(Align)、填充(Padding)、手势处理(GestureDetector)等,它们都是以 widget 的形式提供。
  • Flutter 在构建页面时,会调用组件的build方法,widget 的主要工作是提供一个 build() 方法来描述如何构建 UI 界面(通常是通过组合、拼装其他基础 widget )。
  • MaterialApp 是Material 库中提供的 Flutter APP 框架,通过它可以设置应用的名称、主题、语言、首页及路由列表等。MaterialApp也是一个 widget。
  • home 为 Flutter 应用的首页,它也是一个 widget。
具体代码解析
MyHomePage
复制代码
class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  // 重写createState方法,创建与这个Widget关联的状态类
  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  // ...
}

MyHomePage 是应用的首页,它继承自StatefulWidget类,表示它是一个有状态的组件(Stateful widget)。

  1. Stateful widget 可以拥有状态,这些状态在 widget 生命周期中是可以变的,而 Stateless widget 是不可变的。
  2. Stateful widget 至少由两个类组成:
    • 一个StatefulWidget类。

    • 一个 State类; StatefulWidget类本身是不变的,但是State类中持有的状态在 widget 生命周期中可能会发生变化。

    • _MyHomePageState类是MyHomePage类对应的状态类。这里可以看到,和MyApp 类不同, MyHomePage类中并没有build方法,取而代之的是,build方法被挪到了_MyHomePageState方法中,至于为什么,后面会进行解读

      const MyHomePage({super.key, required this.title});

构造函数,用于初始化一个名为 MyHomePage 的页面(Widget)。

State类

_MyHomePageState在就是一个state类,在MyHomePage里面被createState出来,里面主要有两部分内容, 一是定义了一个计数器状态,再定义一个状态自增函数,当按钮点击时,会调用此函数,该函数的作用是先自增_counter,然后调用setState 方法。setState方法的作用是通知 Flutter 框架,有状态发生了改变,Flutter 框架收到通知后,会执行 build 方法来根据新的状态重新构建界面, Flutter 对此方法做了优化,使重新执行变的很快,所以你可以重新构建任何需要更新的东西,而无需分别去修改各个 widget。

复制代码
  int _counter = 0;

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

二是定一个build函数,用于构建UI页面

复制代码
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            const Text('You have pushed the button this many times:'),
            Text(
              '$_counter',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: const Icon(Icons.add),
      ),
    );
  }

MyHomePage第一次创建时,_MyHomePageState类会被创建,当初始化完成后,Flutter框架会调用 widget 的build方法来构建 widget 树,最终将 widget 树渲染到设备屏幕上。具体解读如下:

  • Scaffold 是 Material 库中提供的页面脚手架,它提供了默认的导航栏、标题和包含主屏幕 widget 树(后同"组件树"或"部件树")的body属性,组件树可以很复杂。本书后面示例中,路由默认都是通过Scaffold创建。
  • body的组件树中包含了一个Center 组件,Center 可以将其子组件树对齐到屏幕中心。此例中, Center 子组件是一个Column 组件,Column的作用是将其所有子组件沿屏幕垂直方向依次排列; 此例中Column子组件是两个 Text,第一个Text 显示固定文本 "You have pushed the button this many times:",第二个Text 显示_counter状态的数值。
  • floatingActionButton是页面右下角的带"+"的悬浮按钮,它的onPressed属性接受一个回调函数,代表它被点击后的处理器,本例中直接将_incrementCounter方法作为其处理函数。

完整流程如下:

当右下角的floatingActionButton按钮被点击之后,会调用_incrementCounter方法。在_incrementCounter方法中,首先会自增_counter计数器(状态),然后setState会通知 Flutter 框架状态发生变化,接着,Flutter 框架会调用build方法以新的状态重新构建UI,最终显示在设备屏幕上。

为什么把build放在State类中
1.方便状态访问

如果我们的StatefulWidget有很多状态,而每次状态改变都要调用build方法,由于状态是保存在 State 中的,如果build方法在StatefulWidget中,那么build方法和状态分别在两个类中,那么构建时读取状态将会很不方便。如果真的将build方法放在 StatefulWidget 中的话,由于构建用户界面过程需要依赖 State,所以build方法将必须加一个State参数,大概是下面这样:

复制代码
Widget build(BuildContext context, State state){
      //state.counter
      ...
  }

这样的话就只能将State的所有状态声明为公开的状态,这样才能在State类外部访问状态!但是,将状态设置为公开后,状态将不再具有私密性,这就会导致对状态的修改将会变的不可控。但如果将build()方法放在State中的话,构建过程不仅可以直接访问状态,而且也无需公开私有状态,这会非常方便。

2.方便继承StatefulWidget

例如,Flutter 中有一个动画 widget 的基类AnimatedWidget,它继承自StatefulWidget类。AnimatedWidget中引入了一个抽象方法build(BuildContext context),继承自AnimatedWidget的动画 widget 都要实现这个build方法。现在设想一下,如果StatefulWidget 类中已经有了一个build方法,正如上面所述,此时build方法需要接收一个 State 对象,这就意味着AnimatedWidget必须将自己的 State 对象(记为_animatedWidgetState)提供给其子类,因为子类需要在其build方法中调用父类的build方法

复制代码
class MyAnimationWidget extends AnimatedWidget{
    @override
    Widget build(BuildContext context, State state){
      //由于子类要用到AnimatedWidget的状态对象_animatedWidgetState,
      //所以AnimatedWidget必须通过某种方式将其状态对象_animatedWidgetState
      //暴露给其子类   
      super.build(context, _animatedWidgetState)
    }
}

这样很显然是不合理的,因为

  • AnimatedWidget的状态对象是AnimatedWidget内部实现细节,不应该暴露给外部。
  • 如果要将父类状态暴露给子类,那么必须得有一种传递机制,而做这一套传递机制是无意义的,因为父子类之间状态的传递和子类本身逻辑是无关的。
总结来说
  • build 方法需要根据可变状态(如 _counter 来构建用户界面。这些可变状态保存在 State 子类(如 _MyHomePageState)中。将 build 方法放在 State 子类里,可以直接访问(通过context)这些状态(例如 _counter),无需通过复杂的传递机制或将状态设置为公开,从而破坏了状态的封装性。
  • StatefulWidget对外 的,负责接收不可变的配置数据。
  • State对内 的,负责管理可变的状态。

参考

《Flutter实战·第二版》

https://juejin.cn/post/7033933629403168799

4.拓展阅读

flutter专栏--移动开发技术的发展与flutter的概要

flutter专栏--dart基础知识

关注我,有空一起闲聊

相关推荐
再学一点就睡3 小时前
多端单点登录(SSO)实战:从架构设计到代码实现
前端·架构
繁依Fanyi3 小时前
思维脑图转时间轴线
前端
发愤图强的羔羊3 小时前
Chartdb 解析数据库 DDL:从 SQL 脚本到可视化数据模型的实现之道
前端
摸着石头过河的石头3 小时前
控制反转 (IoC) 是什么?用代码例子轻松理解
前端·javascript·设计模式
携欢4 小时前
PortSwigger靶场之Stored XSS into HTML context with nothing encoded通关秘籍
前端·xss
小桥风满袖4 小时前
极简三分钟ES6 - const声明
前端·javascript
小小前端记录日常4 小时前
vue3 excelExport 导出封装
前端
南北是北北4 小时前
Flow 的 emit 与 tryEmit :它们出现在哪些类型、背压/缓存语义、何时用谁、常见坑
前端·面试
flyliu4 小时前
继承,继承,继承,哪里有家产可以继承
前端·javascript