DEV Community

Tirth Patel
Tirth Patel

Posted on • Originally published at Medium on

4 1

Paginate API results with BLoC in Flutter

Photo by Christin Hume on Unsplash

Hey Everyone 👋, Today we’re going to learn about Pagination in Flutter. Pagination is considered as one of the best practices while loading a large chunk of data from an API. Pagination offers better performance and a jank free experience to the user.

👀 Demo

🧰 We’ll be using,

  • Punk API to get some beers 😋.
  • BLoC patten for state-management.
  • Json Serializable for automatic serialization-deserialization of the API response.

So let’s get started 🍻

🔌 Prerequisites

  • Flutter SDK
  • IDE of choice: VSCode / Intellij Idea / Android Studio
  • Dart & Flutter Plugins for IDE

🔨 Initial Setup

  • After creating a fresh flutter project, add the following dependencies in pubspec.yaml.
# ...
dependencies:
# ...
flutter_bloc: ^6.0.5
http: ^0.12.2
json_annotation: ^3.0.1
dev_dependencies:
# ...
build_runner: ^1.10.1
json_serializable: ^3.4.1
# ...
view raw pubspec.yaml hosted with ❤ by GitHub
  • Delete everything from the main.dart and paste the following content. It has a main method, app routes, and a bloc observer for debugging purposes. Next, we’ll be building the DisplayBeerScreen widget.
import 'package:beer_app/feature_display_beer/beer_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
void main() {
Bloc.observer = BeerBlocObserver();
runApp(BeerApp());
}
class BeerApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Beer App',
theme: ThemeData.dark(),
initialRoute: '/',
routes: {
'/': (context) => DisplayBeerScreen(),
},
);
}
}
class BeerBlocObserver extends BlocObserver {
@override
void onEvent(Bloc bloc, Object event) {
print(event);
super.onEvent(bloc, event);
}
@override
void onChange(Cubit cubit, Change change) {
print(change);
super.onChange(cubit, change);
}
@override
void onTransition(Bloc bloc, Transition transition) {
print(transition);
super.onTransition(bloc, transition);
}
@override
void onError(Cubit cubit, Object error, StackTrace stackTrace) {
print('$error, $stackTrace');
super.onError(cubit, error, stackTrace);
}
}
view raw main.dart hosted with ❤ by GitHub
  • I’ve organized the project files feature-wise. Here, we have got only a single feature i.e. display freshly brewed beers.

Directory Structure

🎹 Coding

  • Let’s design our BeerRepository. It is going to be a singleton. It contains a method getBeers which requires a page number. Page number will be passed from BeerBloc. I have set the _perPage limit to 10.
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
class BeerRepository {
static final BeerRepository _beerRepository = BeerRepository._();
static const int _perPage = 10;
BeerRepository._();
factory BeerRepository() {
return _beerRepository;
}
Future<dynamic> getBeers({
@required int page,
}) async {
try {
return await http.get(
'https://api.punkapi.com/v2/beers?page=$page&per_page=$_perPage',
);
} catch (e) {
return e.toString();
}
}
}
  • We’ll be creating a model that will map the API response to Dart class (model) using the fromJson method. The model has 5 fields: id, name, tagline, description, and imageUrl. Don’t forget to run flutter pub run build_runner build command to generate the serialization/deserialization code.
import 'package:flutter/foundation.dart';
import 'package:json_annotation/json_annotation.dart';
part 'beer_model.g.dart';
@JsonSerializable()
class BeerModel {
int id;
String name;
String tagline;
String description;
@JsonKey(name: 'image_url')
String imageUrl;
BeerModel({
@required this.id,
@required this.name,
@required this.tagline,
@required this.description,
@required this.imageUrl,
});
factory BeerModel.fromJson(Map<String, dynamic> json) =>
_$BeerModelFromJson(json);
}
view raw beer_model.dart hosted with ❤ by GitHub
  • Now let’s create business logic component related files & classes: BeerBloc , BeerState , and BeerEvent.
  • There will be 4 states: Initial , Loading , Success & Error. These are self-explanatory.
