被面试官问Flutter状态管理后我连夜整理的实战笔记

马到成功
2026-07-27 01:52
阅读 594

上周五下午,我翘了一节水课跑去春熙路那边一家做跨境电商的公司面试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);
      },
    );
  },
)

第二,watchread没分清。 在不需要响应变化的地方(比如按钮的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

最热最新
暂无评论
马到成功Lv.1
0
影响力
0
文章
0
粉丝