自然生长:状态管理的演进

179 阅读6分钟

自然生长: Flutter状态管理的演进

在应用开发中,状态管理是一个核心议题。随着应用规模的扩大和业务逻辑的复杂化,我们对状态管理方案的需求也在不断演进。这种演进并非一蹴而就,更像是一种“自然生长”的过程:从简单场景下的直接处理,到引入专门的工具,再到采纳成熟的架构模式,每一步都是为了应对新的挑战,提升代码的可维护性、可测试性和可扩展性。

本文将回顾 Flutter 状态管理方案的典型演进路径,探讨不同阶段的特点、适用场景以及它们如何为下一阶段的“生长”奠定基础。

1. 萌芽期:StatelessWidget 内嵌网络请求与 FutureBuilder

场景描述: 当某项数据是只读的,并且只在单个 Widget 中使用时。

在应用的早期阶段或处理一些简单页面时,我们可能遇到这样的需求:展示一个从网络获取的、一次性加载且不会变化的信息,比如一个票务详情页。

解决方案: 最直接的方式是在 StatelessWidget 内部定义一个网络请求方法,并结合 FutureBuilder 来处理异步数据流,根据 Future 的状态(加载中、完成、错误)构建不同的 UI。

import 'package:flutter/material.dart';

class TicketInfo extends StatelessWidget {
  final String ticketId;

  TicketInfo({required this.ticketId});

  // 模拟网络请求
  Future<String> fetchInfo(String tickId) async {
    await Future.delayed(Duration(seconds: 2)); // 模拟网络延迟
    if (tickId == "123") {
      return 'Ticket Info for ID $tickId: VIP Seat, Row A, Number 10';
    }
    return 'Ticket not found.';
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<String>(
      future: fetchInfo(ticketId), // 直接调用请求方法
      builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return Center(child: CircularProgressIndicator());
        } else if (snapshot.hasError) {
          return Center(child: Text('Error: ${snapshot.error}'));
        } else if (snapshot.hasData) {
          return Padding(
            padding: const EdgeInsets.all(16.0),
            child: Text(
              snapshot.data!,
              style: TextStyle(fontSize: 18),
            ),
          );
        } else {
          return Center(child: Text('No data.'));
        }
      },
    );
  }
}

// 示例用法
// class MyApp extends StatelessWidget {
//   @override
//   Widget build(BuildContext context) {
//     return MaterialApp(
//       home: Scaffold(
//         appBar: AppBar(title: Text('Ticket Details')),
//         body: TicketInfo(ticketId: '123'),
//       ),
//     );
//   }
// }

优点:

  • 简单直观: 代码集中,易于理解和实现。
  • 自给自足: Widget 自身管理其数据获取和展示,不依赖外部。

局限:

  • 耦合度高: 数据获取逻辑与 UI 展示紧密耦合。
  • 难以复用: 如果其他 Widget 也需要 fetchInfo 的逻辑或数据,难以共享。
  • 无法响应变化: FutureBuilder 适用于一次性加载。如果数据需要刷新或响应用户交互而改变,则力不从心。

这种方法是状态管理的“萌芽”,适用于非常简单的只读场景。

2. 成长期:StatefulWidget 管理状态与 StreamBuilder

场景描述: 当你需要根据用户行为或其他事件修改组件状态时。

随着业务逻辑的增加,Widget 不再仅仅是静态展示。用户交互(如点击按钮)、数据流更新(如实时消息)等都需要 Widget 能够响应并更新其 UI。

解决方案: StatefulWidget 及其关联的 State 对象应运而生。State 对象可以持有可变状态,并通过调用 setState() 方法来通知 Flutter 框架该 Widget 的子树需要重建。如果状态的更新是基于连续的异步事件流(例如来自 WebSocket 或 Firebase 的数据),StreamBuilder 是一个理想的选择。

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

class InteractiveCounter extends StatefulWidget {
  @override
  _InteractiveCounterState createState() => _InteractiveCounterState();
}

class _InteractiveCounterState extends State<InteractiveCounter> {
  int _counter = 0;
  late StreamController<int> _streamController;
  Stream<int> get _counterStream => _streamController.stream;

  @override
  void initState() {
    super.initState();
    _streamController = StreamController<int>();
    // 模拟一个外部事件源,每秒增加计数
    Timer.periodic(Duration(seconds: 1), (timer) {
      if (!_streamController.isClosed) {
         _streamController.add(_counter++);
      } else {
        timer.cancel();
      }
    });
  }

