07 使用组件构建静态界面

1. 代码的分文件管理

在计数器项目中,我们知道界面中展示的内容和组件息息相关,其中其决定性作用的是 MyHomePage 组件。但所有的代码都塞在了 main.dart 文件中,随着需求的增加,把所有代码都放一块,显然是不明智的。所以首先来看一下如何分文件来管理代码。

如下所示,先创建一个 counter 文件夹,用于盛放计数器界面的相关代码文件;然后创建 counter_page.dart 文件,并把 MyHomePage 的相关代码放入其中:

image.png

这时,可以将 main.dartMyHomePage 的相关代码删除;会发现红色的波浪线,表示找不到 MyHomePage 类型:

image.png

此时只需要通过 import 关键字,在 main.dart 上方导入文件即可:

---->[main.dart]----
import 'counter/counter_page.dart';

// 略同...

分文件管理代码就像整理书籍,分门别类地进行摆放,各个区域各司其职,自己容易检阅,别人也容易看懂。下面创建一个 guess 文件夹,用于盛放本模块 猜数字 小项目的相关代码:

image.png


2. 创建你自己的组件

首先要明确一点,文件夹的名称、文件的名称、类型的名称、属性的名称、函数的名称,都是可以任意的,甚至可以使用汉字(但不建议),只要在使用时对应访问即可。但一个好名字对于阅读来非常重要,取 a1,c,b,d45,rrr 这样的名字,对阅读者而言是灾难,也许过两天,连你自己也认不得。所以一个好名字是个非常重要,不要偷懒, 最好有明确的含义。

比如对于猜数字这个需求来说,整体的界面可以叫 GuessPage , 这里我们可以先借用一下计数器中 MyHomePage 代码,照葫芦画瓢,改巴改巴。先把 MyHomePage 代码复制到 guess_page.dart 中。

重命名小技巧: 当你想对一个类型、函数、属性名进行重命名,并且想让在它们使用使用处自动修改。可以将鼠标点在名称上,右键 -> Refactor -> Rename ; 也可以使用后面的快捷键直接操作。

image.png

输入新名称后,点击 Refactor , 所有使用处都会同步更新:

image.png


然后在 main.dart 中,将 GuessPage 作为 home 参数,即可展示 GuessPage 组件中的界面效果:

---->[main.dart]----
import 'guess/guess_page.dart';
    // 略同...
    home: const GuessPage(title: '猜数字'),

接下来的任务是如何修改代码,来完成猜数字的功能需求。首先我们来完成一件简单的事:

点击按钮生成随机 0~99 之间的数字

初始效果 点击生成随机数
ff65cdea34310089ac31947e551fa6f.jpg 200f38a084930d97d4b2420703445c1.jpg

3.随机数的生成与界面更新

计数器项目中,点击按钮之所以界面数字自加,是因为 _incrementCounter 方法中,触发了 _counter++ 。所以想要在点击时显示随机数,思路很简单:将 _counter 变量赋值为随机数即可。

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

所以首先需要了解一下 Dart 中如何生成随机数:在 dart 的 math 包中,通过 Random 类型可以生成随机数。这里生成的是随机整数,使用 nextInt 方法,它有一个入参,表示生成随机数的最大值(不包含)。比如传入 100 时,将返回 0~99 之间的随机整数:

import 'dart:math';

Random _random = Random();
_random.nextInt(100);

这样,点击按钮时只要将 _counter 赋值为随机数即可,如下所示:

void _incrementCounter() {
  setState(() {
    _counter = _random.nextInt(100);
  });
}

但之前说过,名字非常重要。但这里 _counter 含义是计数器,_incrementCounter 含义是自增计数器,在当前需求的语境中并不是非常适合。起名字最好和其功能相关,比如可以将数值变量可以称之 _value ; 方法可以称之 _generateRandomValue,这样代码阅读起来就会更容易理解。 同样,也可以使用 Refactor 重命名:

class _GuessPageState extends State<GuessPage> {

  int _value = 0;
  
  Random _random = Random();
  