import 'package:beer_app/feature_display_beer/models/beer_model.dart';
import 'package:flutter/foundation.dart';
abstract class BeerState {
const BeerState();
}
class BeerInitialState extends BeerState {
const BeerInitialState();
}
class BeerLoadingState extends BeerState {
final String message;
const BeerLoadingState({
@required this.message,
});
}
class BeerSuccessState extends BeerState {
final List<BeerModel> beers;
const BeerSuccessState({
@required this.beers,
});
}
class BeerErrorState extends BeerState {
final String error;
const BeerErrorState({
@required this.error,
});
}
view raw beer_state.dart hosted with ❤ by GitHub
  • There will be 1 event: BeerFetchEvent.
abstract class BeerEvent {
const BeerEvent();
}
class BeerFetchEvent extends BeerEvent {
const BeerFetchEvent();
}
view raw beer_event.dart hosted with ❤ by GitHub
  • Now, let’s design BeerBloc. It will handle BeerFetchEvent and yield an appropriate state to the UI. It’ll maintain the current page number and isFetching boolean flag to prevent duplicate event requests. BeerRepository is injected via BeerBloc constructor. The value of the page is incremented by 1 after the Success state is yielded.
  • We’ll be yielding BeerInitialState as the Initial State of the UI.
  • When BeerFetchEvent is delegated to the BeerBloc , first it’ll yield the BeerLoadingState. Then, it’ll call the getBeers method of BeerRepository.
  • If the return type of response is of type http.Response then status code of the response is checked. If it’s OK , then we’ll parse the JSON using jsonDecode from dart:convert , and map individual objects to BeerModel. Now, these results can be passed to UI by yielding BeerSuccessState.
  • Otherwise, BeerErrorState is yielded to UI to notify about errors that occurred during API call.
import 'dart:convert';
import 'package:beer_app/feature_display_beer/bloc/beer_event.dart';
import 'package:beer_app/feature_display_beer/bloc/beer_state.dart';
import 'package:beer_app/feature_display_beer/models/beer_model.dart';
import 'package:beer_app/feature_display_beer/repository/beer_repository.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:http/http.dart' as http;
class BeerBloc extends Bloc<BeerEvent, BeerState> {
final BeerRepository beerRepository;
int page = 1;
bool isFetching = false;
BeerBloc({
@required this.beerRepository,
}) : super(BeerInitialState());
@override
Stream<BeerState> mapEventToState(BeerEvent event) async* {
if (event is BeerFetchEvent) {
yield BeerLoadingState(message: 'Loading Beers');
final response = await beerRepository.getBeers(page: page);
if (response is http.Response) {
if (response.statusCode == 200) {
final beers = jsonDecode(response.body) as List;
yield BeerSuccessState(
beers: beers.map((beer) => BeerModel.fromJson(beer)).toList(),
);
page++;
} else {
yield BeerErrorState(error: response.body);
}
} else if (response is String) {
yield BeerErrorState(error: response);
}
}
}
}
view raw beer_bloc.dart hosted with ❤ by GitHub
  • At last, comes the UI building part. I’ve kept it clean and simple. There are 3 widgets that compose the entire UI: DisplayBeerScreen, BeerBody, and BeerListItem.
  • DisplayBeerScreen widget displays an AppBar , injects BeerBloc instance via BlocProvider , and renders BeerBody widget.
import 'package:beer_app/feature_display_beer/bloc/beer_bloc.dart';
import 'package:beer_app/feature_display_beer/bloc/beer_event.dart';
import 'package:beer_app/feature_display_beer/repository/beer_repository.dart';
import 'package:beer_app/feature_display_beer/widgets/beer_body.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class DisplayBeerScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => BeerBloc(
beerRepository: BeerRepository(),
)..add(BeerFetchEvent()),
child: Scaffold(
appBar: AppBar(
title: Text('Beers \u{1F37A}'),
),
body: BeerBody(),
),
);
}
}
  • BeerBody widget returns a BlocConsumer to build the reactive UI. BeerBody has 2 fields: a list to hold BeerModel s (_beers), and a ScrollController.
  • listener callback of BlocConsumer conditionally displays the appropriate message using a SnackBar.
  • builder callback of BlocConsumer conditionally builds the widgets. Let’s understand that logic:

Case (1): If the current state is either initial or loading and the value of _beers is empty. In this case, a progress bar is shown. This condition only occurs when the user opens the app for the first time.

Case (2): If the current state is an error and the value of _beers is empty. In this case, IconButton with retry action is shown. This condition only occurs when the user opens the app for the first time and there is some error due to the internet or any other exception. If the user presses the retry then the value of isFetching field of BeerBloc is set to false.