  void _incrementCounter() {
    // setState(() { // 如果只是简单的本地状态改变,可以用setState
    //   _counter++;
    // });
    // 通过StreamController发送事件,StreamBuilder会监听
    _streamController.add(++_counter);
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: <Widget>[
        Text('Counter (via StreamBuilder):'),
        StreamBuilder<int>(
          stream: _counterStream,
          initialData: 0,
          builder: (context, snapshot) {
            return Text(
              '${snapshot.data}',
              style: Theme.of(context).textTheme.headlineMedium,
            );
          },
        ),
        SizedBox(height: 20),
        ElevatedButton(
          onPressed: _incrementCounter, // 外部行为触发状态改变
          child: Text('Increment by Button'),
        ),
      ],
    );
  }

  @override
  void dispose() {
    _streamController.close();
    super.dispose();
  }
}

优点:

  • 响应式更新: Widget 能够根据内部或外部事件改变自身状态并重绘。
  • 封装性: 状态及其管理逻辑被封装在 State 对象内部。

局限:

  • 状态提升困难: 如果多个 Widget 需要共享或修改同一个状态,StatefulWidget 本身的机制(如通过构造函数传递回调)会变得复杂和笨拙,容易导致“回调地狱”或“属性钻取 (prop drilling)”。
  • 逻辑与 UI 混合: 业务逻辑(如 _incrementCounter 中的逻辑)仍然与 UI 代码(build 方法)存在于同一个类中。

这是向更复杂状态管理迈出的第一步,解决了局部状态的动态变化问题。

3. 共享期:状态管理工具管理共享状态 (以 Provider 为例)

场景描述: 当你的状态数据需要在不同 Widget 中共享使用时。

随着应用功能的丰富,状态不再是单个 Widget 的私有财产。例如,用户登录状态、主题偏好、购物车内容等,需要在应用的多个部分被访问和修改。

解决方案: 引入专门的状态管理工具,如 Provider, Riverpod, Bloc/Cubit, GetX 等。以 Provider 为例,它利用 Flutter 的 InheritedWidget 机制,允许我们将状态“提供”给其子树中的任何 Widget。

  1. 定义状态模型 (ChangeNotifier):

    class CounterModel extends ChangeNotifier {
      int _count = 0;
      int get count => _count;
    
      void increment() {
        _count++;
        notifyListeners(); // 通知监听者状态已改变
      }
    }
    
  2. 在顶层注入状态:

    void main() {
      runApp(
        ChangeNotifierProvider(
          create: (context) => CounterModel(),
          child: MyApp(),
        ),
      );
    }
    
  3. 在 Widget 中消费状态:

    class ConsumerWidget1 extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        // 方法1: 使用 Consumer Widget
        return Consumer<CounterModel>(
          builder: (context, counter, child) => Text('Count: ${counter.count}'),
        );
      }
    }
    
    class ConsumerWidget2 extends StatelessWidget {
      @override
      Widget build(BuildContext context) {
        // 方法2: 使用 context.watch (需要 provider 5.0+)
        final counter = context.watch<CounterModel>();
        return ElevatedButton(
          onPressed: () => context.read<CounterModel>().increment(), // context.read 用于调用方法
          child: Text('Increment from Widget 2. Current: ${counter.count}'),
        );
      }
    }
    

优点:

  • 解耦: 将状态的生产、管理和消费分离。
  • 易于共享: 状态可以在 Widget 树中方便地跨层级共享。
  • 按需重建: 只有监听特定状态的 Widget 才会在状态变化时重建。

局限:

  • 业务逻辑位置: 尽管状态本身被分离了,但复杂的业务逻辑可能仍然散布在 ChangeNotifier 或直接在 Widget 中调用 ChangeNotifier 的方法。当业务逻辑变得复杂且需要在多处复用时,这种方式可能不够清晰。
  • 测试性: ChangeNotifier 本身是可测试的,但如果它包含了大量的业务逻辑,尤其是与外部服务(如API调用)的交互,测试可能会变得复杂。

Provider 等工具极大地改善了状态共享问题,是 Flutter 开发中非常流行的选择。

4. 分层期:MVVM 模式 - 状态与 UI 组件分离

