学习 Flutter(二)

本文章学习内容来源于官方文档 https://docs.flutter.cn/ , 本文章只是用于记录学习。

构建 Flutter 布局

学习内容

  • 如何使 Widget 彼此相邻布局

  • 如何在小组件之间添加空间

  • 添加和嵌套 Widget 如何导致 Flutter 布局

根据文档提供的代码,我们可以构建以下应用程序

在这里插入图片描述

绘制布局图

在本节中,请考虑您想要的用户体验类型 您的应用用户。

考虑如何定位用户界面的组件。 布局由这些定位的总最终结果组成。 考虑规划布局以加快编码速度。 使用视觉提示来了解屏幕上的内容可能会有很大帮助。

使用您喜欢的任何方法,例如界面设计工具或铅笔 和一张纸。确定要将元素放置在 screen 之前。这是这句格言的编程版本: “测量两次,切割一次。”

  1. 提出这些问题以将布局分解为其基本元素。

    • 您能识别行和列吗?
    • 布局是否包含网格?
    • 是否有重叠的元素?
    • UI 需要选项卡吗?
    • 您需要对齐、填充或边框的内容是什么?
  2. 确定较大的元素。在此示例中,您将 image、title 和 按钮和 description 合并到一个列中。

在这里插入图片描述

布局中的主要元素: image、row、row 和 text block

  1. 绘制每行的图表

    a. 第 1 行(标题部分)有三个子项: 一列文本、一个星形图标和一个数字。 它的第一个子项 (列) 包含两行文本。 第一列可能需要更多空间。

    在这里插入图片描述

    b. 第 2 行 Button 部分有三个子项:每个子项包含 一个列,然后包含一个图标和文本。

在这里插入图片描述

绘制布局图后,请考虑如何对其进行编码。

你会在一个类中编写所有代码吗? 或者,您是否为布局的每个部分创建一个类?

要遵循 Flutter 最佳实践,请创建一个 class 或 Widget, 以包含布局的每个部分。 当 Flutter 需要重新渲染 UI 的一部分时, 它会更新更改的最小部分。 这就是为什么 Flutter 将 “一切都是 widget” 的原因。 如果 widget 中只有文本发生变化,Flutter 只会重绘该文本。 Flutter 会尽可能少地更改 UI 以响应用户输入。Text

在本教程中,将编写您标识为自身小组件的每个元素。

创建应用程序的基本代码

import 'package:flutter/material.dart';

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

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

  
  Widget build(BuildContext context) {
    const String appTitle = 'Flutter layout demo';
    return MaterialApp(
      title: appTitle,
      home: Scaffold(
        appBar: AppBar(title: const Text(appTitle)),
        body: const Center(
          child: Text('Hello World'),
        ),
      ),
    );
  }
}

添加 Title 部分

在这里插入图片描述

标题部分作为草图和原型 UI

创建一个新 Widget TitleSection

import 'package:flutter/material.dart';

class TitleSection extends StatelessWidget {
  const TitleSection({super.key, required this.name, required this.location});

  final String name;
  final String location;

  
  Widget build(BuildContext context) {
    return Padding(
        padding: const EdgeInsets.all(32),
        child: Row(
          children: [
            Expanded(
                child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Padding(
                    padding: const EdgeInsets.only(bottom: 8),
                    child: Text(name,
                        style: const TextStyle(fontWeight: FontWeight.bold))),
                Text(location, style: TextStyle(color: Colors.grey[500]))
              ],
            )),
            Icon(Icons.star, color: Colors.red[500]),
            const Text('41')
          ],
        ));
  }
}
  1. 要使用行中所有剩余的空闲空间,请使用 Widget 执行以下作 拉伸 Widget。 要将列放在行的开头, 将属性设置为 。Expanded``Column``crossAxisAlignment``CrossAxisAlignment.start
  2. 要在文本行之间添加间距,请将这些行放在 Widget 中。Padding
  3. 标题行以红色星形图标和文本 . 整行落在 widget 内并填充每条边缘 32 像素。41``Padding

将应用程序正文更改为滚动视图

在该属性中,将 Widget 替换为 Widget。 在 SingleChildScrollView 小组件中,将小组件替换为小组件。body``Center``SingleChildScrollView``Text``Column

