Flutter之旅 -- 項(xiàng)目架構(gòu)

本篇文章主要介紹以下幾個(gè)知識(shí)點(diǎn):

  • 分層架構(gòu)設(shè)計(jì)
  • Provider vs Riverpod
  • Riverpod 在實(shí)際項(xiàng)目中的使用示例
Flutter之旅

1. 分層架構(gòu)設(shè)計(jì)

Android 應(yīng)用架構(gòu)指南:https://developer.android.com/topic/architecture?hl=zh-cn
使用 Riverpod 的 Flutter 應(yīng)用架構(gòu)指南:https://codewithandrea.com/articles/flutter-app-architecture-riverpod-introduction/

1.1 Flutter 分層架構(gòu)概述

分層架構(gòu)

基于 Clean Architecture 原則,F(xiàn)lutter 應(yīng)用采用分層架構(gòu)確保關(guān)注點(diǎn)分離:

┌─────────────────────────────────────┐
│         Presentation Layer          │  ← UI 層
├─────────────────────────────────────┤
│         Application Layer           │  ← 業(yè)務(wù)邏輯層
├─────────────────────────────────────┤
│           Domain Layer              │  ← 領(lǐng)域?qū)?├─────────────────────────────────────┤
│            Data Layer               │  ← 數(shù)據(jù)層
└─────────────────────────────────────┘

主要層次包括:

  • data layer:(數(shù)據(jù)層)

    • 職責(zé):數(shù)據(jù)獲取、存儲(chǔ)、網(wǎng)絡(luò)請(qǐng)求
    • 組件:Repositories, Data Sources, APIs
    • 特點(diǎn):處理數(shù)據(jù)的 CRUD 操作、抽象數(shù)據(jù)來(lái)源(網(wǎng)絡(luò)、本地?cái)?shù)據(jù)庫(kù))、數(shù)據(jù)轉(zhuǎn)換和緩存策略
  • domain layer:(領(lǐng)域?qū)樱?/p>

    • 職責(zé):業(yè)務(wù)實(shí)體、業(yè)務(wù)規(guī)則(構(gòu)造model模型)
    • 組件:Models, Entities, Value Objects
    • 特點(diǎn):純 Dart 代碼(不依賴(lài) Flutter)、定義數(shù)據(jù)結(jié)構(gòu)和業(yè)務(wù)規(guī)則
  • application layer:(應(yīng)用層)
    • 職責(zé):狀態(tài)管理、業(yè)務(wù)流程編排
    • 組件:StateNotifier, Provider, Use Cases
    • 特點(diǎn):協(xié)調(diào)不同領(lǐng)域服務(wù)、管理應(yīng)用狀態(tài)、處理用戶(hù)操作的業(yè)務(wù)邏輯
  • presentation layer:(表現(xiàn)層)
    • 職責(zé):UI 組件、頁(yè)面、用戶(hù)交互
    • 組件:Widgets, Pages, Dialogs
    • 特點(diǎn):只負(fù)責(zé) UI 渲染和用戶(hù)交互、通過(guò) Riverpod Provider 獲取狀態(tài)、不包含業(yè)務(wù)邏輯

1.2 與 Android 架構(gòu)對(duì)比

對(duì)比 Android 應(yīng)用架構(gòu)
層級(jí) Flutter + Riverpod Android MVVM
表現(xiàn)層 Widget + Consumer View + Activity/Fragment
應(yīng)用層 StateNotifier + Provider ViewModel + LiveData/Flow
領(lǐng)域?qū)?/td> Domain Models Use Cases(網(wǎng)域?qū)樱蛇x)
數(shù)據(jù)層 Repository + DataSource Repository + Room/Retrofit

相似點(diǎn):

  • 都遵循單向數(shù)據(jù)流
  • 都有明確的層級(jí)分離
  • 都使用觀察者模式進(jìn)行狀態(tài)管理

差異點(diǎn):

  • Flutter 使用 Riverpod 的依賴(lài)注入更加簡(jiǎn)潔、響應(yīng)式編程更加直觀
  • Android 領(lǐng)(網(wǎng))域?qū)邮强蛇x的,主要用于封裝復(fù)雜業(yè)務(wù)邏輯或多個(gè) ViewModel 共享的業(yè)務(wù)邏輯,對(duì)應(yīng) Flutter 中的應(yīng)用層(項(xiàng)目中采用了 Riverpod 自動(dòng)生成代碼的方式時(shí),也可以省略)。
  • Flutter 的領(lǐng)域?qū)痈嘤糜诙x數(shù)據(jù)模型和業(yè)務(wù)實(shí)體(只針對(duì)本文的架構(gòu))

