Flutter 小白入门教程Flutter 小白入门教程
首页
学习指南
项目实战
Flutter 官网
编程指南
首页
学习指南
项目实战
Flutter 官网
编程指南
  • 入门基础

    • 📚 基础教程
    • 第1章 - 认识 Flutter
    • 第2章 - 环境搭建
    • 第3章 - Dart 语言基础
    • 第4章 - 第一个 Flutter 应用
    • 第5章 - Widget 基础
    • 第6章 - 布局系统
    • 第7章 - 状态管理入门
    • 第8章 - 页面导航
    • 第9章 - 资源管理
  • 进阶开发

    • 第10章 - 网络请求
    • 第11章 - 本地存储
    • 第12章 - 对话框与反馈
    • 第13章 - 列表进阶
    • 第14章 - 主题定制
    • 第15章 - 状态管理进阶
    • 第16章 - 动画入门
    • 第17章 - 常用第三方包
  • 调试与发布

    • 第18章 - 调试与性能优化
    • 第19章 - 打包与发布
  • 附录

    • 附录A - UI 框架与组件库推荐
    • 附录B - 项目结构最佳实践
    • 附录C - 国际化配置
    • 附录D - 权限处理

第15章 - 状态管理进阶

嗨,朋友!我是长安。

在第7章,我们学习了用 setState 管理状态。但当应用变得复杂时,setState 就不够用了——状态需要在多个页面之间共享,业务逻辑和 UI 混在一起难以维护。

这一章,我们来学习更强大的状态管理方案:Provider 和 GetX。

🤔 为什么需要状态管理?

setState 的局限性

// 问题1:状态只能向下传递,不能向上或平级传递
class ParentPage extends StatefulWidget {
  @override
  State<ParentPage> createState() => _ParentPageState();
}

class _ParentPageState extends State<ParentPage> {
  int count = 0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('Count: $count'),
        ChildA(count: count),  // 可以向下传
        ChildB(count: count),  // 可以向下传
        // 但如果 ChildA 想修改 count,怎么办?
        // 需要把回调函数也传下去,层级多了就很麻烦
      ],
    );
  }
}

// 问题2:深层嵌套传递很痛苦
// 如果 count 需要传到第5层的组件,中间每一层都要写参数

状态管理解决的问题

  1. 跨组件共享状态 - 任何组件都能访问共享状态
  2. 状态和 UI 分离 - 业务逻辑独立,便于测试和维护
  3. 避免不必要的重建 - 只有依赖状态的组件才会重建

📦 Provider(官方推荐)

Provider 是 Flutter 官方推荐的状态管理方案,简单易学,功能强大。

1. 添加依赖

dependencies:
  provider: ^6.1.1

2. 基础用法:ChangeNotifierProvider

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

// 第1步:创建状态类(继承 ChangeNotifier)
class CounterProvider extends ChangeNotifier {
  int _count = 0;

  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();  // 通知监听者更新
  }

  void decrement() {
    _count--;
    notifyListeners();
  }

  void reset() {
    _count = 0;
    notifyListeners();
  }
}

// 第2步:在顶层提供状态
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => CounterProvider(),
      child: const MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: const CounterPage(),
    );
  }
}

// 第3步:在任意子组件中使用
class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Provider 示例')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('当前计数:'),
            // 方式1:使用 Consumer
            Consumer<CounterProvider>(
              builder: (context, counter, child) {
                return Text(
                  '${counter.count}',
                  style: const TextStyle(fontSize: 48),
                );
              },
            ),
            const SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  // 方式2:使用 context.read(不监听变化,只调用方法)
                  onPressed: () => context.read<CounterProvider>().decrement(),
                  child: const Text('-'),
                ),
                const SizedBox(width: 20),
                ElevatedButton(
                  onPressed: () => context.read<CounterProvider>().increment(),
                  child: const Text('+'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

3. 读取状态的三种方式

// 方式1:Consumer - 推荐,精确控制重建范围
Consumer<CounterProvider>(
  builder: (context, counter, child) {
    return Text('${counter.count}');
  },
)

// 方式2:context.watch - 监听变化,整个 build 方法会重建
@override
Widget build(BuildContext context) {
  final counter = context.watch<CounterProvider>();
  return Text('${counter.count}');
}

// 方式3:context.read - 不监听变化,只读取一次(用于调用方法)
onPressed: () {
  context.read<CounterProvider>().increment();
}

// 方式4:Provider.of - 传统方式
final counter = Provider.of<CounterProvider>(context);
final counter = Provider.of<CounterProvider>(context, listen: false);

watch vs read

  • context.watch<T>() = 监听变化,值变了会重建
  • context.read<T>() = 只读一次,不监听变化
  • 一般在 build 方法中用 watch,在回调函数中用 read

4. 多 Provider 组合

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => UserProvider()),
        ChangeNotifierProvider(create: (_) => CartProvider()),
        ChangeNotifierProvider(create: (_) => ThemeProvider()),
      ],
      child: const MyApp(),
    ),
  );
}

