被面试官问Flutter状态管理后我连夜整理的实战笔记
上周五下午,我翘了一节水课跑去春熙路那边一家做跨境电商的公司面试Flutter开发岗位。说实话,我简历上写的那些项目,大部分都是在宿舍里边吃外卖边敲出来的,能接到面试电话我都觉得挺意外的。
面试官是个看起来三十出头的大哥,前面问了些Dart基础、Widget生命周期之类的,我都答得还行。结果他冷不丁来了一句:"你们项目里状态管理用的什么方案?能说说为什么选它吗?如果让你重新设计,你会怎么做?"
我当时脑子一片空白,支支吾吾说了句"用的Provider",然后就开始扯一些官方文档上背下来的话。大哥笑了笑,又追问了几个关于跨组件通信、性能优化的问题,我基本就答不上来了。
从公司出来的时候,成都的晚高峰堵得一塌糊涂,我骑着共享单车在人群里钻,心里特别不是滋味。回到宿舍,打开VSCode,看着那堆花里胡哨的插件图标,我暗下决心:这个知识点,我必须给它啃下来。
先说说我踩过的坑
其实我最早写Flutter的时候,根本不知道什么状态管理。当时做了一个课程表的小Demo,所有状态全靠setState硬怼。页面简单的时候还好,后来加了个"课程详情"页面,需要从列表页传数据过去,详情页里还要改状态再同步回列表页,我整个人就懵了。
那段时间我的代码基本长这样:
// 别骂了别骂了,我知道这很烂
class CourseListPage extends StatefulWidget {
@override
_CourseListPageState createState() => _CourseListPageState();
}
class _CourseListPageState extends State<CourseListPage> {
List<Course> courses = [];
bool isLoading = true;
String errorMessage = '';
int selectedIndex = -1;
TextEditingController searchController = TextEditingController();
String searchKeyword = '';
// ... 还有十几个变量,我都不好意思贴出来
void _updateCourse(int index, Course newCourse) {
setState(() {
courses[index] = newCourse;
});
// 这里还要通知详情页更新,怎么通知?
// 当时我的做法是:用全局静态变量...
}
}
后来我室友(他学的Java,天天在那吹Spring Boot多牛)看不下去了,跟我说你去看看状态管理的东西。我一搜,好家伙,Provider、Riverpod、Bloc、GetX、Redux...选择比努力多,我直接选择困难症犯了。
我最终选了啥
纠结了大概一周(主要是白天上课,只能晚上在宿舍研究),我最终决定深入学Provider和Bloc这两个。原因很实际:Provider是Flutter官方推荐的,文档多,出问题了好搜;Bloc在企业级项目里用得很多,面试也爱问。
这里我先说个结论,也是我被面试官问住之后,花了好几天才想明白的事:
| 方案 | 适合场景 | 学习成本 | 代码量 | 可测试性 |
|---|---|---|---|---|
| setState | 单页面局部状态 | 极低 | 极少 | 差 |
| Provider | 中小型项目、跨组件共享 | 低 | 少 | 中 |
| Bloc | 大型项目、复杂业务逻辑 | 中高 | 多 | 极好 |
| Riverpod | 中大型项目、编译时安全 | 中 | 中 | 好 |
| GetX | 快速开发、小团队 | 低 | 极少 | 差 |
别问我为什么没选GetX,虽然它确实写起来很爽,但我后来在一个技术群里看到好几个大佬吐槽GetX的"魔法"太多,出了问题debug能让人崩溃。我这种水平,还是老老实实写规范点好。
Provider实战:我的课程表项目重构
面试回来那天晚上,我把之前那个课程表项目翻出来,用Provider重构了一遍。说实话,重构的过程挺痛苦的,因为要改的地方太多了,好几次我都想放弃,心想"算了反正也没人看这破项目"。但一想到面试官那个意味深长的笑容,我又咬牙继续了。
核心思路其实很简单:把业务逻辑从UI里抽出来,放到一个独立的类里,然后通过Provider把这个类"注入"到Widget树中。
先定义一个ChangeNotifier:
import 'package:flutter/foundation.dart';
class Course {
final String id;
final String name;
final String teacher;
final String time;
bool isCompleted;
Course({
required this.id,
required this.name,
required this.teacher,
required this.time,
this.isCompleted = false,
});
}
class CourseProvider extends ChangeNotifier {
List<Course> _courses = [];
bool _isLoading = false;
String? _errorMessage;
String _searchKeyword = '';
// getter 暴露给UI使用
List<Course> get courses {
if (_searchKeyword.isEmpty) return _courses;
return _courses.where((c) =>
c.name.contains(_searchKeyword) ||
c.teacher.contains(_searchKeyword)
).toList();
}
bool get isLoading => _isLoading;
String? get errorMessage => _errorMessage;
// 业务逻辑方法
Future<void> loadCourses() async {
_isLoading = true;
_errorMessage = null;
notifyListeners(); // 通知UI更新
try {
// 模拟网络请求
await Future.delayed(const Duration(seconds: 1));
_courses = [
Course(id: '1', name: '高等数学', teacher: '张教授', time: '周一 1-2节'),
Course(id: '2', name: '大学英语', teacher: '李老师', time: '周二 3-4节'),
Course(id: '3', name: '数据结构', teacher: '王教授', time: '周三 5-6节'),
];
} catch (e) {
_errorMessage = '加载失败:$e';
} finally {
_isLoading = false;
notifyListeners();
}
}
void toggleComplete(String courseId) {
final index = _courses.indexWhere((c) => c.id == courseId);
if (index != -1) {
_courses[index].isCompleted = !_courses[index].isCompleted;
notifyListeners();
}
}
void setSearchKeyword(String keyword) {
_searchKeyword = keyword;
notifyListeners();
}
}
然后在main.dart里注入:
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => CourseProvider()),
// 以后加其他Provider也很方便
],
child: const MyApp(),
),
);
}
UI层就变得很干净了:
class CourseListPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('我的课程')),
body: Column(
children: [
TextField(
decoration: const InputDecoration(hintText: '搜索课程或老师'),
onChanged: (value) {
// 注意这里,不需要setState了
context.read<CourseProvider>().setSearchKeyword(value);
},
),
// 只监听需要的部分,避免不必要的重建
Consumer<CourseProvider>(
builder: (context, provider, child) {
if (provider.isLoading) {
return const Center(child: CircularProgressIndicator());
}
if (provider.errorMessage != null) {
return Center(child: Text(provider.errorMessage!));
}
return Expanded(
child: ListView.builder(
itemCount: provider.courses.length,
itemBuilder: (context, index) {
final course = provider.courses[index];
return ListTile(
title: Text(course.name),
subtitle: Text('${course.teacher} | ${course.time}'),
trailing: Checkbox(
value: course.isCompleted,
onChanged: (_) {
provider.toggleComplete(course.id);
},
),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => CourseDetailPage(courseId: course.id),
),
);
},
);
},
),
);
},
),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.read<CourseProvider>().loadCourses(),
child: const Icon(Icons.refresh),
),
);
}
}
详情页要读取和修改同一份数据?太简单了:
class CourseDetailPage extends StatelessWidget {
final String courseId;
const CourseDetailPage({required this.courseId});
@override
Widget build(BuildContext context) {
// 直接读取,自动响应变化
final course = context.watch<CourseProvider>()
.courses.firstWhere((c) => c.id == courseId);
return Scaffold(
appBar: AppBar(title: Text(course.name)),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('老师:${course.teacher}'),
Text('时间:${course.time}'),
Text('状态:${course.isCompleted ? "已完成" : "未完成"}'),
ElevatedButton(
onPressed: () {
context.read<CourseProvider>().toggleComplete(courseId);
},
child: const Text('切换完成状态'),
),
],
),
),
);
}
}
写完这段代码的时候,已经是凌晨两点半了。我靠在椅子上,看着窗外成都的夜景,远处九眼桥的灯光还亮着。虽然很累,但那种代码终于变整洁了的感觉,真的很爽。
关于性能优化,我学到的几个教训
重构完之后,我兴冲冲地跑起来测试,结果发现列表滚动的时候有点卡顿。我当时就慌了,心想不会吧,就这么点数据也卡?
后来我仔细排查了一下,发现几个问题:
第一,Consumer的粒度太粗了。 我把整个列表都包在一个Consumer里,任何一个状态变化都会导致整个列表重建。解决方案是用Selector来精确监听:
// 之前:整个列表都重建
Consumer<CourseProvider>(
builder: (context, provider, child) { ... }
)
// 之后:只监听courses的变化
Selector<CourseProvider, List<Course>>(
selector: (context, provider) => provider.courses,
builder: (context, courses, child) {
return ListView.builder(
itemCount: courses.length,
itemBuilder: (context, index) {
// 每个item单独包一个,避免一个变了全部重建
return CourseItem(courseId: courses[index].id);
},
);
},
)
第二,watch和read没分清。 在不需要响应变化的地方(比如按钮的onPressed回调里),用watch会导致不必要的重建。记住一个原则:只在build方法里需要响应数据变化时才用watch,其他时候用read。
第三,ChangeNotifier里的notifyListeners()调用太频繁。 有一次我在一个循环里连续调了好几次notifyListeners(),结果UI疯狂重建。后来学聪明了,批量操作的时候只在最后调一次。
Bloc我也研究了一下
虽然Provider够用了,但为了面试(没错,我就是这么功利),我还是把Bloc也学了一遍。
说实话,Bloc的代码量是真的多。一个简单的计数器功能,用Provider可能就二三十行,用Bloc得写Event、State、Bloc三个类,外加一堆模板代码。
但是!当我项目变复杂之后,我开始理解Bloc的好了。它强制你把UI和业务逻辑完全分离,每个状态变化都有明确的Event触发,调试的时候能清楚地看到状态流转的过程。而且Bloc的测试写起来特别方便,因为逻辑都在Bloc里,不依赖任何UI组件。
这里贴一段我用Bloc重写的课程加载逻辑,大家感受下:
// event
abstract class CourseEvent {}
class LoadCourses extends CourseEvent {}
class SearchCourses extends CourseEvent {
final String keyword;
SearchCourses(this.keyword);
}
// state
abstract class CourseState {}
class CourseInitial extends CourseState {}
class CourseLoading extends CourseState {}
class CourseLoaded extends CourseState {
final List<Course> courses;
CourseLoaded(this.courses);
}
class CourseError extends CourseState {
final String message;
CourseError(this.message);
}
// bloc
class CourseBloc extends Bloc<CourseEvent, CourseState> {
final CourseRepository repository;
CourseBloc(this.repository) : super(CourseInitial()) {
on<LoadCourses>(_onLoadCourses);
on<SearchCourses>(_onSearchCourses);
}
Future<void> _onLoadCourses(
LoadCourses event,
Emitter<CourseState> emit,
) async {
emit(CourseLoading());
try {
final courses = await repository.getCourses();
emit(CourseLoaded(courses));
} catch (e) {
emit(CourseError(e.toString()));
}
}
// ...
}
代码量确实多了一倍不止,但结构清晰,每个状态都是独立的类,不会出现"这个字段到底是null还是空字符串"这种问题。
一些掏心窝子的建议
写到这里,差不多把我这半个月的学习心得都倒出来了。最后给跟我一样的兄弟几点建议:
别纠结选哪个方案。 如果你跟我一样是初学者,先用Provider入门,它足够应对大部分场景。等你项目复杂到Provider hold不住了,或者公司技术栈要求,再切Bloc也不迟。网上那些"XXX方案秒杀XXX"的文章,看看就好,别太当真。
一定要动手写。 我看了无数篇状态管理的文章,收藏了一堆,结果真正理解是在自己重构项目的时候。看十遍不如写一遍,这话虽然老套但真的是真理。
善用AI工具但别依赖。 说实话,我学Bloc的时候,很多模板代码都是让ChatGPT帮我生成的,省了不少时间。但生成之后我一定会自己过一遍,理解每一行在干什么。有一次AI给我生成的代码里有个隐蔽的bug,我直接复制粘贴跑起来了,结果找了半天才发现问题。工具是好工具,但脑子不能偷懒。
另外最近发现了一个叫CodeBuddy的工具,在VSCode里装个插件就能用,写代码的时候能实时给建议,比单纯问ChatGPT方便不少。有一次我写Bloc的Event处理逻辑,它直接帮我补全了整个try-catch的结构,还挺智能的。不过我发现它对Flutter的理解有时候不太准,给的建议需要自己判断一下。
面试真的能倒逼学习。 如果不是那次面试被问住了,我可能还会在setState的泥潭里挣扎很久。建议大家有机会多去面面试,就算面不上,也能知道自己哪里不足。
最后
现在我的课程表项目已经用Provider重构完了,代码比之前清爽了不止一个档次。虽然跟那些大佬的项目比还是差得远,但至少我自己看着舒服了。
下一步我打算把这个项目加上本地持久化(用sqflite),然后再学学路由管理和网络请求的封装。路还很长,慢慢来吧。
对了,如果你也是双非的同学,别太焦虑。我们学校虽然名字不好听,但只要自己肯学,机会还是有的。上周那个面试虽然没通过,但HR说我的基础还不错,让我继续加油,秋招的时候可以再试试。
成都这边生活节奏慢,适合沉下心来学东西。写完这篇文章,我准备去楼下吃碗冒菜,犒劳一下自己。
共勉。


评论 0