2. Provider vs Riverpod

Provider 和 Riverpod 這兩個(gè)庫(kù)的作者都是 Remi Rousselet,新庫(kù)命名是舊庫(kù)的字母重排。

Provider 的優(yōu)點(diǎn)是 簡(jiǎn)單易用,上手難度低,適用于應(yīng)用規(guī)模較小,狀態(tài)管理不太復(fù)雜的場(chǎng)景。

Provider 的局限如下:

  • 依賴(lài) BuildContext。
    Provider 是基于 InheritedWidget 封裝,讀取狀態(tài)需要 BuildContext,所以 只能在Widget樹(shù)中聲明使用。
    而在有些場(chǎng)景下不一定能直接拿到 BuildContext,如在 非UI層 (如業(yè)務(wù)邏輯層) 訪問(wèn)狀態(tài),只能通過(guò)某種方式傳遞 BuildContext 實(shí)例,繁瑣之余還增加了代碼的耦合度。
    使用不當(dāng),還可能導(dǎo)致 ProviderNotFoundException。

  • 多個(gè)相同類(lèi)型的 Provider,需要自己維護(hù)一個(gè) Key 進(jìn)行區(qū)分。
    如:Widget 樹(shù)的同一層級(jí),為相同類(lèi)型的狀態(tài)創(chuàng)建多個(gè)同類(lèi)型的 Provider,子 Widget 無(wú)法確定使用哪個(gè) Provider 的數(shù)據(jù),需要指定一個(gè)特定的 Key 來(lái)進(jìn)行區(qū)分:

void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => Counter(1), key: ValueKey(1)),
        ChangeNotifierProvider(create: (_) => Counter(2), key: ValueKey(2)),
      ],
      child: MyApp(),
    ),
  );
}

// 通過(guò)key指定使用Counter實(shí)例
Provider.of<Counter>(context, listen: false, key: ValueKey(1)).increment();
  • 如果需要跨 Widget 共享狀態(tài),Provider 就沒(méi)法弄成局部私有的,只能是全局可訪問(wèn)的。

Riverpod 在 Provider 的基礎(chǔ)上進(jìn)行重構(gòu),解決上述問(wèn)題之余,提供了 更靈活/精細(xì)的狀態(tài)管理機(jī)制,狀態(tài)不可變,編譯時(shí)類(lèi)型安全、易于測(cè)試等特性,更清晰的代碼組織和維護(hù)方式 (注解代碼生成),可以有效的組織和管理大規(guī)模的狀態(tài)。

選擇 Riverpod 的理由:

  1. 類(lèi)型安全: 編譯時(shí)檢查,減少運(yùn)行時(shí)錯(cuò)誤
  2. 更好的性能: 精確的重建控制,避免不必要的 Widget 重建
  3. 簡(jiǎn)化的 API: 更直觀的語(yǔ)法,減少樣板代碼
  4. 強(qiáng)大的開(kāi)發(fā)工具: 更好的調(diào)試和開(kāi)發(fā)體驗(yàn)
  5. 測(cè)試友好: 更容易進(jìn)行單元測(cè)試和集成測(cè)試

3. Riverpod 基本使用與核心原理

3.1 Riverpod 基本使用

Riverpod 使用詳解可參考:https://juejin.cn/post/7359402114018689076
官方文檔:https://riverpod.dev/docs/introduction/why_riverpod

Riverpod 提供了多種狀態(tài)管理模式,適用于不同的場(chǎng)景:

模式 適用場(chǎng)景 優(yōu)點(diǎn) 缺點(diǎn)
StateProvider 簡(jiǎn)單狀態(tài) 語(yǔ)法簡(jiǎn)單,直接修改 不適合復(fù)雜邏輯
StateNotifier 復(fù)雜狀態(tài) 強(qiáng)類(lèi)型,業(yè)務(wù)邏輯封裝好 需要更多代碼
FutureProvider 一次性異步 自動(dòng)處理加載狀態(tài) 不適合可變異步操作
StreamProvider 持續(xù)數(shù)據(jù)流 自動(dòng)處理流狀態(tài) 需要管理流的生命周期
ChangeNotifier 兼容舊代碼 兼容性好 性能相對(duì)較差
Notifier 現(xiàn)代化狀態(tài)管理 語(yǔ)法簡(jiǎn)潔,性能好 需要代碼生成

3.2 Riverpod 核心原理