Case (3): If the current state is a success. In this case, beers (API response) yielded by the BLoC are added to the _beers list & isFetching field of BeerBloc is set to false.

  • By default, the builder returns a ListView to render the _beers using the BeerListItem widget. ListView ’s controller is set to a ScrollController instance. This controller has a listener which adds a BeerFetchEvent to BeerBloc (to get the response from the next page of the API) if the user has reached the end of the list-view. It also sets isFetching field of BeerBloc is set to true to prevent duplicate event requests.
import 'package:beer_app/feature_display_beer/bloc/beer_bloc.dart';
import 'package:beer_app/feature_display_beer/bloc/beer_event.dart';
import 'package:beer_app/feature_display_beer/bloc/beer_state.dart';
import 'package:beer_app/feature_display_beer/models/beer_model.dart';
import 'package:beer_app/feature_display_beer/widgets/beer_list_item.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class BeerBody extends StatelessWidget {
final List<BeerModel> _beers = [];
final ScrollController _scrollController = ScrollController();
@override
Widget build(BuildContext context) {
return Center(
child: BlocConsumer<BeerBloc, BeerState>(
listener: (context, beerState) {
if (beerState is BeerLoadingState) {
Scaffold.of(context)
.showSnackBar(SnackBar(content: Text(beerState.message)));
} else if (beerState is BeerSuccessState && beerState.beers.isEmpty) {
Scaffold.of(context)
.showSnackBar(SnackBar(content: Text('No more beers')));
} else if (beerState is BeerErrorState) {
Scaffold.of(context)
.showSnackBar(SnackBar(content: Text(beerState.error)));
context.bloc<BeerBloc>().isFetching = false;
}
return;
},
builder: (context, beerState) {
if (beerState is BeerInitialState ||
beerState is BeerLoadingState && _beers.isEmpty) {
return CircularProgressIndicator();
} else if (beerState is BeerSuccessState) {
_beers.addAll(beerState.beers);
context.bloc<BeerBloc>().isFetching = false;
Scaffold.of(context).hideCurrentSnackBar();
} else if (beerState is BeerErrorState && _beers.isEmpty) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
onPressed: () {
context.bloc<BeerBloc>()
..isFetching = true
..add(BeerFetchEvent());
},
icon: Icon(Icons.refresh),
),
const SizedBox(height: 15),
Text(beerState.error, textAlign: TextAlign.center),
],
);
}
return ListView.separated(
controller: _scrollController
..addListener(() {
if (_scrollController.offset ==
_scrollController.position.maxScrollExtent &&
!context.bloc<BeerBloc>().isFetching) {
context.bloc<BeerBloc>()
..isFetching = true
..add(BeerFetchEvent());
}
}),
itemBuilder: (context, index) => BeerListItem(_beers[index]),
separatorBuilder: (context, index) => const SizedBox(height: 20),
itemCount: _beers.length,
);
},
),
);
}
}
view raw beer_body.dart hosted with ❤ by GitHub
  • Finally, there is the BeetListItem widget which shows individual BeerModel data using ExpansionTile , Text , Image.network, and some SizedBox es.
import 'package:beer_app/feature_display_beer/models/beer_model.dart';
import 'package:flutter/material.dart';
class BeerListItem extends StatelessWidget {
final BeerModel beer;
const BeerListItem(this.beer);
@override
Widget build(BuildContext context) {
return ExpansionTile(
title: Text(beer.name),
subtitle: Text(beer.tagline),
childrenPadding: const EdgeInsets.all(16),
leading: Container(
margin: EdgeInsets.only(top: 8),
child: Text(beer.id.toString()),
),
children: [
Text(
beer.description,
textAlign: TextAlign.justify,
),
const SizedBox(height: 20),
beer?.imageUrl == null
? Container()
: Image.network(
beer.imageUrl,
loadingBuilder: (context, widget, imageChunkEvent) {
return imageChunkEvent == null
? widget
: CircularProgressIndicator();
},
height: 300,
),
],
);
}
}

That’s it for this one. Thank you for reading this 💙

You can find the complete source code of this app in this repository.

piedcipher/beer-app

If you find this post useful, press👏 button as many times as you can and share this post with others. You can leave your feedback/suggestions in the comments 💬 below.

For any other issues feel free to reach out to me via Twitter: https://twitter.com/piedcipher

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read full post →

Top comments (0)

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up