【问题标题】:Using flutter_bloc with tabview将flutter_bloc与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;
}

【问题讨论】:

    标签: flutter dart flutter-bloc


    【解决方案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,但它按预期工作。到目前为止看起来不错。非常感谢:)
    猜你喜欢
    • 2021-08-10
    • 1970-01-01
    • 2021-04-17
    • 2015-11-06
    • 1970-01-01
    • 2021-06-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多