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),
),
CLICK HERE to find out more related problems solutions.