How to use Flutter Bloc (Part 2)
Hello readers — we're going to make a simple application to see how Bloc works: an age-guessing app built on the agify.io API. Pass a name as a query parameter and it guesses your age. API URL: https://api.agify.io/?name=NameXYZ
I'm not going into the details of creating a package for the repository; instead there's a simple GuessAgeRepo class provided to the tree with RepositoryProvider.
File structure
The features directory contains presentation and business-logic code (UI, widgets, Bloc), one nested directory per feature — login, signup, homepage, and so on. Here there's a single feature, Guess Age, holding the guess_age_page UI file and the Bloc files. Alongside it: main.dart and form_submission_status, which could live in a core directory for code shared across features.
Repo
class GuessAgeRepo {
final String guessAgeAPIURL = 'https://api.agify.io/?name=';
Future<int> guessAge({required String name}) async {
String APIURL = guessAgeAPIURL + name;
log(APIURL);
var url = Uri.parse(APIURL);
try {
var response = await http.get(url);
var data = jsonDecode(response.body);
log('Data fetched: ${data.toString()}');
if(data['age'] != null) {
return data['age'];
}
else{
throw Exception('Error guessing age');
}
}
catch (e){
log('Error fetching data: ${e.toString()}');
rethrow;
}
}
}The repository holds the API URL and one function that calls it with the provided name. Exceptions — a null age, no internet — are thrown here and caught inside the Bloc, which decides what to do (like showing a failure snackbar).
Bloc
Events
abstract class GuessAgeEvent{}
class UpdateName extends GuessAgeEvent{
String name;
UpdateName({required this.name});
}
class GuessMyAge extends GuessAgeEvent{}Two events: UpdateName fires when the user edits the input (the Bloc emits a new state with the updated name), and GuessMyAge fires when the user taps the button.
State
abstract class GuessAgeState extends Equatable{}
class GuessAgeErrorState extends GuessAgeState {
final String error;
GuessAgeErrorState({required this.error});
@override
List<Object?> get props => [error];
}
class GuessAge extends GuessAgeState {
final String name;
final int? age;
final FormSubmissionState status;
GuessAge({required this.status, this.name = '', this.age});
GuessAge copyWith({
FormSubmissionState? status,
String? name,
int? age,
}) {
return GuessAge(
status: status?? this.status,
name: name ?? this.name,
age: age ?? this.age,
);
}
@override
List<Object?> get props => [status, name, age];
}Polymorphism gives us two states: an error state (with a message for the snackbar) and the normal state carrying the entered name, the guessed age, and a FormSubmissionState used to show a progress indicator while awaiting the API. States extend Equatable so Bloc can compare them and skip needless rebuilds.
The Bloc itself
class GuessAgeBloc extends Bloc<GuessAgeEvent, GuessAgeState>{
GuessAgeBloc({required this.guessAgeRepo}) : super(GuessAge(status: InitialFormSubmissionState())){
on<UpdateName>(_updateName);
on<GuessMyAge>(_guessMyAge);
}
GuessAgeRepo guessAgeRepo;
void _updateName(UpdateName event, Emitter<GuessAgeState> emit) {
if(state is GuessAge){
emit((state as GuessAge).copyWith(name: event.name));
}
}
FutureOr<void> _guessMyAge(GuessMyAge event, Emitter<GuessAgeState> emit) async {
if(state is GuessAge) {
try {
String name = (state as GuessAge).name;
emit((state as GuessAge).copyWith(status: SubmittingFormSubmissionState()));
int age = await guessAgeRepo.guessAge(name: name);
emit((state as GuessAge).copyWith(name: '', age: age, status: InitialFormSubmissionState()));
}
catch (e){
emit(GuessAgeErrorState(error: e.toString()));
emit(GuessAge(name: '', status: InitialFormSubmissionState()));
}
}
}
}The repo is passed through the constructor rather than instantiated inside — so we can pass a mock repository when testing the Bloc. Before awaiting the API we switch FormSubmissionState to Submitting so the UI can show a loader; on success we update the age, on failure we emit the error state and reset.
FormSubmissionState
abstract class FormSubmissionState {}
class InitialFormSubmissionState extends FormSubmissionState{}
class SubmittingFormSubmissionState extends FormSubmissionState{}Why classes instead of a boolean on the state? Reusability — and it lets us use BlocSelector on exactly this slice, reducing rebuilds.
UI
class GuessAgePage extends StatefulWidget {
const GuessAgePage({Key? key}) : super(key: key);
@override
State<GuessAgePage> createState() => _GuessAgePageState();
}
class _GuessAgePageState extends State<GuessAgePage> {
final TextEditingController nameController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Guess My Age'),
),
body: BlocListener<GuessAgeBloc, GuessAgeState>(
listener: (context, state) {
if (state is GuessAgeErrorState) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
backgroundColor: Colors.redAccent,
content: Text(state.error),
),
);
}
},
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 20),
children: [
const Text('Enter you name'),
const SizedBox(height: 10),
TextField(
controller: nameController,
decoration: const InputDecoration(border: OutlineInputBorder()),
onChanged: (name) =>
context.read<GuessAgeBloc>().add(UpdateName(name: name)),
),
const SizedBox(height: 40),
BlocSelector<GuessAgeBloc, GuessAgeState, FormSubmissionState>(
selector: (state) {
if(state is GuessAge){
return (state).status;
}
else{
return InitialFormSubmissionState();
}
},
builder: (context, status) {
return SizedBox(
height: 50,
child: status is SubmittingFormSubmissionState
? const Center(
child: CircularProgressIndicator(),
)
: ElevatedButton(
onPressed: () {
context.read<GuessAgeBloc>().add(GuessMyAge());
nameController.clear();
},
child: const Text('Guess My Age'),
),
);
},
),
const SizedBox(height: 20),
BlocSelector<GuessAgeBloc, GuessAgeState, int?>(
selector: (state) {
if (state is GuessAge && state.age != null) {
return state.age;
}
return null;
},
builder: (context, age) {
return age != null
? Text('Your guessed age is: $age')
: const SizedBox();
},
),
],
),
),
);
}
}BlocListener sits at the top to catch error states — per the documentation, use it for things that should happen once per state change, like navigation or snackbars. The TextField dispatches UpdateName on every change. One BlocSelector swaps the submit button for a progress indicator based only on FormSubmissionState; another renders the guessed age and rebuilds only when the age changes. That's the point of BlocSelector over BlocBuilder here: fewer unnecessary rebuilds.
main.dart
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
home: RepositoryProvider(
create: (BuildContext context) => GuessAgeRepo(),
child: BlocProvider(
create: (BuildContext context) => GuessAgeBloc(
guessAgeRepo: context.read<GuessAgeRepo>(),
),
child: const GuessAgePage()),
),
);
}
}RepositoryProvider provides the repo, BlocProvider provides the Bloc built from it, and the page consumes both.
The end
With Bloc we get reusability, maintainability, and code that stays clean as the application grows — plus easy testing, because the code is separated into decoupled layers.
Originally published on Medium.