-body: const Center(
-  child: Text('Hello World'),
+body: const SingleChildScrollView(
+ child: Column(
+   children: [

这些代码更新会以以下方式更改应用程序。

  • 小组件可以滚动。 这允许显示当前屏幕不适合的元素。SingleChildScrollView
  • 小组件显示其属性中的任何元素 按列出的顺序。 列表中列出的第一个元素显示在 列表的顶部。列表中显示的元素 在屏幕上从上到下按数组顺序排列。Column``children``children``children

更新应用程序以显示标题部分

将 Widget 添加为列表中的第一个元素。 这会将其置于屏幕顶部。 将提供的 name 和 location 传递给构造函数。TitleSection``children``TitleSection

class BuildLayoutExample extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("布局构建示例")),
      body: const SingleChildScrollView(
        child: Column(
          children: [
            TitleSection(
              name: "Oeschinen Lake Campground",
              location: "Kandersteg, Switzerland",
            ),
          ],
        ),
      ),
    );
  }
}

添加 Button 部分

在本节中,添加将为您的应用程序添加功能的按钮。

Button 部分包含使用相同布局的三列: 一行文本上的图标。

在这里插入图片描述

计划将这些列分布在一行中,以便每列采用相同的 空间量。使用主要颜色绘制所有文本和图标。

添加小组件ButtonSection

在 widget 后面添加以下代码以包含代码 构建按钮行。

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

  
  Widget build(BuildContext context) {
    final Color color = Theme.of(context).primaryColor;
    return SizedBox(
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          ButtonWithText(color: color, icon: Icons.call, label: "CALL"),
          ButtonWithText(color: color, icon: Icons.near_me, label: "ROUTE"),
          ButtonWithText(color: color, icon: Icons.share, label: "SHARE"),
        ],
      ),
    );
  }
}

class ButtonWithText extends StatelessWidget {
  const ButtonWithText({
    super.key,
    required this.color,
    required this.icon,
    required this.label,
  });

  final Color color;
  final IconData icon;
  final String label;

  
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Icon(icon, color: color),
        Padding(
          padding: const EdgeInsets.only(top: 8),
          child: Text(
            label,
            style: TextStyle(
                fontSize: 12, fontWeight: FontWeight.w400, color: color),
          ),
        ),
      ],
    );
  }
}

更新应用程序以显示按钮部分

将按钮部分添加到列表中。

class BuildLayoutExample extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("布局构建示例")),
      body: const SingleChildScrollView(
        child: Column(
          children: [
            TitleSection(
              name: "Oeschinen Lake Campground",
              location: "Kandersteg, Switzerland",
            ),
            ButtonSection(),
          ],
        ),
      ),
    );
  }
}

添加 Text 部分

在此部分中,将文本描述添加到此应用程序。

在这里插入图片描述

添加小组件TextSection

将以下代码作为单独的 Widget 添加到 Widget 之后。

class TextSection extends StatelessWidget {
  const TextSection({super.key, required this.description});

  final String description;

  
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(32),
      child: Text(
        description,
        softWrap: true,
      ),
    );
  }
}

通过将 softWrap 设置为 ,文本行将填充之前的列宽 在 word 边界处换行。true

更新应用程序以显示文本部分

在 . 添加 widget 时,将其属性设置为 位置描述的文本

class BuildLayoutExample extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("布局构建示例")),
      body: const SingleChildScrollView(
        child: Column(
          children: [
            TitleSection(
              name: "Oeschinen Lake Campground",
              location: "Kandersteg, Switzerland",
            ),
            ButtonSection(),
            TextSection(
              description:
                  'Lake Oeschinen lies at the foot of the Blüemlisalp in the '
                  'Bernese Alps. Situated 1,578 meters above sea level, it '
                  'is one of the larger Alpine Lakes. A gondola ride from '
                  'Kandersteg, followed by a half-hour walk through pastures '
                  'and pine forest, leads you to the lake, which warms to 20 '
                  'degrees Celsius in the summer. Activities enjoyed here '
                  'include rowing, and riding the summer toboggan run.',
            ),
          ],
        ),
      ),
    );
  }
}

添加 Image 部分

在此部分中,添加图像文件以完成布局。

将应用配置为使用提供的图像

要将应用程序配置为引用图像,请修改其文件。pubspec.yaml

  1. 在项目顶部创建一个目录。images

  2. 下载 lake.jpg 映像并将其添加到新目录。images

  3. 要包含图像,请向文件添加标签 在应用的根目录中。 添加 时,它用作指向图像的指针集 available 提供给您的代码。assets``pubspec.yaml``assets

    flutter:
    
      uses-material-design: true
    
      assets:
        - assets/images/
    

创建 WidgetImageSection

在其他声明之后定义以下小部件。

class ImageSection extends StatelessWidget {
  const ImageSection({super.key, required this.image});

  final String image;

  
  Widget build(BuildContext context) {
    return Image.asset(image, width: 600, height: 240, fit: BoxFit.cover);
  }
}

