从头学 Dart 第六集

72 阅读6分钟

最近打算自己做一个电商 App 调研之后选择技术栈为 Flutter 因此打算梳理一下 Flutter 的所有知识点,做个预热。

  1. flutter 的组件内部提供了全局变量,你可以随时访问 context 变量,其含义表示的就是组件当前的上下文。
  2. 移动端从底部弹出对话框 使用名为 showModalBottomSheet 的函数可以弹出半屏对话框:
void _openAddExpenseOverlay() {
    showModalBottomSheet(
        context: ccontext,
        builder: (ctx) => Text('Modal bottom sheet'),
    )
}

通过 isScrollControlled: true 设置完成全屏弹出:

void _openAddExpenseOverlay() {
    showModalBottomSheet(
        isScrollControlled: true,
        context: context,
        builder: (ctx) => NewExpense(...)
    );
}
  1. 使用 Padding 组件为其子元素设置内边距
Padding(
    padding: EdgeInsets.all(16),
    child: ...
)

除了 EdgeInsets.all 之外还有 EdgeInsets.fromLTRB

padding: EdgeInsets.fromLTRB(16, 48, 16, 16),

这里的 48 是为了留出 safeArea 但是后面我们有更好的做法。

  1. flutter 中的 input 组件 使用 TextField 组件充当 flutter 中的 input 组件,并通过 keyboardType 设置弹出键盘的类型:
TextField(
    maxLength: 50,
    keyboardType: TextInputType.text,
)
  1. 制作一个表单 input 控件
TextField(
    maxLength: 50,
    decoration: InputDecoration(
        label: Text('title'),
    ),
),

我很好奇,label 的值是不是也可以不是 Text 组件。

  1. TextField 的输入回调函数 我们通过以 on 开通的属性为其绑定输入回调函数:
TextField(
    onChange: handleChange,
    maxLength: 50,
    decoration: const InputDecoration(
        label: Text('Title'),
    ),
),

我们可以维护一个变量来保存当前 TextField 的值:

var _enteredTitle = '';

void handleChange(String newVal) {
    _enteredTitle = newVal;
}
  1. TextField 组件的状态管理 我们需要更高级或者更加简洁的状态管理方式,这通常可以通过使用 TextEditingController 类来完成,只不过使用这个类需要在组件销毁之前手动将此类对应的控制实例释放掉。

首先先看看组件在销毁之前会调用哪个生命周期函数:对于 Stateful 类组件,有

@override
void dispose(){
    // other logic
    super.dispose();
}

接下来,我们使用 TextEditingController 实例控制 TextField 的状态:

final _titleController = new TextEditingController();

@override
void dispose(){
    _titleController.dispose();
    super.dispose();
}

...

    TextField(
        controller: _titleController,
        maxLength: 50,
        decoration: const InputDecoration(
            label: Text('Title'),
        ),
    )

可以看出来,只需要将此实例配置给 TextField 的 controller 属性即可!

我们可以通过下面的按钮来验证此控制器是否生效。

ElevatedButton(
    onPressed: () {
        print(_titleController.text);
    },
    child: const Text('Save Expense'),
),

这里只需要注意控制器实例上的 text 属性中可以获取受控组件的实时数据。

  1. 如果观察 TextField 实际的渲染效果,那么容易发现 prefixText 的展示效果,例如在输入金额的时候有的时候我们需要提示货币符号,然后这个时候我们就需要将键盘的类型变成 number 类型的了。
TextField(
    controller: _amountController,
    keyboardType: TextInputType.number,
    decoration: const InputDecoration(
        prefixText: '\$ ',
        label: Text('Amount'),
    )
)
  1. 关闭弹出物 在 flutter 中关闭类似于对话框这类弹出物的基本逻辑并不是将弹出物隐藏起来,而是改变其层级使用将要看到的页面覆盖之
TextButton(
    onPressed: (){
        Navigator.pop(context),
    },
    child: const Text('Cancel'),
),

上面我们调用了 Navigator.pop 弹出想要的页面,入参是 context,之前已经说了 context 表示当前页面的上下文并且是全组件作用域内可 access 的变量。

  1. Row 的无限扩展问题 由于 Row 会在横向无限扩展,所以有的情况下必须对其进行限制,一般采用的策略就是使用 Expanded 组件限制其宽度,这样 Row 就只能最多扩展至 Expanded 的最大尺寸
Expanded(
    child: Row(
        ...
    ),
),
  1. Row 组件对齐方式 类似于 flex 布局,在 Row 组件中也有 主轴 和 附轴 的概念,通过配置这些属性的值可以大概配置 Row 中子组件的位置
Expanded(
    child: Row(
        mainAxisAlignment: MainAxisAlignment.end,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [],
    ),
),

上面就设置了水平据右竖直居中的布局效果。

  1. 日历组件的使用 下面的代码展示了如何打开日历组件并且设置日历组件的初始日期及其它初始值。
void _presentDatePicker() {
    final now = DateTime.now();
    final firstDate = DateTime(now.year - 1, now.month, now.day);

    showDatePicker(
        context: context,
        initialDate: now,
        firstDate: firstDate,
        lastDate: now,
    );
}