把應(yīng)用的“數(shù)據(jù)源”和“依賴(lài)關(guān)系”想象成一張河網(wǎng):上游的水質(zhì)變化,會(huì)沿著支流層層傳導(dǎo)到下游。
Riverpod 做的事情,就是把這張“依賴(lài)河網(wǎng)”用代碼表達(dá)出來(lái),并且自動(dòng)完成“緩存、傳導(dǎo)、重算、清理”。
這讓復(fù)雜異步場(chǎng)景變得簡(jiǎn)單,正如官方所說(shuō):它是一個(gè)“響應(yīng)式緩存框架”,專(zhuān)注于緩存與自動(dòng)刷新。

  • 核心對(duì)象
    • ProviderContainer:一座“水庫(kù)”,保存所有 provider 的緩存與依賴(lài)圖,支持覆蓋與作用域。
    • Provider/Family:一段“取水邏輯”的聲明,描述 value 如何被計(jì)算,是否帶參數(shù)(family)。
    • Ref(WidgetRef/Ref):提供讀依賴(lài)、注冊(cè)監(jiān)聽(tīng)、生命周期回調(diào)(onDispose/keepAlive)的“水工”。
    • Listener:下游訂閱者,只有當(dāng)感興趣的上游發(fā)生變化時(shí)才會(huì)被通知(select 精準(zhǔn)過(guò)濾)。

一個(gè)極簡(jiǎn)(偽)實(shí)現(xiàn):

// 極簡(jiǎn)容器:保存緩存與依賴(lài)
class MiniContainer {
  final Map<Object, dynamic> _cache = {};
  final Map<Object, Set<Object>> _deps = {}; // provider -> its dependencies

  T read<T>(MiniProvider<T> provider) {
    if (_cache.containsKey(provider)) return _cache[provider] as T;

    final tracker = _DependencyTracker(this, provider);
    final value = provider.create(tracker);
    _cache[provider] = value;
    _deps[provider] = tracker.dependencies;
    return value;
  }

  // 當(dāng)依賴(lài)變更時(shí),向下游傳播“需要重算”的信號(hào)
  void markDirty(Object changed) {
    for (final entry in _deps.entries) {
      if (entry.value.contains(changed)) {
        _cache.remove(entry.key); // 使下游失效,下一次 read 時(shí)重算
        markDirty(entry.key);
      }
    }
  }
}

class _DependencyTracker {
  final MiniContainer container;
  final Object owner;
  final Set<Object> dependencies = {};
  _DependencyTracker(this.container, this.owner);

  T watch<T>(MiniProvider<T> dep) {
    dependencies.add(dep);
    return container.read(dep);
  }
}

class MiniProvider<T> {
  final T Function(_DependencyTracker ref) create;
  const MiniProvider(this.create);
}

上面?zhèn)未a展示了 Riverpod 的核心:

  • read 第一次會(huì)計(jì)算并緩存 value;
  • watch 讓容器記錄“誰(shuí)依賴(lài)了誰(shuí)”;
  • 當(dāng)上游變化,容器遞歸失效下游緩存;
  • 下游在下次被讀取/監(jiān)聽(tīng)時(shí)自動(dòng)重算。

4. 工作流實(shí)現(xiàn)

下面展示在實(shí)際項(xiàng)目中的工作流:

  • 模塊目錄結(jié)構(gòu)
lib/
├── api/
│   └── api_service.dart                      # api 相關(guān)
|
├── features/
│   ├── air/
│       ├── application/                      # 應(yīng)用層
│       │   └── providers.dart
│       ├── data/                             # 數(shù)據(jù)層
│       │   └── history_repository.dart       # 數(shù)據(jù)倉(cāng)庫(kù)
│       ├── domain/                           # 領(lǐng)域?qū)?│       │   ├── history.dart                  # 數(shù)據(jù)模型
│       │   └── history.g.dart
│       └── presentation/                     # 表現(xiàn)層
│           ├── widget/                       # 通用組件
│           │   └── chart_bar.dart
|           ├── dialog/                       # 對(duì)話框組件
│           └── statistics_page.dart          # 統(tǒng)計(jì)UI頁(yè)面
  • 定義 API 接口
/// lib/api/api.dart
class Api {
  static const String _path = '/dev/v1/';

  // 獲取指定時(shí)間段的歷史數(shù)據(jù):/user/nodes/tsdata
  static const String historyData = "${_path}user/nodes/tsdata";
}

/// lib/api/api_service.dart
class ApiService {
  // 獲取24小時(shí)平均值
  Future<Response> get24hoursData(
      {required String nodeId,
      required String paramName,
      String type = "float"}) async {
    var response = await _httpUtil
        .request(Method.get, Api.historyData, queryParameters: {
      "node_id": nodeId,
      "param_name": "$paramIdentifier.$paramName",
      "type": type,
      "aggregate": "avg",
      "aggregation_interval": "hour",
      "num_intervals": "24"
    });
    return response;
  }
}
  • 定義數(shù)據(jù)倉(cāng)庫(kù)(data layer)