// 在组件中使用
class HomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final user = context.watch<UserProvider>();
    final cart = context.watch<CartProvider>();
    
    return Scaffold(
      appBar: AppBar(
        title: Text('欢迎,${user.name}'),
        actions: [
          Badge(
            label: Text('${cart.itemCount}'),
            child: IconButton(
              icon: const Icon(Icons.shopping_cart),
              onPressed: () {},
            ),
          ),
        ],
      ),
      body: ...,
    );
  }
}

5. Selector - 精细控制重建

当 Provider 状态很多时,用 Selector 只监听需要的部分:

// UserProvider 有多个属性
class UserProvider extends ChangeNotifier {
  String _name = '';
  int _age = 0;
  String _avatar = '';

  String get name => _name;
  int get age => _age;
  String get avatar => _avatar;

  void updateName(String name) {
    _name = name;
    notifyListeners();
  }

  void updateAge(int age) {
    _age = age;
    notifyListeners();
  }
}

// 只监听 name 变化
Selector<UserProvider, String>(
  selector: (_, provider) => provider.name,
  builder: (_, name, __) {
    return Text('姓名:$name');
  },
)

// 只监听 age 变化
Selector<UserProvider, int>(
  selector: (_, provider) => provider.age,
  builder: (_, age, __) {
    return Text('年龄:$age');
  },
)

// 使用 context.select
final name = context.select<UserProvider, String>((p) => p.name);

6. 实战:用户登录状态管理

// lib/providers/auth_provider.dart
class AuthProvider extends ChangeNotifier {
  User? _user;
  bool _isLoading = false;
  String? _error;

  User? get user => _user;
  bool get isLoggedIn => _user != null;
  bool get isLoading => _isLoading;
  String? get error => _error;

  // 登录
  Future<bool> login(String email, String password) async {
    _isLoading = true;
    _error = null;
    notifyListeners();

    try {
      // 模拟 API 请求
      await Future.delayed(const Duration(seconds: 2));
      
      if (email == 'test@test.com' && password == '123456') {
        _user = User(id: 1, name: '长安', email: email);
        _isLoading = false;
        notifyListeners();
        return true;
      } else {
        _error = '邮箱或密码错误';
        _isLoading = false;
        notifyListeners();
        return false;
      }
    } catch (e) {
      _error = '登录失败:$e';
      _isLoading = false;
      notifyListeners();
      return false;
    }
  }

  // 登出
  void logout() {
    _user = null;
    notifyListeners();
  }
}

class User {
  final int id;
  final String name;
  final String email;

  User({required this.id, required this.name, required this.email});
}

// lib/pages/login_page.dart
class LoginPage extends StatefulWidget {
  const LoginPage({super.key});

  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final _emailController = TextEditingController();
  final _passwordController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    final auth = context.watch<AuthProvider>();

    return Scaffold(
      appBar: AppBar(title: const Text('登录')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextField(
              controller: _emailController,
              decoration: const InputDecoration(
                labelText: '邮箱',
                hintText: 'test@test.com',
              ),
            ),
            const SizedBox(height: 16),
            TextField(
              controller: _passwordController,
              decoration: const InputDecoration(
                labelText: '密码',
                hintText: '123456',
              ),
              obscureText: true,
            ),
            if (auth.error != null) ...[
              const SizedBox(height: 16),
              Text(
                auth.error!,
                style: const TextStyle(color: Colors.red),
              ),
            ],
            const SizedBox(height: 24),
            SizedBox(
              width: double.infinity,
              child: ElevatedButton(
                onPressed: auth.isLoading
                    ? null
                    : () async {
                        final success = await context.read<AuthProvider>().login(
                          _emailController.text,
                          _passwordController.text,
                        );
                        if (success && mounted) {
                          Navigator.pushReplacementNamed(context, '/home');
                        }
                      },
                child: auth.isLoading
                    ? const SizedBox(
                        height: 20,
                        width: 20,
                        child: CircularProgressIndicator(strokeWidth: 2),
                      )
                    : const Text('登录'),
              ),
            ),
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    _emailController.dispose();
    _passwordController.dispose();
    super.dispose();
  }
}

🚀 GetX(功能最全)

GetX 是一个全能包:状态管理 + 路由管理 + 依赖注入 + 国际化,一个包搞定所有。

1. 添加依赖

dependencies:
  get: ^4.6.6

2. 响应式状态管理

import 'package:flutter/material.dart';
import 'package:get/get.dart';

// 第1步:创建 Controller
class CounterController extends GetxController {
  // .obs 让变量变成响应式
  var count = 0.obs;
  
