Example BloC pattern with tests and Provider and persistent datastore

awaik
6 min readFeb 7, 2020

This article is the continuation of the “Bloc pattern for Flutter on the classic counter app” where we learned what this pattern is and how to use it in the very easy app.

According to the comments and for my best understanding, I decided to write an application in which answer to the questions:

1. How to transfer the state of the class in which the BloC is located throughout the application
2. How to write tests for this pattern
3. (additional question) How to maintain data state between application launches while the code logic remaining within the BLoC pattern

Below is the gif of the resulting example. And at the end of the article, an interesting problem: how to modify the application for applying the Debounce statement from ReactiveX pattern (more precisely, reactiveX is an extension of the Observer pattern)

Description of application and base code

Not related to BLoC and Provider

1. The application has buttons + — and swipe that duplicate these buttons work
2. Animation made through the built-in flutter mixin — TickerProviderStateMixin

Linked to BLoC and Provider

1. Two screens — on the first pile up, on the second the counter changes are displayed
2. We write the state to the permanent storage of the phone (iOS & Android, package https://pub.dev/packages/shared_preferences)
3. Writing and reading information from the permanent storage is asynchronous, we also do it through BLoC

Create an application

As follows from the definition of the BLoC pattern, our task is to remove all logic from widgets and work with data through a class in which all inputs and outputs are Streams.

At the same time, since the class in which BLoC is located is used on different screens, we need to transfer the object created from this class throughout the application.

There are different methods for this, namely:

1. Transfer through class constructors, the so-called lifting state up. We will not use it since it turns out to be very confusing. And later, it is not possible to track the state transfers.
2. Make a singleton from the class where we have BLoC and import this class whenever we need it. It is simple and convenient, but, from my purely personal point of view, complicates the class constructor and confuses the logic a bit.
3. Use the Provider package — which is recommended by the Flutter team for state management. See more in the video from Google.

In this example, we will use the Provider.

The app structure

So we create the BLoC class

class SwipesBloc {
// some stuff
}

and create the object from this class that will be accessible throughout the widget tree. For this, we, at a certain level of application widgets, define a provider from this class. I did this at the very top of the widget tree, but it’s best to do it at the lowest possible level.

void main() async {
runApp(MyApp());
}

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider<SwipesBloc>(
create: (_) => SwipesBloc(),
dispose: (_, SwipesBloc swipesBloc) => swipesBloc.dispose(),
),
],
child: MaterialApp(
title: 'Swipe BLoC + Provider',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: SwipeScreen(),
routes: <String, WidgetBuilder>{
SwipeScreen.id: (BuildContext context) => SwipeScreen(),
CounterData.id: (BuildContext context) => CounterData(),
},
),
);
}
}

After adding this beautiful code we have access to the class objects in any widget with all the data is available to us. Details on how to work with the Provider are here.

Next, we need to make sure that when you click on the button or swipe all the data is transferred to Stream and, then, on any other screens, the data is updated from the same Stream.

Class for BLoC

To do this, we create a BLoC class in which we describe not only the streams but also the receipt and recording of status from the phone’s permanent storage.

import 'dart:async';
import 'package:rxdart/rxdart.dart';
import 'package:shared_preferences/shared_preferences.dart';

class SwipesBloc {
Future<SharedPreferences> prefs = SharedPreferences.getInstance();
int _counter;

SwipesBloc() {
prefs.then((val) {
if (val.get('count') != null) {
_counter = val.getInt('count') ?? 1;
} else {
_counter = 1;
}
_actionController.stream.listen(_changeStream);
_addValue.add(_counter);
});
}

final _counterStream = BehaviorSubject<int>.seeded(1);
Stream get pressedCount => _counterStream.stream;
Sink get _addValue => _counterStream.sink;

StreamController _actionController = StreamController();
void get resetCount => _actionController.sink.add(null);
StreamSink get incrementCounter => _actionController.sink;

void _changeStream(data) async {
if (data == null) {
_counter = 1;
} else {
_counter = _counter + data;
}
_addValue.add(_counter);
prefs.then((val) {
val.setInt('count', _counter);
});
}

void dispose() {
_counterStream.close();
_actionController.close();
}
}

