最近打算自己做一个电商 App 调研之后选择技术栈为 Flutter 因此打算梳理一下 Flutter 的所有知识点,做个预热。
- flutter 中也分 dependencies 和 dev_dependencies 比较常见的依赖库有
- cupertino_icons@1.0.2
- uuid@3.0.7
- intl@0.18.0 而常见的开发依赖是:
- flutter_lints@2.0.0
- 为了能够得到设备的实时方向信息(横屏或者是竖屏)我们需要引入内置库 services
import 'package:flutter/material.dart';
然后将 runApp 放入下面所示的 then 的回调函数中
void main(){
WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setPreferredOrientations([
Device0rientation.portraitUp,
]).then((fn){
runApp(...);
})}
其中 WidgetsFlutterBinding.ensureInitialized(); 的作用是为了保证执行顺序,和 window.onload 类似。
- 组件在设备横竖切换之后会自动调用 build 重新构建页面,可以在 build 函数中打印查看 使用 媒体查询可以得到当前页面的尺寸:
MediaQuery.of(context).size.width
MediaQuery.of(context).size.height
这里的宽和高都是属性而不是方法。
- 数组中不仅有 isEmpty 方法还有 isNotEmpty 方法,这并不冗余
Widget mainContent = const Center(
child: Text('No expenses found. start adding some!'),
);
if (registeredExpenses.isNotEmpty) {
mainContent = ExpensesList(
expenses: registeredExpenses,
onRemoveExpense: removeExpense,
);
}
- 使用媒体查询的结果针对横屏或者竖屏使用不同的渲染布局
body: width < 600
? Column(
children: [
Chart(expenses: _registeredExpenses),
Expanded(
child: mainContent,
),
],
)
: Row(children: [
Expanded(
child: Chart(expenses: registeredExpenses),
),
Expanded(
child: mainContent,
),
]),
这个能生效完全基于 build 函数的再次执行。
- 组件的默认尺寸
Widget 的大小取决于它们的大小偏好和父 Widget 的尺寸约束。以 Scaffold 和 Column 为例,Scaffold 的尺寸约束是最大设备高度和宽度,而 Column 则尽可能多地获取高度,宽度由其子 Widget 决定。最终,Column 的大小将具有最大设备高度和根据子 Widget 决定的宽度。而 Row 刚好是相反的。
在 Flutter 中,无限尺寸的父元素不能直接包含无限尺寸的子元素,否则可能导致布局问题。通常需要在它们之间添加一个有固定尺寸的元素,如 SizedBox,来提供约束。
- 弹出键盘之后的空间预留问题 可以查询得到键盘所需的预留高度:
final keyboardSpace = MediaQuery.of(context).viewInsets.bottom;
得到这个值之后我们给一个 padding 预留此位置:
child: singlechildscrollview(
child: Padding(
padding:EdgeInsets.fromLTRB(16,48,16,keyboardSpace + 16),
child: Column(
children:[...],
),
),
),
这里使用的组件搭配是 singlechildscrollview -> Padding -> Column
- 值得注意的是在页面的头部也存在上述安全区域的问题 但是并不是所有的组件都需要手动去设置安全区域的,如果我们使用的是 Scaffold 组件,那么它会自动留出安全区域,并且提供 AppBar 配置项供开发者自定义。但是对于有些 Modal 你必须显式告诉它需要留出安全区域:
void openAddExpenseOverlay() {
showModalBottomSheet(
useSafeArea: true,
isScrollControlled: true,
context: context,
builder: (ctx) => NewExpense(onAddExpense: addExpense),
);
}
这里是通过 useSafeArea: true 设置实现的。
- 得到组件的空间信息 如果我们需要设置 padding 就用 Padding 组件包裹目标组件;同理,如果我们想要获取组件的空间信息那么同样需要为其包裹特殊组件 LayoutBuilder, 这个组建为其子组件提供了一个名为 constraints 的上下文对象,相关的位置信息都保留在此对象中。
@override
Widget build(BuildContext context) {
final keyboardSpace = MediaQuery.of(context).viewInsets.bottom;
return LayoutBuilder(
builder: (ctx, constraints) {
final width = constraints.maxWidth;
return SizedBox(
height: double.infinity,
child: SingleChildScrollView(
child: Padding(
padding: EdgeInsets.fromLTRB(16, 16, 16, keyboardSpace + 16),
child: Column(
children: [
if (width >= 600)
Row(
children: [
TextField(
controller: titleController,
maxLength: 50,
decoration: const InputDecoration(
label: Text('Title'),
),
),
const SizedBox(width: 24),
Expanded(
child: TextField(
controller: amountController,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
prefixText: '\$',
),
),
),
],
),
// ... 其他组件
],
),
),
),
);
},
);
}
屏幕方向影响的是组件可用空间的变化,因此有的时候比起屏幕的状态,我们更关心组件能用的空间有多少,这就是使用 Layout 组件的意义。
- if 中可以省略的花括号
在 Dart 中,你可以在以下情况下省略
if-else语句的花括号:
- 当
if或else块中只有一个语句时。 - 当
if或else块中的语句是一个返回语句(return)时,即使有多个语句也不需要花括号。
以下是一些例子:
单语句省略花括号:
int max(int a, int b) {
return a > b ? a : b;
}
在这个例子中,if-else表达式被用作返回语句,因此不需要花括号。
多语句省略花括号(当使用return时):
void exampleFunction() {
String condition = "some condition";
if (condition == "some condition")
return "Condition is true";
else
return "Condition is false";
}
在这个例子中,即使if和else块中有多条语句,但由于它们都是返回语句,所以不需要花括号。
单语句省略花括号(非返回语句):
void exampleFunction(bool flag) {
if (flag) print("Flag is true");
else print("Flag is false");
}
在这个例子中,if和else块中都只有一个print语句,因此可以省略花括号。
需要注意的是,虽然可以省略花括号,但在某些情况下,省略花括号可能会导致代码的可读性降低,尤其是在复杂的条件语句中。因此,是否省略花括号应根据具体情况和代码风格指南来决定。
- 在 dart 中获取设备信息 最重要的就是获取当前程序是跑在 ios 还是 android 上面。
以下面为例,这里 AppBar 组件的文字在 Android 和 ios 上一个居中一个靠左,所以默认布局是不同的,为此我们可以手动的强制规定其排布的方式:
Scaffold(
appBar: AppBar(
centerTitle: false,
title: const Text('Flutter ExpenseTracker'),
actions: [
IconButton(
onPressed: openAddExpenseOverlay,
icon: const Icon(Icons.add),
),
],
),
// ... 其他Scaffold属性和body
)
这样 title 就统一靠左了。
- cupertino Cupertino 是苹果公司设计的一种用户界面风格,它以简洁、直观和响应式为特点,广泛应用于 iOS 和 macOS 操作系统中。这种设计风格强调清晰、易读的字体、微妙的动效和自然的过渡效果,以及直观的手势操作。
在 Flutter 框架中,Cupertino 指的是一套与 iOS 原生应用风格一致的 UI 组件库。这些组件包括按钮、滑块、开关、导航栏、标签栏等,它们的外观和行为都与 iOS 系统中的原生组件非常相似。使用 Cupertino 组件,开发者可以为 iOS 设备创建具有原生感的应用程序,而无需使用 Swift 或 Objective-C。
Cupertino 组件库提供了以下优势:
- 一致性:确保应用的 UI 与 iOS 系统的其他应用保持一致。
- 易用性:用户可以轻松地与熟悉的界面元素进行交互。
- 性能:Flutter 的 Cupertino 组件是高效且响应迅速的,它们利用了 Flutter 的高性能渲染引擎。
- 定制性:虽然 Cupertino 组件模仿了原生 iOS 组件的外观和行为,但它们仍然可以通过 Flutter 的定制能力进行个性化调整。
使用 Cupertino 组件,开发者可以创建出既符合 iOS 设计规范,又能提供流畅用户体验的应用。
- 判断设备使用 ISO 平台并使用 cupertino 风格的组件 首先引入相关的依赖包:
import 'package:flutter/cupertino.dart';
然后引入判断设备平台的包:
import 'dart:io';
最后我们就可以判断性的,在 IOS 平台上使用 cupertino 风格的组件了
void showDialog() {
if (Platform.isIOS) {
showCupertinoDialog(
context: context,
builder: (ctx) => CupertinoAlertDialog(
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'),
),
],
),
);
} else {
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'),
),
],
),
);
}
}
我们进行条件渲染的依据就是:Platform.isIOS.
- flutter 中的三大 tree flutter 中有三大 tree 分别是:widget element 和 render tree.
-
Widget 树:这是你的代码中组合的 UI 构建块,是 Flutter 应用界面的内存表示。
-
Element 树:这个树用于确定必要的 UI 更新。当
build方法被调用时,它会频繁地检查是否有需要更新的地方,并通过 Element 树的比较来决定是否需要重新渲染。 -
Render 树:这是实际可见的 UI 构建块,用于渲染最终用户界面。
强调以下几点:
- 初始创建是在
build方法之后进行的。 - 如果需要,Widget 树会被重新渲染。
- Element 树会在确定需要更新时通过比较来更新。
build方法被频繁调用以检查所需的更新。- 如果可能,Element 会被重用,以优化性能。
与此相关的还有一个非常重要的原则,那就是 flutter 只需更新需要更新的组件,所以会尽可能的进行复用,但是这是有代价的,复用和新建比起来总是会出现问题,后面会看到。
而 flutter 复用组件的原则就是树上的节点在更新前后是否属于同一个 widge. flutter 通过对比 element tree 决定重绘 render tree 的哪一部分。
所以,flutter 中复用的对象从来就不是 widget tree 上的节点而是 element tree 上的节点。
基于此,一个重要的提升 flutter application 性能的原则就是:让可变组件体量越小越好。
- widget element 和 state、 在 Flutter 框架中,Widget、Element 和 State 之间存在着一种特殊的关系。Widget 是构建用户界面的蓝图,而 State 则负责管理 Widget 的状态。这种状态可以是任何东西,比如文本字段中的文字、复选框是否被选中,或者是任何其他需要在 Widget 重建之间保持的信息。
Element 是 Widget 的运行时实体,它包含了 Widget 的配置信息(如属性和状态)以及与渲染树的连接。当 Widget 被插入到树中时,Flutter 会为每个 Widget 创建一个 Element。Element 负责将 Widget 的配置信息传递给 Render 树,Render 树则是实际在屏幕上绘制内容的部分。
State 与 Widget 的连接不是直接的,而是通过 Element 来实现的。当 Widget 需要重建时,它的 Element 会保留 State,这样即使 Widget 本身被重建,State 也不会丢失。这种设计允许 Flutter 框架在构建新的 Widget 树时,能够保持状态的连续性,从而为用户提供一致的体验。
这里的关键点是:
- Widget:定义了用户界面的结构和布局。
- Element:是 Widget 的实例,负责将 Widget 的配置信息传递给 Render 树。
- State:与 Element 关联,而不是直接与 Widget 关联,它保存了 Widget 的状态信息。
这种设计模式使得 Flutter 能够在 Widget 树发生变化时,例如在列表项重新排序时,保持 Widget 的状态。但是,如前所述,这也可能导致一些问题,比如在列表项重新排序时,Widget 的状态没有正确更新。在这种情况下,使用key属性可以帮助 Flutter 正确地识别和更新 Widget 的状态,确保状态与 Widget 的新位置同步。
- element 复用造成的问题 在 Flutter 中,为了提高性能,框架会尝试重用已经存在的 Widget 实例,尤其是在处理列表时,如果列表项的顺序发生变化,Flutter 会尝试保持 Widget 的实例不变,即使它们在视觉上移动到了不同的位置。这种优化策略被称为“复用”。
然而,这种复用机制可能会带来一些副作用。在 Flutter 中,Widget 的状态(State)是与 Element 关联的,而不是与 Widget 本身直接关联。Element 是 Widget 在 Flutter 渲染树中的运行时表示。当 Widget 的顺序改变时,虽然 Widget 的引用可能发生了变化,但是它们的 Element 和 State 可能保持不变。
这可能导致一些预期之外的行为,特别是对于那些有内部状态的 Widget,比如复选框(Checkbox)。如果你有一个可排序的列表,其中包含了复选框,当列表项的顺序改变时,复选框的显示状态(被选中或未选中)可能不会随之更新,因为状态仍然与原来的 Element 关联,而不是新的 Widget 位置。
为了解决这个问题,你可以使用key属性。在 Flutter 中,key是一个唯一的标识符,用于帮助 Flutter 确定哪些 Widget 是相同的,哪些是不同的。当你为 Widget 指定一个key时,Flutter 会使用这个key来跟踪 Widget 的身份,即使它们在列表中的位置发生变化。这样,当 Widget 的顺序改变时,Flutter 可以正确地更新 Widget 的状态,确保状态与 Widget 的新位置保持同步。
简而言之,使用key可以确保即使 Widget 在视觉上移动了位置,它们的状态也能正确地随之更新,避免了因 Widget 复用而导致的状态错位问题。
- keys in flutter flutter 的复用规则是看 widget 前后是否一致,如果一致会复用但是不会检查 state 但是加上 key 之后它也会检查 key 是否一致,如果不一致,则会进一步检查 state, 因此有了 key 就可以保证 state 随动。
为每个组件设置独一无二的 id 即 key,首先我们必须引入相关库:
import "package:flutter internals/keys/keys.dart';
- 一种是设置 值 key:
key: ValueKey(todo.text)
- 另一种是设置 组件 key:
key: objectKey(todo),
第一种简单所以用的多,但是不管怎么样都不要使用随机数作为 key 生成用的盐。
- const 是不可变的常量 为了说明这个问题,看下面两种等价的写法:
const numbers = [1,2,3];
final _numbers = const [1,2,3];
- 数组上的一些方法 使用 of 来完成对现有数组的复制,这样做的原因在于有一些数组上的方法会导致数组本身的改变,也就是 js 数组中的 mutation methods 这个时候,如果我们不希望接下来的操作能够影响原数组,通常我们会对原数组进行复制:
final sortedTodos = List.of(_todos);
sortedTodos.sort((a,b){
return a-b;
});