  // 也可以这样定义
  final RxInt count2 = 0.obs;
  final RxString name = '长安'.obs;
  final RxBool isLoading = false.obs;
  final RxList<String> items = <String>[].obs;

  void increment() => count++;
  void decrement() => count--;
  void reset() => count.value = 0;
}

// 第2步:使用 GetMaterialApp
void main() {
  runApp(GetMaterialApp(
    home: const CounterPage(),
  ));
}

// 第3步:在组件中使用
class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    // 注入 Controller(Get.put 会自动管理生命周期)
    final controller = Get.put(CounterController());

    return Scaffold(
      appBar: AppBar(title: const Text('GetX 示例')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('当前计数:'),
            // Obx 自动监听响应式变量
            Obx(() => Text(
              '${controller.count}',
              style: const TextStyle(fontSize: 48),
            )),
            const SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: controller.decrement,
                  child: const Text('-'),
                ),
                const SizedBox(width: 20),
                ElevatedButton(
                  onPressed: controller.increment,
                  child: const Text('+'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

3. GetBuilder(非响应式)

如果不想用 .obs,可以用 GetBuilder:

class CounterController extends GetxController {
  int count = 0;

  void increment() {
    count++;
    update();  // 手动通知更新
  }
}

// 使用 GetBuilder
GetBuilder<CounterController>(
  init: CounterController(),
  builder: (controller) {
    return Text('${controller.count}');
  },
)

4. 依赖注入

// 注册依赖
Get.put(CounterController());                    // 立即创建
Get.lazyPut(() => CounterController());          // 延迟创建
Get.putAsync(() async => await ApiService());    // 异步创建

// 获取依赖
final controller = Get.find<CounterController>();

// 删除依赖
Get.delete<CounterController>();

// 使用 Bindings 集中管理依赖
class HomeBinding extends Bindings {
  @override
  void dependencies() {
    Get.lazyPut(() => HomeController());
    Get.lazyPut(() => UserController());
  }
}

// 在路由中使用 Binding
GetPage(
  name: '/home',
  page: () => const HomePage(),
  binding: HomeBinding(),
)

5. 路由管理

// 基本导航(无需 context!)
Get.to(() => const DetailPage());              // 跳转
Get.toNamed('/detail');                        // 命名路由跳转
Get.back();                                    // 返回
Get.off(() => const LoginPage());              // 跳转并移除当前页
Get.offAll(() => const HomePage());            // 清空所有路由
Get.offNamed('/home');                         // 命名路由替换

// 传参
Get.to(() => const DetailPage(), arguments: {'id': 123});
// 接收参数
final args = Get.arguments;

// 命名路由传参
Get.toNamed('/detail', arguments: {'id': 123});
Get.toNamed('/detail?id=123');
// 接收参数
final id = Get.parameters['id'];

// 返回并传值
Get.back(result: 'success');
// 接收返回值
final result = await Get.to(() => const EditPage());

// 定义路由
GetMaterialApp(
  initialRoute: '/',
  getPages: [
    GetPage(name: '/', page: () => const HomePage()),
    GetPage(name: '/detail', page: () => const DetailPage()),
    GetPage(
      name: '/login',
      page: () => const LoginPage(),
      transition: Transition.fadeIn,
      transitionDuration: const Duration(milliseconds: 300),
    ),
  ],
)

6. 便捷工具

// SnackBar(无需 context!)
Get.snackbar(
  '标题',
  '内容',
  snackPosition: SnackPosition.BOTTOM,
  duration: const Duration(seconds: 3),
);

// Dialog
Get.defaultDialog(
  title: '提示',
  middleText: '确定要删除吗?',
  textConfirm: '确定',
  textCancel: '取消',
  onConfirm: () {
    Get.back();
    // 执行删除
  },
);

// BottomSheet
Get.bottomSheet(
  Container(
    color: Colors.white,
    padding: const EdgeInsets.all(16),
    child: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        ListTile(
          leading: const Icon(Icons.camera),
          title: const Text('拍照'),
          onTap: () => Get.back(),
        ),
        ListTile(
          leading: const Icon(Icons.photo),
          title: const Text('相册'),
          onTap: () => Get.back(),
        ),
      ],
    ),
  ),
);

// 主题切换
Get.changeTheme(ThemeData.dark());
Get.isDarkMode;  // 是否深色模式

// 语言切换
Get.updateLocale(const Locale('zh', 'CN'));

7. 实战:完整的 GetX 应用结构

// lib/main.dart
import 'package:flutter/material.dart';
import 'package:get/get.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'GetX Demo',
      theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
      initialRoute: '/',
      getPages: [
        GetPage(
          name: '/',
          page: () => const HomePage(),
          binding: HomeBinding(),
        ),
        GetPage(
          name: '/detail/:id',
          page: () => const DetailPage(),
        ),
      ],
    );
  }
}

