Using flutter_bloc with tabview

Issue

I have a tabview where I have popular, recent and upcoming categories. They all have same response on the api. I am trying to fetch data from api using flutter_bloc. Previously I was using rxdart subject and I made a subject for each type of data. Now using flutter bloc I want to achieve the same. What I want to do is to switch between the tabs. Previously I used behaviorsubject to hold the data until next event but now I want to transition to bloc pattern. How do I achieve same type of result using flutter_bloc? Or I need to create bloc for each type? And finally how can I fetch data from api such that when tab is switched, state is persisted? My Rxdart implementation:

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;
}

Solution

Definitely there is not a single solution for your problem. I will answer to your questions and I’ll give you a full implementation/example to be easy to understand.

I need to create bloc for each type?

What I propose to you is to create a BLoC for each data (as in example) because it will simplify the logic of the BLoC (especially if you don’t want to load all data at once) and the application will be structured and less coupled which is good. But still you can accomplish this in one BLoC if you want.

How can I fetch data from api such that when tab is switched, state is persisted?

Yes, the states are persisted as soon as it uses the same BLoC/Cubit instance . Every time when you build the bloc (using BlocBuilder) you will get the last state. In my example we call load() event only once, when the tab view is rendered.

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));
    }
  }
}

Just copy and paste the code in the main.dart and see the results. I tried to comment the code as much as I could to help you understand.

Also I recommend BLOC from Zero to Hero tutorial. It will help you a lot to understand the BLoC library and correctly use it.


UPDATE 1

Reload data every time the Tab is changed

To reload the data every time the Tab is changed you can use onTap from TabBar as below.

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"),
  ],
),

Note: Now you don’t have to emit load() when the the Recent and Upcoming cubits are created (non default tabs) – because Tab tap takes care of that.

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

Answered By – jorjdaniel

Answer Checked By – Robin (FlutterFixes Admin)

Leave a Reply

Your email address will not be published. Required fields are marked *