/// lib/features/air/data/history_repository.dart
class HistoryRepository {
  final ApiService apiService;

  HistoryRepository({required this.apiService});

  // 獲取24小時(shí)平均值
  Future<History> get24hoursData(
      {required String nodeId,
      required String paramName,
      String type = "float"}) async {
    var response = await apiService.get24hoursData(
        nodeId: nodeId, paramName: paramName, type: type);
    Log.d("get24hoursData: ${response.data}");
    return History.fromJson(response.data);
  }
}

/// Providers
final historyRepositoryProvider = Provider<HistoryRepository>((ref) {
  return HistoryRepository(apiService: ApiService.instance());
});
  • 定義數(shù)據(jù)模型(domain layer)
import 'package:json_annotation/json_annotation.dart';

part 'history.g.dart';

/// lib/features/air/domain/history.dart
@JsonSerializable()
class History {
  @JsonKey(name: "ts_data")
  final List<TsDatum> tsData;

  History({
    required this.tsData,
  });

  factory History.fromJson(Map<String, dynamic> json) => _$HistoryFromJson(json);

  Map<String, dynamic> toJson() => _$HistoryToJson(this);
}
  • 實(shí)現(xiàn)狀態(tài)管理(application layer)
/// 狀態(tài)定義
class AirState {
  final AsyncValue<History> data; 

  const AirState({
    this.data = const AsyncLoading(),
  });

  AirState copyWith({AsyncValue<History>? data}) {
    return AirState(data: data ?? this.data);
  }
}

/// 狀態(tài)管理器
class AirNotifier extends StateNotifier<AirState> {
  final HistoryRepository _repository;
  final String _nodeId;

  AirNotifier(this._repository, this._nodeId) : super(const AirState());

  // 獲取歷史數(shù)據(jù)
  Future<History> getHistoryData() async {
    final result =  _repository.get24hoursData(_nodeId);
    if (mounted) {
        state = state.copyWith(data: result);
    }
    return result;
  }
}

// 歷史數(shù)據(jù)狀態(tài)管理器 provider
final airNotifierProvider = StateNotifierProvider.autoDispose
    .family<AirNotifier, AirState, String>((ref, nodeId) {
  final repository = ref.watch(historyRepositoryProvider);
  return AirNotifier(repository, nodeId);
});
  • UI 層實(shí)現(xiàn)(presentation layer)
/// lib/features/air/presentation/statistics_page.dart
class StatisticsPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final state = ref.watch(airNotifierProvider('node_id'));
    return state.when(
      data: (data) => HistoryView(state),
      loading: () => CircularProgressIndicator(),
      error: (err, stack) => Text('Error: $err'),
    );
  }
}

小結(jié)

這個(gè)工作流展示了完整的數(shù)據(jù)流向:

  1. API 定義 → 定義網(wǎng)絡(luò)請(qǐng)求接口
  2. Repository 實(shí)現(xiàn) → 處理數(shù)據(jù)獲取、緩存、錯(cuò)誤處理
  3. Domain 模型 → 定義業(yè)務(wù)數(shù)據(jù)結(jié)構(gòu)
  4. StateNotifier → 管理復(fù)雜業(yè)務(wù)狀態(tài)
  5. Provider 定義 → 提供依賴(lài)注入和狀態(tài)訪問(wèn)
  6. UI 消費(fèi) → 響應(yīng)式 UI 更新

數(shù)據(jù)流向:

UI (Consumer) 
  ↓ ref.watch()
Provider 
  ↓ StateNotifier
Application Layer 
  ↓ Repository
Data Layer 
  ↓ API/Database
External Data Source

5. 注意事項(xiàng)

  • Provider 生命周期管理
// ? 錯(cuò)誤:不必要的長(zhǎng)期持有
final expensiveProvider = Provider<ExpensiveService>((ref) {
  return ExpensiveService(); // 會(huì)一直存在內(nèi)存中
});

// ? 正確:使用 autoDispose
final expensiveProvider = Provider.autoDispose<ExpensiveService>((ref) {
  final service = ExpensiveService();
  
  // 清理資源
  ref.onDispose(() {
    service.dispose();
  });
  
  return service;
});

// 需要時(shí)保持活躍
final cacheProvider = Provider.autoDispose<CacheService>((ref) {
  final cache = CacheService();
  
  // 在有數(shù)據(jù)時(shí)保持活躍
  if (cache.hasData) {
    ref.keepAlive();
  }
  
  return cache;
});
  • 避免過(guò)度監(jiān)聽(tīng)