场景描述: 当你的多个相同业务逻辑需要在不同的 Widget 中共享使用时,或者当 Widget 中的业务逻辑变得过于复杂时。

当应用增长到一定规模,我们不仅需要共享状态,还需要共享和复用处理这些状态的业务逻辑。同时,为了保持 Widget 的简洁(只负责 UI 展示和用户输入),需要将业务逻辑抽离出来。

解决方案: 引入 MVVM (Model-View-ViewModel) 架构模式。

  • Model (模型): 数据模型,通常是简单的 Dart 对象,代表应用的数据结构(例如 User, Product)。
  • View (视图): Flutter 中的 Widget。它负责展示数据和捕获用户输入,不包含业务逻辑。View 通过监听 ViewModel 的状态变化来更新自己。
  • ViewModel (视图模型): 连接 View 和 Model 的桥梁。它从 Model 获取数据,处理业务逻辑(如数据校验、格式化、API 请求),并将处理后的状态暴露给 View。ViewModel 通常使用 ChangeNotifier 或类似机制来通知 View 更新。
// Model (与之前类似,或更复杂的数据结构)
class User {
  final String id;
  final String name;
  User({required this.id, required this.name});
}

// ViewModel
class UserViewModel extends ChangeNotifier {
  User? _user;
  User? get user => _user;

  bool _isLoading = false;
  bool get isLoading => _isLoading;

  String? _error;
  String? get error => _error;

  // 模拟API请求
  Future<void> fetchUser(String userId) async {
    _isLoading = true;
    _error = null;
    notifyListeners();

    try {
      // 模拟网络请求
      await Future.delayed(Duration(seconds: 1));
      if (userId == "1") {
        _user = User(id: "1", name: "Alice");
      } else {
        throw Exception("User not found");
      }
    } catch (e) {
      _error = e.toString();
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }
}

// View (使用 UserViewModel)
class UserProfileView extends StatelessWidget {
  final String userId;
  UserProfileView({required this.userId});

  @override
  Widget build(BuildContext context) {
    // 假设 UserViewModel 已通过 Provider 在上层提供
    // final userViewModel = context.watch<UserViewModel>(); // 监听变化
    // 或者在 StatefulWidget 中:
    // @override
    // void initState() {
    //   super.initState();
    //   // 在 initState 中获取 Provider 实例并调用方法,但不监听
    //   // 通常通过 Provider.of<UserViewModel>(context, listen: false).fetchUser(userId);
    //   // 或者使用专门的 Consumer/Selector 来管理生命周期和重建
    // }
    //
    // 为了演示,这里简化为直接创建一个,但在实际项目中应由Provider管理
    return ChangeNotifierProvider(
      create: (_) => UserViewModel()..fetchUser(userId), // 创建并立即获取用户
      child: Consumer<UserViewModel>(
        builder: (context, viewModel, child) {
          if (viewModel.isLoading) {
            return Center(child: CircularProgressIndicator());
          }
          if (viewModel.error != null) {
            return Center(child: Text('Error: ${viewModel.error}'));
          }
          if (viewModel.user != null) {
            return Text('User: ${viewModel.user!.name}');
          }
          return Center(child: Text('No user data.'));
        },
      ),
    );
  }
}

优点:

  • 关注点分离 (SoC): UI (View)、业务逻辑 (ViewModel) 和数据 (Model) 各司其职。
  • 可测试性: ViewModel 不依赖 Flutter UI 框架,可以进行纯 Dart 单元测试。
  • 可复用性: 同一个 ViewModel 可以被多个不同的 View 使用。
  • 代码清晰: Widget 代码变得更简洁,专注于渲染。

局限:

  • ViewModel 膨胀: 如果 ViewModel 负责了过多的数据获取和处理逻辑(例如直接调用多个 API、处理本地数据库等),它本身也可能变得臃肿。
  • 数据源耦合: ViewModel 直接与数据源(如API客户端)交互,如果数据源实现发生变化,ViewModel 需要修改。

MVVM 是一个重要的里程碑,它为构建可维护的大型应用提供了坚实的结构基础。

5. 数据层分离:MVVM + Repository

场景描述: 当你的多个相同数据需要在不同的 ViewModel 中共享使用时,或者当数据获取逻辑(包括缓存策略、远程/本地数据源选择)变得复杂时。

在 MVVM 的基础上,如果多个 ViewModel 都需要访问同一种数据(例如用户信息、产品列表),那么在每个 ViewModel 中都实现一遍数据获取逻辑(如 API 调用、JSON 解析)会造成代码重复,并且难以统一管理数据源。

解决方案: 引入 Repository (仓库) 模式。Repository 负责封装与特定数据类型相关的所有数据操作逻辑,为 ViewModel 提供一个统一的数据访问接口,屏蔽了数据来源的细节(是来自网络、本地缓存还是数据库)。