  void _generateRandomValue() {
    setState(() {
      _value = _random.nextInt(100);
    });
  }

4. 头部栏 AppBar 和输入框 TextFiled 的使用

接下来,我们要将头部的标题栏 AppBar 改成如下的样式,中间是可以输入的输入框,右侧是一个运行的图标按钮。

image.png


AppBar 常用于左中右布局结构,如下所示:

AppBar(
  leading: 左侧,
  actions: [右侧列表],
  title: 中间部分,
)

也就是说在不同的参数中,可以插入不同的组件进行显示。比如这里 leading 指定为 Icon 组件,展示图标:

leading: Icon(Icons.menu, color: Colors.black,),

actions 入参是一个组件列表,这里放入一个 IconButton 组件,展示图标按钮:

actions: [
  IconButton(
    splashRadius: 20,
    onPressed: (){}, 
    icon: Icon(Icons.run_circle_outlined, color: Colors.blue,)
  )
],

title 入参是中间部分,使用 TextField 组件展示输入框。这里组件构造时的入参对象都是用于配置展示信息的,可以简单认识一下,不用急着背诵,以后慢慢接触,早晚都会非常熟悉。

TextField(
  keyboardType: TextInputType.number, //键盘类型: 数字
  decoration: InputDecoration( //装饰
      filled: true, //填充
      fillColor: Color(0xffF3F6F9), //填充颜色
      constraints: BoxConstraints(maxHeight: 35), //约束信息
      border: UnderlineInputBorder( //边线信息
        borderSide: BorderSide.none,
        borderRadius: BorderRadius.all(Radius.circular(6)),
      ),
      hintText: "输入 0~99 数字", //提示字
      hintStyle: TextStyle(fontSize: 14) //提示字样式
   ),
),

最后说一下,如何去掉顶部的灰块:AppBar 组件在构造时,可以通过 systemOverlayStyle 入参控制状态类和导航栏的信息。如下代码可以使顶部状态栏变成透明色,文字图标是暗色:

image.png