// lib/bindings/home_binding.dart
class HomeBinding extends Bindings {
  @override
  void dependencies() {
    Get.lazyPut(() => HomeController());
  }
}

// lib/controllers/home_controller.dart
class HomeController extends GetxController {
  final items = <String>[].obs;
  final isLoading = false.obs;

  @override
  void onInit() {
    super.onInit();
    loadData();
  }

  Future<void> loadData() async {
    isLoading.value = true;
    await Future.delayed(const Duration(seconds: 1));
    items.value = ['Item 1', 'Item 2', 'Item 3'];
    isLoading.value = false;
  }

  void addItem(String item) {
    items.add(item);
  }

  void removeItem(int index) {
    items.removeAt(index);
  }
}

// lib/pages/home_page.dart
class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    final controller = Get.find<HomeController>();

    return Scaffold(
      appBar: AppBar(
        title: const Text('首页'),
        actions: [
          IconButton(
            icon: const Icon(Icons.add),
            onPressed: () {
              Get.defaultDialog(
                title: '添加',
                content: TextField(
                  onSubmitted: (value) {
                    controller.addItem(value);
                    Get.back();
                  },
                ),
              );
            },
          ),
        ],
      ),
      body: Obx(() {
        if (controller.isLoading.value) {
          return const Center(child: CircularProgressIndicator());
        }
        return ListView.builder(
          itemCount: controller.items.length,
          itemBuilder: (context, index) {
            return ListTile(
              title: Text(controller.items[index]),
              trailing: IconButton(
                icon: const Icon(Icons.delete),
                onPressed: () => controller.removeItem(index),
              ),
              onTap: () => Get.toNamed('/detail/$index'),
            );
          },
        );
      }),
    );
  }
}

// lib/pages/detail_page.dart
class DetailPage extends StatelessWidget {
  const DetailPage({super.key});

  @override
  Widget build(BuildContext context) {
    final id = Get.parameters['id'];

    return Scaffold(
      appBar: AppBar(title: Text('详情 $id')),
      body: Center(
        child: Text('这是第 $id 项的详情'),
      ),
    );
  }
}

⚖️ Provider vs GetX 对比

特性ProviderGetX
学习曲线简单简单
官方推荐✅ 是❌ 否
状态管理✅✅
路由管理❌ 需要额外包✅ 内置
依赖注入❌ 需要额外包✅ 内置
国际化❌ 需要额外包✅ 内置
代码量较多较少
测试友好✅ 更好一般
社区支持很好很好

选择建议

  • 选 Provider:

    • 追求官方标准
    • 项目较大,需要更好的可测试性
    • 团队熟悉 Provider
  • 选 GetX:

    • 追求开发速度
    • 想要一个包搞定所有
    • 个人项目或小团队

📝 小结

这一章我们学习了两种主流的状态管理方案:

Provider

  • ChangeNotifierProvider - 提供状态
  • Consumer / context.watch - 监听状态
  • context.read - 读取状态(不监听)
  • Selector - 精细控制重建

GetX

  • .obs / Obx - 响应式状态
  • Get.put / Get.find - 依赖注入
  • Get.to / Get.toNamed - 路由导航
  • Get.snackbar / Get.dialog - 便捷工具

状态管理最佳实践

  1. 分离关注点 - 把业务逻辑放在 Provider/Controller 中
  2. 最小化重建 - 用 Consumer/Selector/Obx 精确控制
  3. 合理划分状态 - 按功能模块划分多个 Provider/Controller
  4. 避免过度设计 - 简单场景用 setState 就够了

💪 练习题

  1. 用 Provider 实现一个购物车功能(添加、删除、计算总价)
  2. 用 GetX 实现一个用户登录/登出功能
  3. 对比两种方案的代码量和开发体验

🚀 下一步

掌握了状态管理后,下一章我们来学习 动画入门,让你的应用动起来!


由 编程指南 提供

最近更新: 2026/2/3 16:24
Contributors: 王长安
Prev
第14章 - 主题定制
Next
第16章 - 动画入门