If we carefully look at this class, we will see that:

1. Any properties available externally — inputs and outputs in Streams.
2. In the designer, at the first start, we try to get data from the phone’s permanent storage.
3. Conveniently recorded in the permanent storage of the phone

Small exercises for better understanding:

- To take out a piece of code from the constructor with .then — it is more beautiful to make a separate method.
- Try to implement this class without a provider as Singleton

Receive and transmit data in the application

Now we need to transfer data to Stream when clicking buttons or swipe and get this data on the card and on a separate screen.

There are different options for how to do this, I chose the classic one, we wrap those parts of the tree where you need to receive / transfer data to Consumer

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:test_swipes/src/bloc/swipes/swipes_bloc.dart';

class CounterData extends StatefulWidget {
static String id = 'counter_data';
@override
_CounterDataState createState() => _CounterDataState();
}

class _CounterDataState extends State<CounterData> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Counter data screen'),
backgroundColor: Color(0xFF3D9098),
),
body: Center(
child: Consumer<SwipesBloc>(
builder: (context, _swipesBloc, child) {
return StreamBuilder<int>(
stream: _swipesBloc.pressedCount,
builder: (context, snapshot) {
String counterValue = snapshot.data.toString();
return Text(counterValue);
},
);
},
),
),
);
}
}

And receiving data from BLoC
_swipesBloc.pressedCount,

And transfer data to BLoC
_swipesBloc.incrementCounter.add(1);

That’s it! We got clear and expandable code with BLoC patterns rules.

[Fully working app here](https://github.com/awaik/swipe_bloc_flutter)

Tests

We can test widgets, we can make mocks, we can use e2e.

We will test the widgets and run the application to check how the counter increase worked. Information on the tests is here.

Testing widgets

If we have synchronous data, then we could test everything with widgets. In our case, we can only check how the widgets were created and how the initialization went.

The code is here, in the code, there are attempts to check the counter increase after clicking — it gives an error because the data goes through BLoC.

To run the test, use the command
flutter test

Integration tests

In this test option, the application runs on the emulator and we can press buttons, swipe and check what happened as a result.

To do this, we create 2 files:

test_driver / app.dart
test_driver / app_test.dart

In first, we connect what is needed, and in the second directly tests. For example, I did the checks:

  • The initial state
    - Increase the counter after pressing the button
import 'package:flutter_driver/flutter_driver.dart';
import 'package:test/test.dart';

void main() {
group(
'park-flutter app',
() {
final counterTextFinder = find.byValueKey('counterKey');
final buttonFinder = find.byValueKey('incrementPlusButton');

FlutterDriver driver;
setUpAll(() async {
driver = await FlutterDriver.connect();
});

tearDownAll(() async {
if (driver != null) {
driver.close();
}
});

test('test init value', () async {
expect(await driver.getText(counterTextFinder), "1");
});

test('test + 1 value after tapped', () async {
await driver.tap(buttonFinder);
// Then, verify the counter text is incremented by 1.
expect(await driver.getText(counterTextFinder), "2");
});
},
);
}

[The code is here](https://github.com/awaik/swipe_bloc_flutter)

For run test use
flutter drive — target=test_driver/app.dart

Exercise

Just to deepen our understanding.

In modern applications (sites), the Debounce function from ReactiveX is often used.

For instance:

1. A word is entered in the search bar and a hint falls out only when the gap between the set of letters is more than 2 seconds
2. When likes are put, you can click 10 times per second — writing to the database will occur if the gap in the clicks was more than 2–3 seconds
3. … etc.

Exercise: make the digit change only if more than 2 seconds elapse between pressing + or -. To do this, edit only the BLoC class, the rest of the code should remain the same.

Full source code https://github.com/awaik/swipe_bloc_flutter

--

--