将flutter_bloc与tabview一起使用

Posted

技术标签:

【中文标题】将flutter_bloc与tabview一起使用【英文标题】:Using flutter_bloc with tabview 【发布时间】:2021-02-14 03:31:36 【问题描述】:

我有一个标签视图,其中包含流行的、最近的和即将到来的类别。他们对 api 都有相同的响应。我正在尝试使用 flutter_bloc 从 api 获取数据。以前我使用 rxdart 主题,并为每种类型的数据创建了一个主题。现在使用颤振块我想达到同样的效果。我想要做的是在选项卡之间切换。以前我使用 behaviorsubject 将数据保存到下一个事件,但现在我想转换为 bloc 模式。如何使用 flutter_bloc 获得相同类型的结果?或者我需要为每种类型创建块?最后,如何从 api 获取数据,以便在切换选项卡时保持状态?我的 Rxdart 实现:

class DataBloc 
  final DataRepo _repository = DataRepo();
  final BehaviorSubject<Data> _recent = BehaviorSubject<Data>();
  final BehaviorSubject<Data> _popular = BehaviorSubject<Data>();
  final BehaviorSubject<Data> _upcoming = BehaviorSubject<Data>();
  
getData(String type) async 
    
    Data response = await _repository.getData(type);
    if (type == "recent") 
      _recent.sink.add(response);
     else if (type == "upcoming") 
      _upcoming.sink.add(response);
     else 
      _popular.sink.add(response);
    
  

  dispose() 
    _recent?.close();
    _popular?.close();
    _upcoming?.close();
  

  BehaviorSubject<Data> get recent => _recent;
  BehaviorSubject<Data> get popular => _popular;
  BehaviorSubject<Data> get upcoming => _upcoming;

【问题讨论】:

【参考方案1】:

您的问题肯定没有单一的解决方案。我会回答你的问题,我会给你一个完整的实现/示例,以便于理解。

我需要为每种类型创建 bloc 吗?

我向您建议的是为每个数据创建一个 BLoC(如示例),因为它会简化 BLoC 的逻辑(特别是如果您不想一次加载所有数据)并且应用程序将结构化且耦合较少,这很好。但是,如果您愿意,您仍然可以在一个 BLoC 中完成此操作。

如何从 api 获取数据,以便在切换选项卡时保持状态?

是的只要使用相同的 BLoC/Cubit 实例,状态就会被持久化。每次构建集团(使用BlocBuilder)时,您都会获得最后一个状态。在我的示例中,我们只调用一次load() 事件,在呈现标签视图时。

import 'package:bloc/bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

void main() 
  runApp(MyApp());


class MyApp extends StatelessWidget 
  ///
  /// The repository that is shared among all BLOCs
  ///
  final Repository repository = Repository();

  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) 
    // For example purpose I will expose them as global cubits
    return MultiBlocProvider(
        providers: [
          BlocProvider<PopularCategoriesCubit>(
            create: (context) =>
                // Create the cubit and also call the LOAD event right away.
                //
                // NOTE #4. The cubit is created only when is requested (by
                // BlocBuilder, BlocListener, etc). This is why when you move to
                // a FRESH NEW tab you see the loading state.
                //
                PopularCategoriesCubit(repository: repository)..load(),
          ),
          BlocProvider<RecentCategoriesCubit>(
            create: (context) =>
                RecentCategoriesCubit(repository: repository)..load(),
          ),
          BlocProvider<UpcomingCategoriesCubit>(
            create: (context) =>
                UpcomingCategoriesCubit(repository: repository)..load(),
          ),
        ],
        child: MaterialApp(
          title: 'Flutter Demo',
          theme: ThemeData(
            primarySwatch: Colors.blue,
            visualDensity: VisualDensity.adaptivePlatformDensity,
          ),
          home: MyHomePage(),
        ));
  