该值告诉 Flutter 使用 两个约束。首先,将图像显示得尽可能小。 其次,覆盖布局分配的所有空间,称为 render box。BoxFit.cover

更新应用程序以显示图像部分

将 Widget 添加为列表中的第一个子项。 将属性设置为您在 配置应用程序以使用提供的图像 中添加的图像的路径。ImageSection``children``image

class BuildLayoutExample extends StatelessWidget {
  
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text("布局构建示例")),
      body: const SingleChildScrollView(
        child: Column(
          children: [
            ImageSection(image: 'assets/images/lake.jpg'),
            TitleSection(
              name: "Oeschinen Lake Campground",
              location: "Kandersteg, Switzerland",
            ),
            ButtonSection(),
            TextSection(
              description:
                  'Lake Oeschinen lies at the foot of the Blüemlisalp in the '
                  'Bernese Alps. Situated 1,578 meters above sea level, it '
                  'is one of the larger Alpine Lakes. A gondola ride from '
                  'Kandersteg, followed by a half-hour walk through pastures '
                  'and pine forest, leads you to the lake, which warms to 20 '
                  'degrees Celsius in the summer. Activities enjoyed here '
                  'include rowing, and riding the summer toboggan run.',
            ),
          ],
        ),
      ),
    );
  }
}

最终我们就实现了如下所示的应用程序

在这里插入图片描述

如何修改你的应用程序以使其对用户输入做出反应?接下来,你将为仅包含非交互式 widget 的应用程序添加交互性。具体来说,你将通过创建一个管理两个无状态 widget 的自定义有状态 widget,修改一个图标实现使其可点击。

当应用第一次启动时,这个星形图标是实心红色,表明这个湖以前已经被收藏过了。星号旁边的数字表示 41 个人已经收藏了此湖。完成本教程后,点击星形图标将取消收藏状态,然后用轮廓线的星形图标代替实心的,并减少计数。再次点击会重新收藏,并增加计数。

在这里插入图片描述

为了实现这个,你将创建一个包含星形图标和计数的自定义 widget,它们都是 widget。因为点击星形图标会更改这两个 widget 的状态,所以同一个 widget 应该同时管理这两个 widget。

有状态和无状态的 widgets

有些 widgets 是有状态的, 有些是无状态的。如果用户与 widget 交互,widget 会发生变化,那么它就是 有状态的

无状态的 widget 自身无法改变。 IconIconButtonText 都是无状态 widget,它们都是 StatelessWidget 的子类。

有状态的 widget 自身是可动态改变的(基于State)。例如,可以通过与用户的交互或是随着数据的改变而导致外观形态的变化。 CheckboxRadioSliderInkWellFormTextField 都是有状态 widget,它们都是 StatefulWidget 的子类。

一个 widget 的状态保存在一个 State 对象中,它和 widget 的显示分离。 Widget 的状态是一些可以更改的值,如一个滑动条的当前值或一个复选框是否被选中。当 widget 状态改变时,State 对象调用 setState(),告诉框架去重绘 widget。

创建一个有状态的 widget

重点是什么?

  • 实现一个有状态 widget 需要创建两个类:一个 StatefulWidget 的子类和一个 State 的子类。

  • State 类包含该 widget 的可变状态并定义该 widget 的 build() 方法。

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

在本节中,你将创建一个自定义有状态的 widget。你将使用一个自定义有状态 widget 来替换两个无状态 widget—— 红色实心星形图标和其旁边的数字计数—— 该 widget 用两个子 widget 管理一行 IconButtonText

实现一个自定义的有状态 widget 需要创建两个类:

  • 一个 StatefulWidget 的子类,用来定义一个 widget 类。

  • 一个 State 的子类,包含该widget状态并定义该 widget 的 build() 方法。

这一节展示如何为 Lakes 应用程序构建一个名为 FavoriteWidget 的 StatefulWidget。第一步是选择如何管理 FavoriteWidget 的状态。

Step 1: 决定哪个对象管理 widget 的状态

一个 widget 的状态可以通过多种方式进行管理,但在我们的示例中,widget 本身 ——FavoriteWidget—— 将管理自己的状态。在这个例子中,切换星形图标是一个独立的操作,不会影响父窗口 widget 或其他用户界面,因此该 widget 可以在内部处理它自己的状态。

你可以在 状态管理 中了解更多关于 widget 和状态的分离以及如何管理状态的信息。

Step 2: 创建 StatefulWidget 的子类