和前端的组件库不同,flutter 中弹出的日历组件的入参和出参数据类型都是 DateTime 这和 dart 语言本身保持一直,这一点比 js 要好一些。

注意如果获取当前的时间,并通过一些特定的方法获取当前日期的年月日等信息:DateTime.now(); .year; .month; .day 这些都是属性而不是方法。

  1. flutter 中的 Promise 在 flutter 中,没有 Promise 而是 Future 它们在 api 和设计理念上是高度一致的。我们调用 showDatePicker 方法,此方法返回的就是 Future 类型。
void _presentDatePicker() {
    final now = DateTime.now();
    final firstDate = DateTime(now.year - 1, now.month, now.day);

    showDatePicker(
        context: context,
        initialDate: now,
        firstDate: firstDate,
        lastDate: now,
    ).then((value){

    },)
}
  1. 使用 async/await 处理 Future 数据类型 这个简直和在 js 中一模一样
void _presentDatePicker() async {
    final now = DateTime.now();
    final firstDate = DateTime(now.year - 1, now.month. now.day);
    final pickedDate = await showDatePicker(
        context: context,
        initialDate: now,
        firstDate: firstDate,
        lastDate: now,
    );

    // 检查用户是否选择了日期
    if (pickedDate != null) {
        setState(() {
            _selectedDate = pickedDate;
        });
    }
}

注意的点是,在 dart 中 async 的位置是在小括号之后在花括号之前,这一点和在 js 中不同,但是 await 的位置是正确的。

  1. 日历选择值的使用及断言 如下所示,如果我们想要将 DateTime 的值显示成字符串,则应该事先就确定好处理此种变换的转换函数
Text(_selectedDate == null ? 'No date selected' : formatter.format(_selectedDate!),),

注意这里的 formatter 是事先定义好的。_selectedDate! 则是告诉编译器你错了,这里值肯定是存在的。

  1. 使用 DropdownButton 组件 DropdownButton 组件必须设置的两个配置属性是 value, items 和 onChanged 顾名思义,它们一个为当前选择的值,一个决定需要渲染的内容,一个是选择之后的回调函数。
DropdownButton(
    value: _selectedCategory,
    items: Category.values.map(
        (category) => DropdownMenuItem(
            value: category,
            child: Text(
                category.name.toUpperCase(),
            ),
        )
    ).toList(),
    onChange: (value){
        if(value == null) {
            return;
        }
        setState((){
            _selectedCategory = value;
        }),
    },
)

总结一下:

  • 对于 enum 类型,使用 Category.values 类似 Object.values
  • DropdownMenuItem 类似于 ant design 中的 option 只不过不是 value-label 而是 value-child 因为 child 可能是组件而不只是字符串
  • .map 之后必须对结果使用 toList() 将其转成 children 可以识别的数组
  1. double.tryParse -- flutter 中的 parseFloat 函数
var value = double.tryParse('3.14');// 3.14

value = double.tryParse('3.14 \xA0');// 3.14

value = double.tryParse('0.');//0.0

value = double.tryParse('.0');// 0.0

value = double,trvParse('-1.e3')://-1000.0
  1. 弹出对话框 之前说了从底部弹出对话框,弹出日历,下面介绍弹出一个真正的 dialog 我们使用的是 showDialog 函数。

final enteredAmount = double.tryParse(_amountController.text);
final amountIsInvalid = enteredAmount == null || enteredAmount <= 0;

if(_titleController.text.trim().isEmpty || amountIsInvalid || _selectedDate == null) {
    showDialog(
        context: context,
        builder: (ctx) => AlertDialog(
            title: const Text('Invalid input'),
            content: const Text('Please make sure a valid title, amount, date and category was entered,'),
            actions: [
                TextButton(
                    onPressed: (){
                        Navigator.pop(ctx);
                    },
                    child: const Text('Okay'),
                ),
            ],
        ),
    );

    return;
}

总结一下:

  • 组件库那些可以进行选择的组件,如果没有选择任何项目,那么在回调中将会返回一个 null 值。
  • showDialog 接受两个参数,一个 context 表示打开它的组件,一个是 builder 表示需要弹出的内容。
  • AlertDialog 组件有三个配置项:title content actions 和 Modal.confirm 非常的像。
  1. 数组的可变方法也可以引起组件的更新
void _addExpense(Expense expense) {
    setState((){
        _registeredExpenses.add(expense);
    })
}

void _openAddExpenseOverlay() {
    showModalBottomSheet(
        context: context,
        builder: (ctx) => NewExpense(onAddExpense: _addExpense)
    );
}
  1. 举例说明 State 类调用外部类中的方法
widget.onAddExpense(
    Expense(
        title: _titleController.text,
        amount: enteredAmount,
        data: _selectedDate!,
        category: _selectedCategory,
    )
)

Navigator.pop(context);

这里的逻辑是:点击添加按钮然后进行表单校验,成功添加新组件失败弹窗。