  • Repository: 定义数据操作接口,并实现具体的获取、存储、更新、删除逻辑。它可以决定是从网络获取新数据,还是从缓存读取,或者两者结合。
// Model (同上)
// User

// Repository Interface (可选,但推荐用于解耦和测试)
abstract class UserRepository {
  Future<User> getUser(String userId);
  // Future<void> updateUser(User user);
}

// Repository Implementation
class UserRepositoryImpl implements UserRepository {
  // 伪API客户端
  final ApiClient _apiClient = ApiClient();
  final LocalCache _localCache = LocalCache();

  @override
  Future<User> getUser(String userId) async {
    try {
      // 尝试从缓存获取
      User? cachedUser = _localCache.getUser(userId);
      if (cachedUser != null) return cachedUser;

      // 缓存未命中,从网络获取
      final userData = await _apiClient.fetchUserData(userId);
      final user = User(id: userData['id'], name: userData['name']);
      _localCache.saveUser(user); // 保存到缓存
      return user;
    } catch (e) {
      // 错误处理
      throw Exception('Failed to get user: $e');
    }
  }
}

// ViewModel (现在依赖 UserRepository)
class UserViewModel extends ChangeNotifier {
  final UserRepository _userRepository; // 依赖注入
  UserViewModel(this._userRepository);

  User? _user;
  User? get user => _user;
  // ... isLoading, error ...

  Future<void> fetchUser(String userId) async {
    // _isLoading = true; notifyListeners();
    try {
      _user = await _userRepository.getUser(userId); // 通过Repository获取数据
    } catch (e) {
      // _error = e.toString();
    } finally {
      // _isLoading = false; notifyListeners();
    }
  }
}

// 伪 API 和 Cache
class ApiClient {
  Future<Map<String, dynamic>> fetchUserData(String userId) async {
    await Future.delayed(Duration(seconds: 1));
    if (userId == "1") return {'id': '1', 'name': 'Alice (from API)'};
    throw Exception('API User not found');
  }
}
class LocalCache {
  final Map<String, User> _cache = {};
  User? getUser(String userId) => _cache[userId];
  void saveUser(User user) => _cache[user.id] = User(id: user.id, name: "${user.name} (cached)");
}

优点:

  • 数据逻辑集中: 数据获取、缓存、同步等逻辑被封装在 Repository 中,ViewModel 只需调用其方法。
  • 单一数据源: 对同一种数据,应用中只有一个权威的获取途径,避免数据不一致。
  • 可测试性增强: Repository 和 ViewModel 都可以独立测试。可以轻松 mock Repository 来测试 ViewModel。
  • 易于切换数据源: 如果后端 API 改变或需要替换缓存方案,只需修改 Repository 的实现,ViewModel 无需变动。

局限:

  • 业务逻辑分散: 如果某些业务逻辑涉及多个 Repository 的协作或复杂的数据转换、校验,这些逻辑放在哪里?如果放在 ViewModel,可能导致 ViewModel 仍然庞大,或者在多个 ViewModel 中重复。

Repository 模式有效地将数据层从业务逻辑中分离出来,是现代应用架构中非常重要的一环。

6. 领域层分离:MVVM + UseCase + Repository

场景描述: 当你的多个相同的复杂业务逻辑需要在不同的 ViewModel 中共享使用时。这些业务逻辑可能涉及多个数据源(即多个 Repository)的协调,或者包含一些不属于任何特定 Repository 或 ViewModel 的纯业务规则。

当应用的核心业务逻辑变得复杂且具有高度可复用性时,直接将其放在 ViewModel 中可能会导致 ViewModel 臃肿且职责不清。例如,“用户下单”这一行为可能涉及:检查库存 (ProductRepository)、创建订单 (OrderRepository)、更新用户积分 (UserRepository)、发送通知等。

解决方案: 引入 UseCase (用例) 或 Interactor (交互器) 层,这是 Clean Architecture 等分层架构中的一个核心概念。UseCase 代表了应用中的一个具体用户场景或业务操作。