 AppBar(
  systemOverlayStyle: SystemUiOverlayStyle(
      statusBarIconBrightness: Brightness.dark,
      statusBarColor: Colors.transparent
  ),

5. 叠放 Stack 组件和列 Column 组件的使用

当用户输入的数字大了,或小了。需要界面上给予提示。为了更加醒目,这里给出的设计如下所示。如果大了,上半屏亮红色,展示 大了;如果小了,下半屏亮蓝色,展示 小了

小了提示 大了提示
f74d44ab6106026953e6dda9e450c54.jpg b4916896eb9b558ad636b628f744151.jpg

可以看出此时中间的文字是浮在提示色块之上的,想实现这种多个组件层层堆叠的效果,可以使用 Stack 组件来完成。其构造函数中 children 入参传入组件列表,在显示层次上后来居上。 如下所示,将 Stack 作为 body , 在其中放入一个红色的 Container 容器组件, 以及之前的主内容。运行后会看到:中间文字就会浮在红色容器之上。

image.png

body: Stack(
  children: [
    Container(color: Colors.redAccent),
    //主内容略...
  ],
),

除了堆叠的效果,还有一个问题,如何实现 上下平分区域 呢? 我们前面知道 Column 组件可以让两个组件竖直排列。在 Column 中可以通过 Expanded 组件延展剩余区域,另外 Spacer() 组件相当于空白的 Expanded。当存在多个 Expanded 组件时,就可以按比例分配剩余空间,默认是 1:1 ,也可以通过 flex 入参调节占比。举个小例子,如下所示:

Expanded 红+Expanded 蓝 Expanded 红+ Spacer
88cd5a24357e1c6d7aa2316843f88c6.jpg e89ef153faf5d9911939eb610d8a2d8.jpg
---->[红+蓝]----
Column(
  children: [
    Expanded( child: Container(color: Colors.redAccent)),
    Expanded( child: Container(color: Colors.blueAccent)),
  ],
),

---->[红+空]----
Column(
  children: [
    Expanded( child: Container(color: Colors.redAccent)),
    Spacer()
  ],
),

到这里,猜数字项目中需要使用的基础组件就已经会师完毕。大家可以基于现在已经掌握的知识,完成如下效果: 注: Container 组件的 child 入参可以设置内容组件。 本例参考代码见: guess_page.dart

image.png


6.组件的简单封装

上面虽然完成了布局效果,但是从 guess_page.dart 中可以感觉到,随着需求的增加,各种组件全都塞到了一块。这才只是一个简单的布局,就已经有点不堪入目了;如果再加上交互的逻辑,恐怕要乱成一锅粥了。

其实对于新手而言,编程语言语法本身并不是什么难事,对规整代码的把握才是最大的挑战;很容易要什么,写什么,最后什么东西都塞在一块,连自己都看不下去了,从而心灰意冷,劝退放弃。小学时,老师就教导我们,遇到巨大的问题,要尝试将它分解成若干个小问题,逐一解决。

其实有些大问题在肢解过程中,会有某些类似的小问题,这些小问题可以通过某种相同的解决方案来处理。这种通用解决方案,就是一种封装的思想,问题的专属解决方案一旦封装完毕,输入问题,就可以解决问题,使用者不必在意处理的过程,可以大大提升解决问题的效率。回忆一下,在介绍函数时,通过 bmi 函数,封装体质指数的计算公式,就是通过函数来封装解决方案。


对于组件来说,也是一样:某些相似的结构,也可以通过 封装 进行复用。比如这里 大了小了 只是颜色和文字不同,两者的结构类似。就可以通过封装来简化代码: 最简单的封装形式是通过函数封装,通过入参来提供界面中差异性的信息,如下所示 _buildResultNotice 函数接收颜色和消息,返回 Widget 组件:

Widget _buildResultNotice(Color color, String info) {
  return Expanded(
      child: Container(
    alignment: Alignment.center,
    color: color,
    child: Text(
      info,
      style: TextStyle(
          fontSize: 54, color: Colors.white, fontWeight: FontWeight.bold),
    ),
  ));
}

如果把相似的结果写两遍,只会徒增无意义的代码。而封装之后,只需要调用方法,就可以完成任务:

Column(
  children: [
    _buildResultNotice(Colors.redAccent,'大了'),
    _buildResultNotice(Colors.blueAccent,'小了'),
  ],
),

如果封装体的代码非常复杂,或者需要单独维护,以便之后修改方便定位,也可以通过新组件的形式来封装组件。如下所示,新创建 result_notice.dart 文件,在其中定义 ResultNotice 组件,专门处理结果提示信息的展示任务。

image.png

一方面,专人专用,需要更改时直接在这里更改。比如你让另一个人帮忙改改某处的代码,而他对项目不熟悉,如果代码分离的得当,你告诉他这个界面由 xxx.dart 文件负责,他就可以在不了解项目的前提下,对界面进行修改。这就是 职责分离 的益处。不同人干自己擅长的事,有利于整体结构的稳定。

另一方面也能缓解 guess_page.dart 中的代码压力,不至于随着需求的增加代码量激增,对可读性友好。在使用时,可以将 ResultNotice 视为普通的组件,放在 Column 之中:

Column(
  children: [
    ResultNotice(color:Colors.redAccent,info:'大了'),
    ResultNotice(color:Colors.blueAccent,info:'小了'),
  ],
),

同样,这里 AppBar 组件的构建逻辑也是太复杂的,可以在 guess_app_bar.dart 中单独维护。这里主要是出于简化 guess_page.dart 中代码的考量,不强求可复用性。

image.png

这样 guess_page.dart 中的代码就整洁了很多,其他两个文件也在各司其职。相对与之前全塞在一块,更便于阅读,封装之后的代码见 guess_page.dart


7.本章小结

本章主要学习了如何通过 Flutter 框架提供的组件,来搭建期望的界面呈现效果。期间介绍了如何分文件来管理代码、以及通过自定义组件来封装构建逻辑。最后简单分析了一下组件封装的优势。

学完本章,你应该能够自己动手搭建一些简单的静态界面了。但应用程序是要和用户进行交互的,就需要界面随着用户的交互进行变化。下一篇将从用户交互的角度,通过代码来实现猜数字的具体功能。