FavoriteWidget 类管理自己的状态,因此它通过重写 createState() 来创建状态对象。框架会在构建 widget 时调用 createState()。在这个例子中,createState() 创建 _FavoriteWidgetState 的实例,你将在下一步中实现该实例。

class FavoriteWidget extends StatefulWidget {
  const FavoriteWidget({super.key});

  
  State<FavoriteWidget> createState() => _FavoriteWidgetState();
}

Step 3: 创建 State 的子类

_FavoriteWidgetState 类存储可变信息;可以在 widget 的生命周期内改变逻辑和内部状态。当应用第一次启动时,用户界面显示一个红色实心的星星形图标,表明该湖已经被收藏,并有 41 个「喜欢」。状态对象存储这些信息在 _isFavorited_favoriteCount 变量中。

class _FavoriteWidgetState extends State<FavoriteWidget> {
  bool _isFavorited = true;
  int _favoriteCount = 41;

状态对象也定义了 build() 方法。这个 build() 方法创建一个包含红色 IconButtonText 的行。该 widget 使用 IconButton(而不是 Icon),因为它具有一个 onPressed 属性,该属性定义了处理点击的回调方法 (_toggleFavorite)。你将会在接下来的步骤中尝试定义它。

class _FavoriteWidgetState extends State<FavoriteWidget> {
  // ···
  
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        Container(
          padding: const EdgeInsets.all(0),
          child: IconButton(
            padding: const EdgeInsets.all(0),
            alignment: Alignment.center,
            icon: (_isFavorited
                ? const Icon(Icons.star)
                : const Icon(Icons.star_border)),
            color: Colors.red[500],
            onPressed: _toggleFavorite,
          ),
        ),
        SizedBox(width: 18, child: SizedBox(child: Text('$_favoriteCount'))),
      ],
    );
  }

  // ···
}

按下 IconButton 时会调用 _toggleFavorite() 方法,然后它会调用 setState()。调用 setState() 是至关重要的,因为这告诉框架, widget 的状态已经改变,应该重绘。 setState() 在如下两种状态中切换 UI:

  • 实心的星形图标和数字 41

  • 轮廓线的星形图标 star_border 和数字 40 之间切换 UI

void _toggleFavorite() {
  setState(() {
    if (_isFavorited) {
      _favoriteCount -= 1;
      _isFavorited = false;
    } else {
      _favoriteCount += 1;
      _isFavorited = true;
    }
  });
}

完整的 FavoriteWidget 如下

import 'package:flutter/material.dart';

class FavoriteWidget extends StatefulWidget {
  const FavoriteWidget({super.key});

  
  State<FavoriteWidget> createState() => _FavoriteWidgetState();
}

class _FavoriteWidgetState extends State<FavoriteWidget> {
  bool _isFavorited = true;
  int _favoriteCount = 41;

  void _toggleFavorite() {
    setState(() {
      if (_isFavorited) {
        _favoriteCount -= 1;
        _isFavorited = false;
      } else {
        _favoriteCount += 1;
        _isFavorited = true;
      }
    });
  }

  
  Widget build(BuildContext context) {
    return Row(
      mainAxisSize: MainAxisSize.min,
      children: [
        Container(
          padding: const EdgeInsets.all(0),
          child: IconButton(
            padding: const EdgeInsets.all(0),
            alignment: Alignment.center,
            onPressed: _toggleFavorite,
            icon: (_isFavorited
                ? const Icon(Icons.star)
                : const Icon(Icons.star_border)),
            color: Colors.red[500],
          ),
        ),
        SizedBox(
          width: 18,
          child: SizedBox(child: Text('$_favoriteCount')),
        )
      ],
    );
  }
}

Step 4: 将有 stateful widget 插入 widget 树中

将你自定义 stateful widget 在 build() 方法中添加到 widget 树中。首先,找到创建 IconText 的代码,并删除它,在相同的位置创建有状态的 widget,修改后TitleSection代码如下所示

import 'package:flutter/material.dart';
import 'package:flutter_official_example/widgets/favorite_widget.dart';

class TitleSection extends StatelessWidget {
  const TitleSection({super.key, required this.name, required this.location});

  final String name;
  final String location;

  
  Widget build(BuildContext context) {
    return Padding(
        padding: const EdgeInsets.all(32),
        child: Row(
          children: [
            Expanded(
                child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Padding(
                    padding: const EdgeInsets.only(bottom: 8),
                    child: Text(name,
                        style: const TextStyle(fontWeight: FontWeight.bold))),
                Text(location, style: TextStyle(color: Colors.grey[500]))
              ],
            )),
            const FavoriteWidget(),
          ],
        ));
  }
}

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