  • UseCase: 封装单一的业务操作。它不包含任何 UI 或框架相关的代码,只包含纯粹的业务规则。UseCase 会协调一个或多个 Repository 来完成其任务,并可能进行数据转换和校验。
// Model, Repository (同上)

// UseCase
class GetUserProfileUseCase {
  final UserRepository _userRepository;
  // 可能还有其他Repository,如 UserPreferencesRepository

  GetUserProfileUseCase(this._userRepository);

  Future<UserPresentationModel> execute(String userId) async {
    // 核心业务逻辑
    final user = await _userRepository.getUser(userId);
    // 可以在这里进行数据转换、组合、校验等业务逻辑
    // 例如,根据用户类型决定展示哪些信息
    bool isVip = user.name.contains("VIP"); // 简化版业务逻辑
    return UserPresentationModel(
      displayName: "User: ${user.name}",
      isVip: isVip,
    );
  }
}

// ViewModel (现在依赖 UseCase)
class UserViewModel extends ChangeNotifier {
  final GetUserProfileUseCase _getUserProfileUseCase;
  UserViewModel(this._getUserProfileUseCase);

  UserPresentationModel? _userPresentation;
  UserPresentationModel? get userPresentation => _userPresentation;
  // ... isLoading, error ...

  Future<void> loadUserProfile(String userId) async {
    // _isLoading = true; notifyListeners();
    try {
      _userPresentation = await _getUserProfileUseCase.execute(userId);
    } catch (e) {
      // _error = e.toString();
    } finally {
      // _isLoading = false; notifyListeners();
    }
  }
}

// 一个简单的 Presentation Model,用于适配UI显示
class UserPresentationModel {
  final String displayName;
  final bool isVip;
  UserPresentationModel({required this.displayName, required this.isVip});
}

优点:

  • 高度内聚、低耦合: 每个 UseCase 专注于一个明确的业务目标。
  • 业务逻辑复用: 相同的业务流程可以被多个不同的 ViewModel 调用。
  • 极致的可测试性: UseCase 是纯 Dart 类,非常容易进行单元测试。
  • 清晰的架构边界: 形成了 View -> ViewModel -> UseCase -> Repository -> DataSource 的清晰分层。
  • 符合 Clean Architecture 思想: 业务逻辑(领域层)位于架构中心,不依赖外部层(如 UI、数据持久化)。

局限:

  • 额外的抽象层: 对于简单的应用或业务逻辑,引入 UseCase 可能会增加不必要的复杂性和模板代码。
  • 调用链变长: 数据流和控制流需要经过更多层。

引入 UseCase 代表了向更成熟、更健壮的架构设计迈进,特别适合大型、复杂且需要长期维护的项目。

小结

Flutter 状态管理的演进是一个从简单到复杂、从耦合到解耦的自然生长过程:

  1. Stateless + FutureBuilder:适合简单只读数据场景。
  2. Stateful + StreamBuilder:支持动态交互,但逻辑与 UI 耦合。
  3. Provider:实现状态共享,解耦初步显现。
  4. MVVM:分离状态与 UI,适应复杂业务逻辑。
  5. MVVM + Repository:数据层分离,提升复用性。
  6. MVVM + UseCase + Repository:领域逻辑分离,适合大型项目。

最后:没有银弹,只有适者生存

这个“自然生长”的过程告诉我们,并不存在一个“最好”的状态管理方案,只有“最适合当前项目阶段和复杂度”的方案。

  • 对于小型或一次性项目,StatefulWidget 或简单的 FutureBuilder/StreamBuilder 可能就足够了。
  • 当需要状态共享时,ProviderRiverpod 是优秀的选择。
  • 对于中大型应用,MVVM 结合 Repository 提供了良好的结构和可维护性。
  • 而对于业务逻辑极其复杂、追求极致可测试性和长期演进能力的大型企业级应用,引入 UseCase 层的类 Clean Architecture 方案则更具优势。

需要注意的是, MVVM是一个分水岭. 一个企业级项目, 必然会达到MVVM级别的复杂度.

如同生物进化一样,我们的代码架构也应该随着环境(需求)的变化而“自然生长”. 一个新项目, 没有必要立即启用 MVVM+UseCase+Repo的模式, 一个健壮、高效且易于维护的代码库需要的只是简单的开始和自然生长的空间.

相关