Flutter - 状态管理
Flutter - 状态管理
Section titled “Flutter - 状态管理”管理状态——决定用户界面(UI)显示什么以及如何行为的数据——是应用程序开发的一个基本方面。随着应用程序的增长,高效地处理状态对于可维护性和性能至关重要。
考虑一个购物应用程序:
- 用户登录状态需要在多个屏幕上访问。
- 用户在产品页面和购物车屏幕之间切换时,购物车的内容需要保持不变。
- 产品的评分可能只在其详情页相关。
- “添加到购物车”动画的状态是临时的。
Flutter 根据状态的需求范围对其进行分类:
- Ephemeral State (临时状态/局部状态): 状态仅限于单个 widget 或 widget 树的一小部分。示例包括
PageView中的当前页面、动画的进度或TabBar中选定的选项卡。Flutter 内置的StatefulWidget及其关联的State对象(使用setState())非常适合管理临时状态。 - App State (应用状态/共享状态): 多个、可能相距较远的 widget 树部分所需的状态。示例包括用户偏好设置、登录信息、通知或购物车。仅仅使用
StatefulWidget通过向下传递回调和数据 (‘prop drilling’ / 属性逐级传递) 来管理应用状态会迅速变得复杂且低效。
管理应用状态:超越 setState
Section titled “管理应用状态:超越 setState”虽然 StatefulWidget 是必不可少的,但 Flutter 鼓励使用专门的状态管理方法来处理应用状态。这些解决方案提供了集中、高效且可测试的方式来管理需要共享的数据。
流行的选项包括(但不限于):
- Provider: (推荐的入门方案) 构建在
InheritedWidget之上,provider简化了依赖注入(dependency injection)和状态管理。它允许暴露状态对象(如ChangeNotifier或任何其他数据),并在特定状态发生变化时,只高效地重建依赖于该状态的 widgets。 - Riverpod: 通常被认为是 Provider 的继任者,Riverpod 解决了 Provider 的一些限制,提供了编译时安全的状态管理、改进的可测试性,并避免了对
BuildContext的依赖来访问状态。 - Bloc/Cubit: 实现了业务逻辑组件(Business Logic Component)模式,促进了 UI、状态和业务逻辑之间的关注点分离。非常适合状态转换复杂的应用程序。
- GetX: 一个全面的框架,提供了状态管理、依赖注入、路由管理(route management)以及其他具有简洁语法的实用工具。
- MobX: 使用响应式编程(reactive programming)原理,通过 observables 和 reactions 自动跟踪和更新状态变化。
原始教程使用了 scoped_model,该 package 现在已经过时且通常不建议用于新项目。Provider 提供了一个类似的概念简洁性,但具有更好的性能和功能。本章将简要演示涉及应用状态时使用 Provider 的概念。
选择合适的状态管理解决方案取决于您的应用程序的复杂性和团队偏好。从简单的开始 (Provider/Riverpod),并根据需要采用更结构化的方法 (Bloc)。请参阅 Flutter 官方文档中关于状态管理的详细概述:https://docs.flutter.dev/data-and-backend/state-mgmt/options
导航定义了用户如何在应用程序中的不同屏幕(或称为 ‘路由’)之间移动。Flutter 提供了一个强大的路由系统。
命令式导航 (Navigator 1.0)
Section titled “命令式导航 (Navigator 1.0)”这是处理导航的传统方式,通常也更简单,适用于许多应用程序。
MaterialPageRoute: 一种常见的Route实现,使用平台自适应的动画进行屏幕切换。它接受一个builder函数,该函数负责创建新屏幕的 widget。Navigator.push: 将一个新路由压入导航栈(navigation stack)。用户看到新屏幕。Navigator.pop: 从导航栈中弹出当前路由,显示前一个屏幕。可以可选地向前一个路由返回一个结果。
// Navigate to a new screen (ProductDetailPage)Navigator.push( context, MaterialPageRoute( builder: (context) => ProductDetailPage(product: selectedProduct), ),);
// Go back to the previous screenNavigator.pop(context);
// Go back and return a resultNavigator.pop(context, 'Data from detail page');命名路由 (Named Routes)
Section titled “命名路由 (Named Routes)”为了更好地组织结构,尤其是在较大的应用程序中,您可以在 MaterialApp 中定义命名路由,并使用 Navigator.pushNamed 进行导航。
// In MaterialApp:MaterialApp( initialRoute: '/', routes: { '/': (context) => HomeScreen(), '/productDetail': (context) => ProductDetailPage(), // Need to handle arguments separately // ... other routes },);
// Navigate using the name:Navigator.pushNamed(context, '/productDetail', arguments: selectedProduct);声明式导航 (Navigator 2.0 / Router API)
Section titled “声明式导航 (Navigator 2.0 / Router API)”Navigator 2.0 是后来引入的,提供了一种更声明式和强大的 API (Router, Page, RouteInformationParser, RouterDelegate)。它提供了对导航栈更精细的控制,使得处理复杂场景(如深度链接 deep linking、嵌套路由 nested routing 和浏览器历史管理 browser history management,尤其适用于 web 应用程序)更加容易。然而,它的学习曲线比 Navigator 1.0 更陡峭。像 go_router 这样的 package 简化了 Navigator 2.0 的使用。
示例:简单的产品导航 (使用 Navigator 1.0)
Section titled “示例:简单的产品导航 (使用 Navigator 1.0)”让我们使用基本导航和 StatefulWidget 来管理局部状态(如评分)来构建产品列表/详情示例。
- 创建项目:
flutter create product_nav_app - 添加资源 (Assets): 创建
assets/images/文件夹,添加产品图片(例如,iphone.png,pixel.png),并在pubspec.yaml中声明该文件夹:
flutter: uses-material-design: true assets: - assets/images/- 定义
Product模型 (lib/product.dart):
class Product { final String name; final String description; final int price; final String image;
const Product({required this.name, required this.description, required this.price, required this.image});
// Sample data generation static List<Product> getProducts() { return [ const Product(name: "Pixel", description: "Pixel is the most feature-full phone ever", price: 800, image: "pixel.png"), const Product(name: "Laptop", description: "Laptop is most productive development tool", price: 2000, image: "laptop.png"), const Product(name: "Tablet", description: "Tablet is the most useful device ever for meeting", price: 1500, image: "tablet.png"), const Product(name: "Pendrive", description: "Pendrive is useful storage medium", price: 100, image: "pendrive.png"), const Product(name: "Floppy Drive", description: "Floppy drive is useful rescue storage medium", price: 20, image: "floppy.png"), const Product(name: "iPhone", description: "iPhone is the stylish phone ever", price: 1000, image: "iphone.png"), ]; }}- 创建
RatingBoxWidget (lib/rating_box.dart): (管理其自身的临时状态 ephemeral state)
import 'package:flutter/material.dart';
class RatingBox extends StatefulWidget { const RatingBox({Key? key}) : super(key: key);
@override _RatingBoxState createState() => _RatingBoxState();}
class _RatingBoxState extends State<RatingBox> { int _rating = 0;
void _setRating(int newRating) { setState(() { _rating = newRating; }); }
@override Widget build(BuildContext context) { const double size = 20; return Row( mainAxisAlignment: MainAxisAlignment.end, children: <Widget>[ IconButton( icon: Icon(_rating >= 1 ? Icons.star : Icons.star_border, size: size), color: Colors.red[500], onPressed: () => _setRating(1), padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), IconButton( icon: Icon(_rating >= 2 ? Icons.star : Icons.star_border, size: size), color: Colors.red[500], onPressed: () => _setRating(2), padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), IconButton( icon: Icon(_rating >= 3 ? Icons.star : Icons.star_border, size: size), color: Colors.red[500], onPressed: () => _setRating(3), padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), ], ); }}- 创建
ProductBoxWidget (lib/product_box.dart): (显示单个产品)
import 'package:flutter/material.dart';import 'product.dart';import 'rating_box.dart';
class ProductBox extends StatelessWidget { final Product item;
const ProductBox({Key? key, required this.item}) : super(key: key);
@override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.all(2), height: 140, child: Card( child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: <Widget>[ Image.asset("assets/images/" + item.image, width: 100), // 添加宽度以保持一致性 Expanded( child: Container( padding: const EdgeInsets.all(5), child: Column( mainAxisAlignment: MainAxisAlignment.spaceEvenly, crossAxisAlignment: CrossAxisAlignment.start, // 文本左对齐 children: <Widget>[ Text(item.name, style: const TextStyle(fontWeight: FontWeight.bold)), Text(item.description), Text("Price: ${item.price.toString()}"), const RatingBox(), ], ), ), ) ], ), ), ); }}- 创建
ProductPageWidget (lib/product_page.dart): (详情屏幕)
import 'package:flutter/material.dart';import 'product.dart';import 'rating_box.dart';
class ProductPage extends StatelessWidget { final Product item;
const ProductPage({Key? key, required this.item}) : super(key: key);
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(item.name), ), body: Center( child: Container( padding: const EdgeInsets.all(16), // 添加内边距 child: Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center, // 内容居中 children: <Widget>[ Image.asset("assets/images/" + item.image, height: 200), // 调整大小 const SizedBox(height: 20), // 添加间距 Text(item.name, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 24)), const SizedBox(height: 10), Text(item.description, textAlign: TextAlign.center), const SizedBox(height: 10), Text("Price: ${item.price.toString()}", style: const TextStyle(fontSize: 20)), const SizedBox(height: 10), const RatingBox(), const Spacer(), // 将按钮推到底部 ElevatedButton( // 现代风格按钮 onPressed: () { Navigator.pop(context); }, child: const Text('Go Back'), ) ], ), ), ), ); }}- 更新
main.dart: (主应用结构和首页列表)
import 'package:flutter/material.dart';import 'product.dart';import 'product_box.dart';import 'product_page.dart';// 假设 rating_box.dart 也已创建
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget { const MyApp({Key? key}) : super(key: key);
@override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Navigation Demo', theme: ThemeData(primarySwatch: Colors.blue), home: const MyHomePage(title: 'Product List'), ); }}
class MyHomePage extends StatelessWidget { final String title; final List<Product> items = Product.getProducts();
const MyHomePage({Key? key, required this.title}) : super(key: key);
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(title)), body: ListView.builder( itemCount: items.length, itemBuilder: (context, index) { return GestureDetector( child: ProductBox(item: items[index]), onTap: () { // 点击时导航到 ProductPage Navigator.push( context, MaterialPageRoute( builder: (context) => ProductPage(item: items[index]), ), ); }, ); }, ), ); }}运行应用程序。您将看到一个产品列表。点击一个产品会导航到其详情页。两个页面上的评分星级使用 StatefulWidget (RatingBox) 独立维护各自的局部状态(local state)。应用状态(如一个全局购物车)则需要像 Provider 这样的解决方案来管理。