How to use BLOC to update favorites?

Reading time: 3 minutes

In this example, we will partly reuse our REST code. This time we will add one more “layer” for the repository class, it will look like this:

import 'countries_rest.dart';
import 'country.dart';

class CountriesRepository {
  final rest = CountriesRest();
  final favorites = Set<String>();
  final countries = List<Country>();

  Future<List<Country>> getCountries() async {
    if (countries.isEmpty) countries.addAll(await rest.fetchCountries());
    return countries;
  }
}

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

class CountriesRest {
  Future<List<Country>> fetchCountries() async {
    final response = await http.get('https://restcountries.eu/rest/v2/all');
    if (response.statusCode == 200) {
      return (json.decode(response.body) as List<dynamic>).map((e) => Country.fromJson(e)).toList();
    } else {
      throw Exception('Failed to load countries');
    }
  }
}

The repository will be a high-level component, while CountriesRest will be responsible for HTTP calls, we can use any other provider if we want. We will “cache” our countries list and hold a list of favorites.

Country class receives favorite field and looks like this:

class Country {
  final String name;
  final String capital;
  bool favorite = false;

  Country({this.name, this.capital});

  factory Country.fromJson(Map<String, dynamic> json) {
    return Country(
      name: json["name"],
      capital: json["capital"],
    );
  }
}

Now let’s see how BLOC logic works.

import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'countries_repository.dart';
import 'country.dart';

class CountriesBloc extends Bloc<CountriesEvent, CountriesState> {
  CountriesBloc({this.repository}) : super(CountriesLoading());

  final CountriesRepository repository;

  @override
  Stream<CountriesState> mapEventToState(CountriesEvent event) async* {
    if (event is LoadCountriesEvent)
      yield* _loadCountries();
    else if (event is UpdateFavoriteCountriesEvent) yield* _updateFavoritesCountries(event);
  }

  Stream<CountriesState> _loadCountries() async* {
    yield CountriesLoading();
    try {
      var list = await repository.getCountries();
      list.forEach((element) {
        element.favorite = repository.favorites.contains(element.name);
      });
      list.sort((a, b) {
        if (a.favorite && !b.favorite) return -1;
        if (!a.favorite && b.favorite) return 1;
        return 0;
      });
      yield CountriesLoaded(countries: list);
    } catch (e) {
      yield CountriesFailed();
    }
  }

  Stream<CountriesState> _updateFavoritesCountries(UpdateFavoriteCountriesEvent event) async* {
    if (event.favorite)
      repository.favorites.add(event.name);
    else
      repository.favorites.remove(event.name);
    yield* _loadCountries();
  }
}

abstract class CountriesEvent extends Equatable {
  const CountriesEvent();
}

class LoadCountriesEvent extends CountriesEvent {
  @override
  List<Object> get props => [];
}

class UpdateFavoriteCountriesEvent extends CountriesEvent {
  final String name;
  final bool favorite;

  UpdateFavoriteCountriesEvent({this.name, this.favorite});

  @override
  List<Object> get props => [this.name, this.favorite];
}

abstract class CountriesState extends Equatable {
  const CountriesState();
}

class CountriesLoading extends CountriesState {
  @override
  List<Object> get props => [];
}

class CountriesFailed extends CountriesState {
  @override
  List<Object> get props => [];
}

class CountriesLoaded extends CountriesState {
  final List<Country> countries;

  CountriesLoaded({this.countries});

  @override
  List<Object> get props => [countries];
}

We added a new event that takes a country name and new favorite state, _updateFavoritesCountries function updates favorites set in our repository and calls _loadCountries where we modify the list of countries and sort it according to favorite set.

And finally we need some icon to tap on and represent favorite state:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'countries_bloc.dart';
import 'countries_repository.dart';

class BlocFavoriteExample extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: BlocProvider(
            create: (context) => CountriesBloc(repository: CountriesRepository())..add(LoadCountriesEvent()),
            child: BlocFavoritePage()));
  }
}

class BlocFavoritePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<CountriesBloc, CountriesState>(builder: (context, state) {
      if (state is CountriesLoaded) {
        return ListView(
            children: state.countries
                .map((c) => ListTile(
                      title: Text("${c.name}"),
                      subtitle: Text("${c.capital}"),
                      trailing: InkWell(
                          onTap: () {
                            BlocProvider.of<CountriesBloc>(context)
                                .add(UpdateFavoriteCountriesEvent(name: c.name, favorite: !c.favorite));
                          },
                          child: c.favorite ? Icon(Icons.star) : Icon(Icons.star_border)),
                    ))
                .toList());
      } else
        return Center(
          child: CircularProgressIndicator(),
        );
    });
  }
}

We show a “star” icon border or whole star depending on the country being favorite or not, and send an event to update state to opposite.

This is how it looks:

Leave a Reply

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