Flutter Bloc (V8): How to fetch data from an API?

In this guide, we will learn how to fetch the data from an API with the help of state management Bloc in a flutter. for this, I am going to use this. It is a REST API that offers 6 common resources i.e posts, comments, albums, photos, todos, user in a consistent and well-formatted manner.

Before driving to these guides At first, we should know some basic architecture of what bloc is in state management for these we will prefer to go to these guides Flutter Bloc: A Complete Guide So, if we haven’t checked it out make sure you check it out first. Or if you have a basic understanding of BLoC you can start right away.

Folder Structure

  • As you can see in the lib folder, there are:
  • bloc folder: Responsible for managing the business logic
  • data folder: It has two folders model (Responsible for creating data model classes) and repositories (Responsible for making and manipulating the data).
  • presentation folder: Responsible for UI design

Dependencies Installation

  • We need two packages:
  • flutter_blochttp Let’s add them to the dependencies.
dependencies:
  cupertino_icons: ^1.0.2
  flutter:
    sdk: flutter
  flutter_bloc: ^8.0.1
  http: ^0.13.4

Creating Model Class

We are using this API to get the Posts.


class Post {
  final int id;
  final String title;
  final String body;
  final int userId;
  Post({
    required this.id,
    required this.title,
    required this.body,
    required this.userId,
  });

  factory Post.fromJson(Map<String, dynamic> map) {
    return Post(
      id: map['id'] as int,
      title: map['title'] as String,
      body: map['body'] as String,
      userId: map['userId'] as int,
    );
  }
}

Fetching Posts From API

  • We’ll use the http package to get the API from the internet.
  • And this will take place in the Service folder.
  • Create a file called data_repository.dart in the lib/data/Service folder and paste the code below into it.

import 'package:http/http.dart' as http;

class DataService {
  Future<http.Response?> getPosts() async {
    const _baseUrl = 'jsonplaceholder.typicode.com';
    try {
      final url = Uri.https(_baseUrl, '/posts');
      final response = await http.get(url);
      return response;
    } catch (e) {
      rethrow;
    }
  }
}

To check the API response status we have created another file api_repository inside the Repository which has the following line of code.

// ignore_for_file: public_member_api_docs, sort_constructors_first
import 'dart:convert';


import 'package:bloc_api/Service/data_service.dart';
import 'package:bloc_api/data/Model/post.dart';

class ApiRepository {
  final DataService dataService;
  ApiRepository({
    required this.dataService,
  });

  Future<List<Post>> getPostsList() async {
    final response = await dataService.getPosts();
    if (response!.statusCode == 200) {
      final json = jsonDecode(response.body) as List;
      final posts = json.map((e) => Post.fromJson(e)).toList();
      return posts;
    } else {
      throw Exception('Failed to load Posts');
    }
  }
}

Building Bloc

States

  • There will be just three states in our case: LoadingStateLoadedState, and FailureLoadState.
  • When the Posts are presently being fetched, the LoadingState is utilized to display the Progress Indicator.
  • A state with the PostModel is the LoadedState. It will provide the post data to the user interface.
  • If any errors happen during the fetching, the FailureLoadState will return an Error message.
  • Let’s implement it in lib\Bloc\Post\bloc\post_state.dart
// ignore_for_file: public_member_api_docs, sort_constructors_first
part of 'post_bloc.dart';

@immutable
abstract class PostState {}

class PostInitial extends PostState {}

class LoadingState extends PostState {}

class LoadedState extends PostState {
  final List<Post> posts;
  LoadedState({
    required this.posts,
  });
}

class FailureLoadState extends PostState {
  final String message;
  FailureLoadState({
    required this.message,
  });
}

Events

  • Events are nothing but different actions (button click, submit, etc) triggered by the user from UI. It contains information about the action and gives it to the Bloc to handle.
  • In our case, we only have two events one is LoadEvent and another is PullToRefreshEvent
  • So let’s define it inside lib\Bloc\Post\bloc\post_event.dart
part of 'post_bloc.dart';

@immutable
abstract class PostEvent {}

class LoadEvent extends PostEvent {}

class PullToRefreshEvent extends PostEvent {}

Bloc

  • It acts as a middle man between UI and Data layer, Bloc takes an event triggered by the user (ex: LoadNewJoke button press, Submit form button press, etc) as an input, and responds back to the UI with the relevant state
import 'package:bloc/bloc.dart';

import 'package:bloc_api/data/Model/post.dart';
import 'package:bloc_api/data/Repository/api_repository.dart';
import 'package:meta/meta.dart';

part 'post_event.dart';
part 'post_state.dart';

class PostBloc extends Bloc<PostEvent, PostState> {
  final ApiRepository apiRepository;
  PostBloc({required this.apiRepository}) : super(PostInitial()) {
    on<PostEvent>((event, emit) async {
      if (event is LoadEvent || event is PullToRefreshEvent) {
        emit(LoadingState());
        try {
          final posts = await apiRepository.getPostsList();
          emit(LoadedState(posts: posts));
        } catch (e) {
          emit(FailureLoadState(message: e.toString()));
        }
      } else {}
    });
  }
}

Providing Bloc

  • BlocProvider widget provides a bloc to its children (i.e Widgets).
  • BlocProvider is used as dependency injection (DI) widget so that a single instance of a bloc can be provided to multiple widgets within a subtree.
  • Let’s wrap our main.dart around BlocProvider
import 'package:bloc_api/Bloc/Post/bloc/post_bloc.dart';

import 'package:bloc_api/Service/data_service.dart';
import 'package:bloc_api/data/Repository/api_repository.dart';
import 'package:bloc_api/presentation/screen/home_screen.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

void main() {
  runApp(MyApp(
    dataService: DataService(),
  ));
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key, required this.dataService}) : super(key: key);
  final DataService dataService;
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'Flutter Bloc Api',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: BlocProvider(
        create: (context) =>
            PostBloc(apiRepository: ApiRepository(dataService: dataService))
              ..add(LoadEvent()),
        child: const HomeScreen(),
      ),
    );
  }
}
  • As we can see, we used cascading to add the LoadEvent. The initial state will be LoadEvent, and the joke data will be displayed on the screen as soon as the screen loads.

Rendering Widgets Based on the State – BlocBuilder

  • Now it’s time to render the widget in accordance with the Bloc’s state.
  • To do this, we must wrap our Home screen body around BlocBuilder.
  • Now we need to show the CircularProgressIndicator if the post is still loading, or the actual post data after it has been obtained. Finally, the error that happened during data retrieval.
  • So, let’s render all of the widgets based on their state.
  body: BlocBuilder<PostBloc, PostState>(
        builder: (context, state) {
          if (state is LoadedState) {
            return RefreshIndicator(
              onRefresh: () async {
                context.read<PostBloc>().add(PullToRefreshEvent());
              },
              child: ListView.builder(
                  itemBuilder: (context, index) {
                    final posts = state.posts[index];
                    return Card(
                      margin: const EdgeInsets.all(8),
                      child: ListTile(
                        leading: CircleAvatar(
                          child: Text(posts.id.toString()),
                        ),
                        title: Text(posts.title),
                        subtitle: Text(posts.body),
                      ),
                    );
                  },
                  itemCount: state.posts.length),
            );
          }
          return const Center(
            child: CircularProgressIndicator(),
          );
        },
      ),

Meanwhile, when we run this application in the emulator or device, we should get the UI similar to the screenshot below:

For the Code Snippet Download from here