class MyHomePage extends StatelessWidget 
  @override
  Widget build(BuildContext context) 
    return DefaultTabController(
      length: 3,
      child: Scaffold(
        appBar: AppBar(
          title: Text("Bloc Tabs"),
          bottom: TabBar(
            tabs: [
              Tab(text: "Popular"),
              Tab(text: "Recent"),
              Tab(text: "Upcoming"),
            ],
          ),
        ),
        body: TabBarView(
          children: [
            // POPULAR TAB
            BlocBuilder<PopularCategoriesCubit, GenericState>(
              builder: (context, state) 
                if (state.isFailed)
                  return Text("Failed to fetch popular categories.");

                if (state.isLoading)
                  return Text("Loading popular categories...");

                return ListView(
                  children: [
                    for (var category in state.categories) Text(category)
                  ],
                );
              ,
            ),

            // RECENT TAB
            BlocBuilder<RecentCategoriesCubit, GenericState>(
              builder: (context, state) 
                if (state.isFailed)
                  return Text("Failed to fetch recent categories.");

                if (state.isLoading)
                  return Text("Loading recent categories...");

                return ListView(
                  children: [
                    for (var category in state.categories) Text(category)
                  ],
                );
              ,
            ),

            // UPCOMMING TAB
            BlocBuilder<UpcomingCategoriesCubit, GenericState>(
              builder: (context, state) 
                if (state.isFailed)
                  return Text("Failed to fetch upcoming categories.");

                if (state.isLoading)
                  return Text("Loading upcoming categories...");

                return ListView(
                  children: [
                    for (var category in state.categories) Text(category)
                  ],
                );
              ,
            ),
          ],
        ),
        // This trailing comma makes auto-formatting nicer for build methods.
      ),
    );
  


// =============================================================================

///
/// Repository Mock
///
class Repository 
  ///
  /// Retreive data by type.
  ///
  /// NOTE #1. Is better to use enum instead of String.
  ///
  Future<List<String>> getData(String type) async 
    // Emulate netword delay
    return Future<List<String>>.delayed(Duration(seconds: 2)).then((_) 
      switch (type) 
        case "popular":
          return [
            "Popular 1",
            "Popular 2",
            "Popular 3",
            "Popular 5",
            "Popular 6"
          ];

        case "recent":
          return [
            "Recent 1",
            "Recent 2",
            "Recent 3",
          ];

        case "upcoming":
        default:
          return [
            "Upcomming 1",
            "Upcomming 2",
          ];
      
    );
  


///
/// This is a generic state used for all categories types
///
/// NOTE #2. Use Equatable. Also if you feel you can break this GenericState in
/// multiple classes as CategoriesLoadedState, CategoriesLoadingState,
/// CategoriesFailedState ...
///
class GenericState 
  ///
  /// Categories data
  ///
  final List<String> categories;

  ///
  /// Tells the data is loading or not
  ///
  final bool isLoading;

  ///
  /// Tells whether the state has errors or not
  ///
  final bool isFailed;

  GenericState(
      this.categories, this.isLoading = false, this.isFailed = false);


///
/// Popular categories Cubit
///
class PopularCategoriesCubit extends Cubit<GenericState> 
  ///
  /// Repository dependency
  ///
  final Repository repository;

  ///
  /// Cubit constructor. Send a loading state as default.
  ///
  PopularCategoriesCubit(@required this.repository)
      : super(GenericState(isLoading: true));

  // ==================================
  // EVENTS
  // ==================================

  ///
  /// Load data from repository
  ///
  void load() async 
    //#log
    print("[EVENT] Popular Categories :: Load");

    // Every time when try to load data from repository put the application
    // in a loading state
    emit(GenericState(isLoading: true));

    try 
      // Wait for data from repository
      List categories = await this.repository.getData("popular");

      // Send a success state
      emit(GenericState(categories: categories, isFailed: false));
     catch (e) 
      // For debugging
      print(e);
      // For example purpose we do not have a message
      emit(GenericState(isFailed: true));
    
  


