Foreword
Due to the rising popularity of Flutter I feel it’s a good time to review some of the aspects of keeping your code clean and maintainable.
One of the things that developers might not pay so much attention to while building apps is error handling. While it might not be so glamorous it is definitely a key part of your application.
Who wants to use an app which looks unresponsive; having confusing error messages; or downright crashing in every step? With this article I’d like to give some good pointers on how to deal with error handling in flutter.
Set up
Let’s start by making a simple application. (If you want to skip right to the meaty part then check out the “handling errors” section.)
I will be using the test drive app as a base (https://flutter.dev/docs/get-started/test-drive#create-app) and start building it from there.
As you can see the app doesn’t do much at the moment. Let’s make a bit more exciting by trying to build a simple screen where you enter your phone number and it returns a one time password(OTP)
Let’s start by creating a number input field with a submit button.
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
...
class _MyHomePageState extends State<MyHomePage> {
String _phoneNumber;
void getOneTimePassword(){}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Container(
child: TextField(
onChanged: (phoneNumber) {
setState(() {
_phoneNumber = phoneNumber;
});
},
decoration: InputDecoration(hintText: 'Enter a phone number'),
inputFormatters: [WhitelistingTextInputFormatter.digitsOnly],
keyboardType: TextInputType.number),
width: MediaQuery.of(context).size.width * 0.5,
),
RaisedButton(
onPressed: () {getOneTimePassword();},
child: Text('Get Code'),
),
],
),
),
);
}
}
This looks better. But it still doesn’t have any functionality yet. Let’s change that.
Adding more functionality
We will create a OneTimePasswordService and a mock HttpClient that servers our requests. Oh and we also need a response object that will parse the json string we get from the mock client. Let’s create the following 2 files.
import 'dart:math';
import 'otp_response.dart';
class MockHttpClient {
var randomGenerator = new Random();
Future<String> getResponseBody() async {
await Future.delayed(Duration(milliseconds: 1000));
return _generateOneTimePassword();
}
_generateOneTimePassword() {
return '{ "verificationCode": "' +
randomGenerator.nextInt(10).toString() +
randomGenerator.nextInt(10).toString() +
randomGenerator.nextInt(10).toString() +
randomGenerator.nextInt(10).toString() +
'"}';
}
}
class OneTimePasswordService {
final httpClient = MockHttpClient();
Future<OneTimePasswordResponse> getOneTimePassword(String phoneNumber) async {
final responseBody = await httpClient.getResponseBody();
return OneTimePasswordResponse.fromJson(responseBody);
}
}
import 'dart:convert';
import 'package:flutter/foundation.dart';
class OneTimePasswordResponse {
final String verificationCode;
OneTimePasswordResponse({
@required this.verificationCode,
});
static OneTimePasswordResponse fromMap(Map<String, dynamic> map) {
if (map == null) return null;
return OneTimePasswordResponse(
verificationCode: map['verificationCode'],
);
}
static OneTimePasswordResponse fromJson(String source) => fromMap(json.decode(source));
@override
String toString() {
return 'verificationCode: $verificationCode';
}
}
Let’s also modify our main.dart as well
...
import 'otp_service.dart';
...
class _MyHomePageState extends State<MyHomePage> {
String _phoneNumber;
String _oneTimePassword;
final otpService = OneTimePasswordService();
void getOneTimePassword() async {
var oneTimePasswordResponse = await otpService.getOneTimePassword(_phoneNumber);
setState(() {
_oneTimePassword = oneTimePasswordResponse.toString();
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
...
width: MediaQuery.of(context).size.width * 0.5,
),
if(_oneTimePassword != null) Text(_oneTimePassword),
RaisedButton(
...
Now it’s faintly starting to look like something that actually resembles an application.
Error handling
In a perfect world, everything works flawlessly and there is no need to worry about errors or bugs that might ruin our day. Sadly we don’t live in a perfect world.
Let’s see what will happen if for some reason there is an error from the HttpClient side.
Since we are using our own Mock client we can just replace
return _generateOneTimePassword();
with
throw HttpException(‘500’);
in otp_service.dart
When we try to get the code this time we will be greeted by an error in the console. For the user however nothing will be shown. This is not good because the user might think the application is buggy and broken.
Catching exceptions
Let’s try catching the exception.
...
Future<OneTimePasswordResponse> getOneTimePassword(String phoneNumber) async {
try {
final responseBody = await httpClient.getResponseBody();
return OneTimePasswordResponse.fromJson(responseBody);
} catch (e) {
print(e);
}
}
...
Looks good right?
Well. Not really. We actually didn’t really make it any better since all it does now is that it prints the error message in the console. The user will still have no idea why the app is not working. We should avoid this blanket catching at all costs.
But the problem remains. We have to get the error message to the UI somehow.
Useful widgets
Luckily Flutter has an awesome widget called Futurebuilder just for this purpose.
I won’t go over the details for this but if you want to know more then check out the link:
https://api.flutter.dev/flutter/widgets/FutureBuilder-class.html
Let’s modify our main.dart to add the previously mentioned Widget
It will allow us to display the latest snapshot based on the otpResponseFuture response
...
import 'otp_response.dart';
...
class _MyHomePageState extends State<MyHomePage> {
String _phoneNumber;
Future<OneTimePasswordResponse> otpResponseFuture;
final otpService = OneTimePasswordService();
void getOneTimePassword() async {
setState(() {
otpResponseFuture = otpService.getOneTimePassword(_phoneNumber);
});
}
...
width: MediaQuery.of(context).size.width * 0.5,
),
FutureBuilder<OneTimePasswordResponse>(
future: otpResponseFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.hasError) {
final error = snapshot.error;
return Text(error.toString());
} else if (snapshot.hasData) {
final response = snapshot.data;
return Text(response.toString());
} else {
return Text('After entering the phone number, press the button below');
}
},
),
RaisedButton(
...
We also need to remove the try catch block we added in OneTimePasswordService and let the error propagate to our Futurebuilder.
...
Future<OneTimePasswordResponse> getOneTimePassword(String phoneNumber) async {
final responseBody = await httpClient.getResponseBody();
return OneTimePasswordResponse.fromJson(responseBody);
}
Let’s try it out now!
Success! We can now display the error message. The only problem is that the user still won’t understand what this means. We shouldn’t actually show low-level error messages like that to the user.
Another big problem is that we let every exception propagate and get caught by the Futurebuilder. In some cases it might be better to let the app crash instead.
Customized exceptions
A good way is to catch only a particular set of exceptions and display a message based on those.
For that we are also going to create our own custom VerificationException class to customize our messages.
class VerificationException {
final String message;
VerificationException(this.message);
@override
String toString() => message;
}
Let’s also add an additional catch block for SocketExceptions. (You can additionally test it by throwing a SocketException instead of HttpException in our Mock client)
...
import 'verification_exception.dart';
...
try {
final responseBody = await httpClient.getResponseBody();
return OneTimePasswordResponse.fromJson(responseBody);
}
on SocketException {
throw VerificationException('No Internet connection');
} on HttpException {
throw VerificationException("Service is unavailable");
}
...
Looks much more readable for the user now doesn’t it?
There is still one issue we need to address. Namely our FutureBuilder currently still catches all errors and displays them which is bad. We only want to catch our own custom exceptions.
Having more control
Thankfully there is a better solution: ChangeNotifier (https://api.flutter.dev/flutter/foundation/ChangeNotifier-class.html)
Let’s create a new file called verification_change_notifier
import 'otp_response.dart';
import 'otp_service.dart';
import 'verification_exception.dart';
import 'package:flutter/cupertino.dart';
enum NotifierState { initial, loading, loaded }
class VerificationChangeNotifier extends ChangeNotifier {
final _otpService = OneTimePasswordService();
NotifierState _state = NotifierState.initial;
NotifierState get state => _state;
void _setState(NotifierState state) {
_state = state;
notifyListeners();
}
VerificationException _exception;
VerificationException get exception => _exception;
void _setVerificationException(VerificationException exception) {
_exception = exception;
}
OneTimePasswordResponse _otpResponse;
OneTimePasswordResponse get otpResponse => _otpResponse;
void _setOtpResponse(OneTimePasswordResponse otpResponse) {
_otpResponse = otpResponse;
}
void getOneTimePassword(String phoneNumber) async {
_setState(NotifierState.loading);
try {
final otpResponse = await _otpService.getOneTimePassword(phoneNumber);
_setOtpResponse(otpResponse);
} on VerificationException catch (f) {
_setVerificationException(f);
}
_setState(NotifierState.loaded);
}
}
I feel a little bit of explanation is in order.
First of all we have a NotifierState with 3 values:
initial – This is the UI state for when the screen is initially loaded
loading – This state will display the loading indicator
loaded – Finally this state will display us the result or the error, depending on the response from the client
We also define getters and setters for private fields _state,_otpResponse,_exception
Now whenever we call the getOneTimePassword method it will set the correct state and only when we have our custom exception it will set the exception.
In our main class we will replace our Futurebuilder with a Consumer widget (Don’t forget to add the provider dependency)
...
width: MediaQuery.of(context).size.width * 0.5,
),
Consumer<VerificationChangeNotifier>(
builder: (_, notifier, __) {
if (notifier.state == NotifierState.initial) {
return Text('After entering the phone number, press the button below');
} else if (notifier.state == NotifierState.loading) {
return CircularProgressIndicator();
} else {
if (notifier.exception != null) {
return Text(notifier.exception.toString());
} else {
return Text(notifier.otpResponse.toString());
}
}
},
),
RaisedButton(
...
...
dependencies:
flutter:
sdk: flutter
provider: ^3.2.0
...
We will replace our getOneTimePassword method as well and remove otpResponseFuture &otpService since our ChangeNotifier does everything already.
Finally we need to wrap all this into a ChangeNotifierProvider
...
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: ChangeNotifierProvider(
create: (_) => VerificationChangeNotifier(),
child: MyHomePage(title: 'Flutter Error Handling Demo'),
),
);
}
}
...
void getOneTimePassword() async {
Provider.of<VerificationChangeNotifier>(context).getOneTimePassword(_phoneNumber);
}
...
There you have it! Now we have a working app that shows an intelligible error message to the user and also doesn’t catch and show all errors.
Conclusion
Handling errors is nothing you should be afraid of. It will save you a lot of headache later on if you properly manage your errors. The suggestions in this article is only the tip of the iceberg when it comes to error handling but I hope it gave a vague idea how it’s possible to do this in Flutter.
Happy Coding!