最近打算自己做一个电商 App 调研之后选择技术栈为 Flutter 因此打算梳理一下 Flutter 的所有知识点,做个预热。
- 滑动删除组件 移动端常见的交互效果,例如滑动屏幕之后删除某个 item 这个效果就是通过名为 Dismissible 组件实现的。
这是一个包裹组件,被包裹的组件才是真正要显示出来的内容,通常 Dismissible 组件会结合 ListView 组件一起使用。
final List<Expense> expenses;
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: expenses.length,
itemBuilder: (ctx, index) => Dismissible(
key: ValueKey(expenses[index]),
child: ExpenseItem(expenses[index]),
)
);
}
先不用去管 key 这是 ListView 要求的,并且会在后面详细说。这样来看 Dissmissible 本质上就是一个简单的包裹组件。
Attention!
必须注意,Dismissible 的删除只是在 ui 上删除,也就是 element 层面的删除,而不会涉及 widget 层面,如果组件刷新还是会出来的。所以开发者必须保证两者的统一,即手动删除背后的渲染数据!
- 数组的删除元素的方法 dart 中提供了删除数组元素的方法,和 js 中不同,并不是通过 index 和 splice 完成的,而是直接提供了 remove 方法,如果具有待删元素的引用则删除起来非常简单:
void _removeExpense(Expense expense) {
setState((){
_registeredExpenses.remove(expense);
})
}
然后我们将删除元素的回调设置在 Dismissible 组件配置项里面就可以了:
final List<Expense> expenses;
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: expenses.length,
itemBuilder: (ctx, index) => Dismissible(
key: ValueKey(expenses[index]),
onDismissed: (direction) {
onRemoveExpense(expenses[index]);
},
child: ExpenseItem(expenses[index]),
)
);
}
所以 Dismissible 的核心配置就是 child 和 onDismissed 两个配置项。
- 页面效果:没有元素的时候显示占位符 字符串中提供了 trim 方法 和 isEmpty 属性,当这个字符串是关键数据的时候,我们可以根据其值的合理性选择性的渲染真实的组件或者是占位符:
import 'package:flutter/material.dart';
class YourWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 假设 registeredExpenses 是一个包含费用项的列表
final List<Expense> registeredExpenses = [];
// 当列表中没有元素时显示占位符文本
Widget mainContent = const Center(
child: Text('No expenses found. Start adding some!'),
);
// 如果列表中有元素,则显示费用列表
if (registeredExpenses.isNotEmpty) {
mainContent = ExpensesList(
expenses: registeredExpenses,
onRemoveExpense: removeExpense,
);
}
return mainContent;
}
void removeExpense(Expense expense) {
// 实现移除费用项的逻辑
}
}
- 页面效果:删除元素之后弹出 snackbar 并提供撤销操作的按钮 与此相关有四个知识点:
- 数组删除某个元素 (remove)
- 数组添加某个元素,在特定位置 (insert)
- 弹出 snackbar 组件 ScaffoldMessenger.of(context).showSnackBar
- 移除 snackBar 组件 ScaffoldMessenger.of(context).clearSnackBars
void removeExpense(Expense expense) {
final expenseIndex = registeredExpenses.indexOf(expense);
setState(() {
registeredExpenses.remove(expense);
});
ScaffoldMessenger.of(context).clearSnackBars();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
duration: const Duration(seconds: 3),
content: const Text('Expense deleted.'),
action: SnackBarAction(
label: 'undo',
onPressed: () {
setState(() {
registeredExpenses.insert(expenseIndex, expense);
});
},
),
),
);
}
这里有一个很重要的顺序,那就是先清除 snackbar 然后再弹出新的。
其中,duration 是 snackbar 的持续时间,content 是内容,action 表示按钮。
- 配置自定义主题 在 main runApp MaterialApp 的结构中,我们挨着 home 配置 theme
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
theme: ThemeData().copyWith(
useMaterial3: true,
scaffoldBackgroundColor: Color.fromARGB(255, 220, 189, 252),
),
home: const Expenses(),
),
);
}
使用 ThemeData().copyWith() 可以提供比直接使用 useMaterial3: true 这种配置更完整的配置。如果不用 copywith 则主题中只有前景色和背景色生效。
- 使用颜色种子
var kColorScheme = ColorScheme.fromSeed(
seedColor: const Color.fromARGB(255, 96, 59, 181),
);
然后在主题配置的时候使用这个颜色种子:
void main() {
runApp(
MaterialApp(
theme: ThemeData().copyWith(
useMaterial3: truem
colorScheme: kColorScheme,
appBarTheme: AppBarTheme().copyWith(
backgroundColor: kColorScheme.onPrimaryContainer,
foregroundColor: kColorScheme.primaryContainer,
)
),
home: const Expenses(),
)
)
}
不难看出来,颜色种子既可以作为一个集合设置一大片,也可以通过属性的方式精确设置。上面代码的 onPrimaryContainer 和 primaryContainer 表示的是平常状态下的颜色和按下激活的颜色。
- 更多示例
// 定义应用的主题数据
final ThemeData themeData = ThemeData()
// 设置颜色方案
.copyWith(
colorScheme: kColorScheme,
// 自定义AppBar的主题
appBarTheme: const AppBarTheme().copyWith(
// AppBar的背景颜色
backgroundColor: kColorScheme.onPrimaryContainer,
// AppBar的前景色
foregroundColor: kColorScheme.primaryContainer,
),
// 自定义卡片的主题
cardTheme: const CardTheme().copyWith(
// 卡片的背景颜色
color: kColorScheme.secondaryContainer,
// 卡片的外边距
margin: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
),
// 自定义凸起按钮的主题
elevatedButtonTheme: ElevatedButtonThemeData(
// 凸起按钮的样式
style: ElevatedButton.styleFrom(
// 按钮的背景颜色
backgroundColor: kColorScheme.primaryContainer,
),
),
// 自定义文本主题
textTheme: ThemeData().textTheme.copyWith(
// 标题大号文本样式
titleLarge: TextStyle(
// 字体的粗细
fontWeight: FontWeight.normal,
// 文本的颜色
color: kColorScheme.onSecondaryContainer,
// 文本的字体大小
fontSize: 14,
),
),
);
// 注意:kColorScheme 应该是一个已经定义好的ColorScheme对象,这里没有给出其定义。
- 精确使用设置的主题 以 Text 组件为例
Text(
expense.title,
style: Theme.of(context).textTheme.titleLarge,
)
我们使用的 Theme 就是全局配置的主题,而 Theme.of(context) 表示的是在设置主题的大前提下适用此组件的的主题分类,然后在此分类之下又有几层分类。
设置 Dissmissible 组件移除的时候露出来的背景色
// itemBuilder 用于构建列表中的每个项目
itemBuilder: (ctx, index) => Dismissible(
// 使用 ValueKey 来唯一标识每个 Dismissible widget
key: ValueKey(expenses[index]),
// 设置滑动删除时的背景色
background: Container(
color: Theme.of(context).colorScheme.error, // 使用主题中的错误颜色
margin: EdgeInsets.symmetric(horizontal: Theme.of(context).cardTheme.margin!.horizontal) // 使用卡片主题的外边距
),
// 当 Dismissible 被滑动删除时调用的回调
onDismissed: (direction) {
// 调用 removeExpense 方法来处理删除逻辑
onRemoveExpense(expenses[index]);
},
// Dismissible 的子组件,即列表项内容
child: ExpenseItem(
expense: expenses[index],
),
),
主题不仅包括颜色还有内外边距等内容。
- 设置颜色的透明度,使用
.withOpacity(0.75)
background: Container(
color: Theme.of(context).colorScheme.error.withOpacity(0.75),
margin: EdgeInsets.symmetric(
horizontal: Theme.of(context).cardTheme.margin!.horizontal,
),
)
- 使用暗黑颜色种子设置暗黑主题
// 定义暗色主题的ColorScheme
var kDarkColorScheme = ColorScheme.fromSeed(
// 使用Color.fromARGB定义一个深紫色作为种子颜色
seedColor: const Color.fromARGB(255, 5, 99, 125),
// 设置暗色主题的背景色、表面色、错误色等
brightness: Brightness.dark,
);
void main() {
runApp(
MaterialApp(
// 使用暗色主题
darkTheme: ThemeData.dark(
// 复制暗色主题并启用Material 3风格
.copyWith(
useMaterial3: true,
colorScheme: kDarkColorScheme,
),
),
// 使用自定义的主题
theme: ThemeData().copyWith(
// 复制默认主题并启用Material 3风格
useMaterial3: true,
// 使用自定义的ColorScheme
colorScheme: kDarkColorScheme,
),
home: const Expenses(), // 假设Expenses是你的主页组件
),
);
}
和 theme 配置项并列的是名为 darkTheme 的配置项,在此配置即可。在配置暗色颜色种子的时候,我们一般会给一个 brightness 设置明暗程度 brightness: Brightness.dark,。
- 亮色 暗色的切换准则 在 theme 配置项的同一级使用名为 themeMode: ThemeMode.x 配置亮色暗色切换规则,其中 x 的取值可以有:
- dark
- light
- system
- 一个比较完整的配色方案
import 'package:flutter/material.dart';
import 'package:expense_tracker/widgets/expenses.dart'; // 确保路径正确
// 定义亮色主题的ColorScheme
var kColorScheme = ColorScheme.fromSeed(
seedColor: const Color.fromARGB(255, 96, 59, 181),
);
// 定义暗色主题的ColorScheme
var kDarkColorScheme = ColorScheme.fromSeed(
seedColor: const Color.fromARGB(255, 5, 99, 125),
);
void main() {
runApp(
MaterialApp(
// 使用暗色主题
darkTheme: ThemeData.dark().copyWith(
useMaterial3: true,
colorScheme: kDarkColorScheme,
cardTheme: const CardTheme().copyWith(
color: kColorScheme.secondaryContainer, // 使用亮色主题的secondaryContainer颜色
margin: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
),
elevatedButtonTheme: ElevatedButtonThemeData(
// 凸起按钮的样式
style: ElevatedButton.styleFrom(
// 按钮的背景颜色
backgroundColor: kColorScheme.primaryContainer,
foregroundColor: kColorScheme.onPrimaryContainer,
),
),
),
// 使用亮色主题
theme: ThemeData().copyWith(
useMaterial3: true,
colorScheme: kColorScheme,
cardTheme: const CardTheme().copyWith(
color: kColorScheme.secondaryContainer, // 使用亮色主题的secondaryContainer颜色
margin: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 8,
),
),
),
home: const Expenses(), // 假设Expenses是你的主页组件
),
);
}
- for...in 遍历 如下代码,我们有时候需要在 dart 中完成一些遍历,例如计算数组的和,这个时候使用 for in 循环再合适不过
double get totalExpenses {
double sum = 0;
for (final expense in expenses) {
sum += expense.amount;
}
return sum;
}
到这里,大致可以将 final 理解成 js 中的 let 了。
下面是使用数组上面的 fold 方法,类似于 js 中的 reduce:
double get totalExpenses {
return expenses.fold<double>(
0,
(sum, expense) => sum + expense.amount,
);
}
- 构造函数重载和 where 在其中的使用 将两个知识点结合起来。
class ExpenseBucket {
// 定义一个构造函数,接收分类和费用列表
const ExpenseBucket({
required this.category,
required this.expenses,
});
// 构造函数重载,根据分类从所有费用中筛选出当前分类的费用
ExpenseBucket.forCategory(this.category, List<Expense> allExpenses)
: expenses = allExpenses.where((expense) => expense.category == category).toList();
final Category category; // 分类
final List<Expense> expenses; // 费用列表
// 计算总费用
double get totalExpenses {
return expenses.fold<double>(
0,
(sum, expense) => sum + expense.amount,
);
}
}
- for in 循环代替 map 使用数组的 map 渲染列表的时候在最后要调一下 toList 方法,但是如果使用 for in 循环就不用,但是 for in 循环外层需要一对中括号包裹:
children: [
for(final bucket in buckets) ChartBar(fill: bucket.totalExpenses / maxTotalExpense,)
]
在 Dart 语言中,当 for 循环是列表构造函数(如 [] 或 {})或集合构造函数(如 Set() 或 Map())参数列表中的单一语句时,可以省略花括号。这是 Dart 提供的一种语法糖,旨在使代码更加简洁。
- 通过媒体查询获取当前是暗黑还是亮色主题 如下所示,依据就是通过比较当前颜色种子的亮度完成的。
final isDarkMode = MediaQuery.of(context).platformBrightness == Brightness.dark;
然后通过此布尔值可以进行条件渲染或者进行更加精细的设置:
color: isDarkMode ? Theme.of(context).colorScheme.secondary : Theme.of(context).colorScheme.rpimary.withOpacity(0.7),
或者,
child: FractionallySizedBox(
heightFactor: fill, // 0 <= fill <= 1
child: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.rectangle,
borderRadius: const BorderRadius.vertical(top: Radius.circular(8)),
color: isDarkMode
? Theme.of(context).colorScheme.secondary
: Theme.of(context).colorScheme.primary.withOpacity(0.65),
), // BoxDecoration
), // DecoratedBox
), // FractionallySizedBox
- FractionallySizedBox 通过百分比设置尺寸的组件
- DecoratedBox 可以设置边框,圆角,背景色的组件