Flutter: an example of a medical app to classify arterial blood pressure

Francesco Mineo
7 min readDec 15, 2018

After the first article about a Child-Pugh score calculator, today I’m going to show you an example of app who analyzes the arterial blood pressure and give as a result the blood pressure classification, the mean arterial pressure (MAP) and the pulse pressure.

The app consists of two pages: in the main page will be the input fields for the systolic and the diastolic pressure, the button e the box for the result; another page will be used to show the history of the values examined. Using the streams and the BLoC pattern, both the pages will be subclasses of the StatelessWidget class.

Step 1 — Model

Let’s begin with the model. We define a class BloodPressure with four properties of type double for the blood pressure parameters and one of type Classification for the classification. I override the toString method so I can print the instance and easily see the result on the debug console.

To handle the classifications to me is useful to use a map where I assign to every classification (key) a related map (value). Every one of these maps will be a map with as key a string and for value a dynamic type: a string for the classification and a Color for the color of the text. We are going to use this map for the result widget of the main page and for the history page: in this way we can assign to every classification a specific color (we could also add for example a key/value for the font size, text style and so on).

2 BLoC

As in the other articles, all the business logic of the app will be in a BLoC class. We will have:

  • Four RxDart’s BehaviorSubject streams: systolic pressure, diastolic, one for the result and last one for the history page.
  • A stream of type bool to handle the button state.
  • Validator methods using a mixin class.
  • A method (examine) to calculate the MAP, the pulse pressure and to classify the result.

2.1 Systolic pressure

To handle the systolic pressure we define a BehaviorSubject stream of type String, a function inSystolic to send the systolic value to stream, and a getter outSystolic to pass the stream to the StreamBuilder widget in the view.

To validate the value inserted in the text field, we pass to the transform method a StreamTransformer to manipulate the stream (we have to avoid that the user could insert a text, a negative number or an out of range value).

double systolic = 120.0;
final _systolic = BehaviorSubject<String>();
Function(String) get inSystolic => _systolic.sink.add;
Stream<double> get outSystolic =>
_systolic.stream.transform(validateSystolic).doOnData((value) {
//
// After validating the systolic pressure store it
// for the examine function
systolic = value;
});

In the handleDatahandler, the input event (it’s a String) is parsed and used to check if the value inserted by the user is correct. If the user inserts a string or a value that it isn’t in the chosen range (this value is arbitrary, the systolic could be even greater than 300 mmHg but it covers the vast majority of the cases), then an error event is sent to stream and an error message appears. Once validated, the value through the doOnData method it is assigned to a variable of type double to use it for the calculations in the examine method.

2.2 Diastolic pressure

For the diastolic pressure, we need to check even another condition. First, this must be valid and lesser than 200 mmHg, secondly, it must be lesser than the systolic one. After it passes the validation, we need to check it against the systolic pressure and add an error to stream if the condition (diastolic < systolic) is not met. The validated value is then stored in the diastolic variable.

double diastolic = 80.0;
final _diastolic = BehaviorSubject<String>();
Function(String) get inDiastolic => _diastolic.sink.add;
Stream<double> get outDiastolic =>
_diastolic.stream.transform(validateDiastolic).doOnData((value) {
if (value >= double.tryParse(_systolic.value)) {
_diastolic.addError(“Diastolic must be greater than systolic”);
}
//
// If the diastolic is < than systolic then store it for using it
// in the examine function
//
else
diastolic = value;
});

2.3 CombineLatest2

We need a way to make the button disabled when the input fields are empty or invalid, to do so we use the combineLatest2 method that emits an event only if both streams we passed in as a parameter emitted at least one event.

//both field filled?
Stream<bool> get isComplete =>
Observable.combineLatest2(outDiastolic, outSystolic, (d, s) => true);

In the view the stream is passed to the StreamBuilder through the stream getter isComplete and the snapshot passed to the onPressed handler of the button, will be true, enabling the button, once the users fill both the input fields (and will be valid).

2.4 Examine method

When a user clicks on the examine button the examinemethod of the BloodPressureBloc class is called. First, the mean arterial pressure (MAP) and pulse pressure are calculated. Then, with nested if-else conditions, to the values inserted by the user is assigned the related classification, the result is sent to result stream and added to a list of type BloodPressure. Finally, this list is sent to the history stream to be consumed by the history page.

3 — Main page

3.1 Action button

In the appBar, we add an action button to navigate to the history page. Through the onPressed handler of the IconButton, we call the Navigator.push method to push the page to the stack. The bloc is passed to the history page through the constructor.

3.2 Result widget

The result widget is just a Container with the rounded border with as a child a Column widget with five rows inside. It is wrapped in a StreamBuilder who takes as a parameter the getter for the result stream: if the property hasData of the snapshot is not true, then is checked if the property hasError is true, in that case, the error message is shown; otherwise, it appears a message who tells the user to fill in the form.

To the _buildClassificationRow is passed the classification property of the BloodPressure class emitted by the stream result and used to assign through the map created before, the related classification string and the correct color.

3.3 Input fields and examine button

Both the text fields are wrapped in a StreamBuilder. To the errorText property is passed the snapshot.error in case the validation rules aren’t met and an error message is sent to stream. Through the inSystolic/inDiastolic function passed to the onChangedhandler, the value inserted by the user is sent to stream.

As said before the button is passed the inComplete stream that emits the event (true) in that case both the input fields are filled and validated. Finally, the onPressed handler is passed the examine method to make the calculations and the classification.

4 — History page

The history page, like the main page, is created by extending a Stateless widget. A BloodPressureBloc is declared as final and flagged as a required parameter in the constructor of the class. We need it to access the instance of the bloc and its history stream.

class BloodPressureListPage extends StatelessWidget {BloodPressureListPage({Key key, @required this.bloc})
: super(key: key);
final BloodPressureBloc bloc;

In the examinemethod of the bloc, the result is added to a list and sent to the history stream of type List<BloodPressure>. Then the stream is used, through a StreamBuilder, to build a ListView showing all the values examined.

5 — Widget test

Why don’t test the widget if all is working? Let’s create a bloodpressure_home_test.dart file in the test folder of the project.

  • Step 1: import all the needed files.
  • Step 2: create a bloc instance to pass to the provider
  • Step 3: by the WidgetTester let’s load the page
  • Step 4: pass to the find.byKey method the key of the widget to obtain a reference to it. Now with the enterText method we insert the systolic and diastolic values and call the pump method to refresh the widget (otherwise the button won’t be active).
  • Step 5: the reference to the button is passed to the tap method to simulate the click of the button. We call the pumpmethod again to refresh the widget showing the result.
  • Step 6: we pass the expected systolic value to the find.text method and tell to the expect function we expect to find a widget (and only one) with this string. The same for the diastolic and the classification.
  • Step 7: run the test by clicking the Run just above the test in VS code (or by writing in the terminal “flutter test filename.dart”). If all is working all the tests should pass the test.

Conclusion

This journey into Flutter is opening me a nice way to build nice apps in really no time. It is really an amazing tool and the way you can use its widgets let you the freedom to compose every view in such a painless way that it reminds you the way you use HTML frameworks like Bootstrap and others. As always, for any suggestion or error, you can contact me. In the next article, I’ll try to add some animations to the widget result (using the flutter staggered animations) to make the app a little bit more “dynamic”. In the meanwhile, I hope you like this article and find it useful.

--

--

Francesco Mineo

Medical doctor, singer & music producer , software developer.