// ? 錯(cuò)誤:監(jiān)聽(tīng)整個(gè)復(fù)雜狀態(tài)
class MyWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userState = ref.watch(userProvider); // 整個(gè)狀態(tài)變化都會(huì)重建
    
    return Text(userState.user?.name ?? '');
  }
}

// ? 正確:只監(jiān)聽(tīng)需要的部分
final userNameProvider = Provider<String?>((ref) {
  return ref.watch(userProvider.select((state) => state.user?.name));
});

class MyWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userName = ref.watch(userNameProvider); // 只有名字變化才重建
    
    return Text(userName ?? '');
  }
}

// 或者使用 select
class MyWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userName = ref.watch(
      userProvider.select((state) => state.user?.name)
    );
    
    return Text(userName ?? '');
  }
}
  • 正確使用 Family Provider
// ? 錯(cuò)誤:Family Provider 參數(shù)過(guò)于復(fù)雜
final userProvider = StateNotifierProvider.family<UserNotifier, UserState, Map<String, dynamic>>((ref, params) {
  return UserNotifier(params['id'], params['config']);
});

// ? 正確:使用簡(jiǎn)單參數(shù)或自定義類(lèi)
class UserParams {
  final String id;
  final UserConfig config;
  
  UserParams(this.id, this.config);
  
  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is UserParams &&
          runtimeType == other.runtimeType &&
          id == other.id &&
          config == other.config;

  @override
  int get hashCode => id.hashCode ^ config.hashCode;
}

final userProvider = StateNotifierProvider.family<UserNotifier, UserState, UserParams>((ref, params) {
  return UserNotifier(params.id, params.config);
});
  • 性能優(yōu)化
// 使用 select 避免不必要的重建
final isLoadingProvider = Provider<bool>((ref) {
  return ref.watch(userProvider.select((state) => state.isLoading));
});

// 使用 Consumer 局部重建
class UserProfile extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // 靜態(tài)內(nèi)容不會(huì)重建
        const Text('用戶(hù)信息'),
        
        // 只有這部分會(huì)根據(jù)狀態(tài)變化重建
        Consumer(
          builder: (context, ref, child) {
            final user = ref.watch(userProvider.select((state) => state.user));
            return Text(user?.name ?? '未登錄');
          },
        ),
        
        // 其他靜態(tài)內(nèi)容
        const SizedBox(height: 20),
      ],
    );
  }
}

6. 總結(jié)

  • 架構(gòu)優(yōu)勢(shì)
  1. 清晰的分層結(jié)構(gòu): 每一層都有明確的職責(zé),便于維護(hù)和測(cè)試
  2. 強(qiáng)類(lèi)型安全: Riverpod 提供編譯時(shí)檢查,減少運(yùn)行時(shí)錯(cuò)誤
  3. 優(yōu)秀的性能: 精確的重建控制,避免不必要的 UI 更新
  4. 易于測(cè)試: 依賴(lài)注入和狀態(tài)隔離使測(cè)試變得簡(jiǎn)單
  5. 可擴(kuò)展性: 模塊化設(shè)計(jì)便于功能擴(kuò)展和團(tuán)隊(duì)協(xié)作
  • 最佳實(shí)踐
  1. 遵循分層原則: 確保每層只處理自己的職責(zé)
  2. 合理使用 Provider: 根據(jù)需求選擇合適的 Provider 類(lèi)型
  3. 注意生命周期: 使用 autoDispose 避免內(nèi)存泄漏
  4. 優(yōu)化性能: 使用 select 和 Consumer 減少不必要的重建
  5. 統(tǒng)一錯(cuò)誤處理: 在合適的層級(jí)處理和轉(zhuǎn)換錯(cuò)誤
  6. 編寫(xiě)測(cè)試: 利用 Riverpod 的測(cè)試友好特性編寫(xiě)單元測(cè)試
  • 適用場(chǎng)景
    • 中大型 Flutter 應(yīng)用
    • 需要復(fù)雜狀態(tài)管理的應(yīng)用
    • 團(tuán)隊(duì)協(xié)作開(kāi)發(fā)的項(xiàng)目
    • 對(duì)性能和可維護(hù)性要求較高的應(yīng)用

通過(guò)合理運(yùn)用 Riverpod 和分層架構(gòu),可以構(gòu)建出高質(zhì)量、可維護(hù)、可測(cè)試的 Flutter 應(yīng)用。關(guān)鍵是要理解每一層的職責(zé),正確使用 Riverpod 的各種特性,并遵循最佳實(shí)踐。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容