///
/// Recent categories Cubit
///
class RecentCategoriesCubit extends Cubit<GenericState> 
  ///
  /// Repository dependency
  ///
  final Repository repository;

  ///
  /// Cubit constructor. Send a loading state as default.
  ///
  RecentCategoriesCubit(@required this.repository)
      : super(GenericState(isLoading: true));

  // ==================================
  // EVENTS
  // ==================================

  ///
  /// Load data from repository
  ///
  void load() async 
    //#log
    print("[EVENT] Recent Categories :: Load");

    // Every time when try to load data from repository put the application
    // in a loading state
    emit(GenericState(isLoading: true));

    try 
      // Wait for data from repository
      List categories = await this.repository.getData("recent");

      // Send a success state
      emit(GenericState(categories: categories, isFailed: false));
     catch (e) 
      // For debugging
      print(e);
      // For example purpose we do not have a message
      emit(GenericState(isFailed: true));
    
  


///
/// Upcoming categories Cubit
///
class UpcomingCategoriesCubit extends Cubit<GenericState> 
  ///
  /// Repository dependency
  ///
  final Repository repository;

  ///
  /// Cubit constructor. Send a loading state as default.
  ///
  UpcomingCategoriesCubit(@required this.repository)
      : super(GenericState(isLoading: true));

  // ==================================
  // EVENTS
  // ==================================

  ///
  /// Load data from repository
  ///
  void load() async 
    //#log
    print("[EVENT] Upcoming Categories :: Load");

    // Every time when try to load data from repository put the application
    // in a loading state
    emit(GenericState(isLoading: true));

    try 
      // Wait for data from repository
      List categories = await this.repository.getData("upcoming");

      // Send a success state
      emit(GenericState(categories: categories, isFailed: false));
     catch (e) 
      // For debugging
      print(e);
      // For example purpose we do not have a message
      emit(GenericState(isFailed: true));
    
  

只需将代码复制并粘贴到 main.dart 中即可查看结果。我试图尽可能多地注释代码以帮助您理解。

另外我推荐BLOC from Zero to Hero 教程。对理解和正确使用 BLoC 库有很大帮助。


更新 1

每次更改选项卡时重新加载数据

要在每次更改选项卡时重新加载数据,您可以使用 TabBar 中的onTap,如下所示。

TabBar(
  onTap: (tabIndex) 
    switch (tabIndex) 
      // Popular
      case 0:
        BlocProvider.of<PopularCategoriesCubit>(context).load();
        break;

      // Recent
      case 1:
        BlocProvider.of<RecentCategoriesCubit>(context).load();
        break;

      // Upcoming
      case 2:
        BlocProvider.of<UpcomingCategoriesCubit>(context).load();
        break;
    
  ,
  tabs: [
    Tab(text: "Popular"),
    Tab(text: "Recent"),
    Tab(text: "Upcoming"),
  ],
),

注意:现在您不必在创建“Recent”和“Upcoming”cubits(非默认选项卡)时发出load() - 因为Tab 点击会处理这个问题。

BlocProvider<RecentCategoriesCubit>(
  create: (context) =>
      RecentCategoriesCubit(repository: repository),
),
BlocProvider<UpcomingCategoriesCubit>(
  create: (context) =>
      UpcomingCategoriesCubit(repository: repository),
),

【讨论】:

非常感谢。非常丰富的答案,也许是我从堆栈溢出中得到的最佳答案。我在每个选项卡小部件的 initstate 中发送事件,因此每次选项卡更改时,块都会调度加载事件。我可能需要对其进行一些调整才能使其正常工作。 不客气。我已经编辑了这篇文章,以便为您提供一个优雅的解决方案。 是的,这正是我在实现中所做的。尽管我调整了事件和状态并且我使用的是 bloc 而不是 cubit,但它按预期工作。到目前为止看起来不错。非常感谢:)

以上是关于将flutter_bloc与tabview一起使用的主要内容,如果未能解决你的问题,请参考以下文章

从一个选项卡滑动到另一个选项卡时,TabView 不保留状态

如何在具有颤动的可滚动视图中使用具有可变高度内容的TabView?

如何将服务器端和客户端的 tabview 组件与 jsf 匹配?

flutter_bloc 的 BlocBuilder 能否避免重建 Widget 的未更改部分?

Flutter_bloc 从没有 UI 事件的 firestore 获取更新的数据

嵌入在 NavigationView 中的 TabView 将内容下推