You're on a long train journey - currently in the middle of nowhere. The rolling English hills allows your brain to relax. The intrusive programmer in your head, however, magicks up a solution to something you've been working on for a while. Laptop on. Hotspot on. IDE open. Run application ... Network Error - Failed to connect!
Ever since I was introduced to Mock Service Worker in React, I've made it a goal to ensure that all of my React apps work offline in preparation for the aforementioned inevitability. It allows you to be able to develop your app with a fixed dataset, meaning that it doesn't matter whether you have a network or not, whilst also decoupling the frontend and backend development processes.
Offline mocking also serves another, more important purpose: to ensure that everything can be tested, as one really shouldn't be touching the outside world when performing tests.
When looking how I could replicate this in Flutter, all of the resources I found at the time very much leaned towards solving network mocking for testing purposes only. However, I could not for the life of me find a way of doing this for general development! I wasn't too keen to use yet another package, especially as I already use
dio
which supports interceptors, so I cooked up a solution.Problem To Solve
I want to be able to return mocked data, in this case
{"id":1,"name":"Example","attributes":{}}
for all network calls made to
example/path
within my app.Store The Response Data Locally
Firstly, I knew I'd need to store the response data somewhere, so I created a fixtures folder in my app. The file didn't have anything fancy in there, just a response I had copied from Chrome's network tab, and made into a
Map
object.lib/networking/fixtures/example_fixture.dart
const Map exampleFixture = {
"id": 1,
"name": "Example",
"attributes": {},
};
Create A New Interceptor To Perform The Mocking
Dio has a concept of Interceptors, which (as the name suggests) allow you to intercept all network requests that pass through Dio. I knew that I could utilise this to my advantage, as I had done something similar using the http_mock_adapter when testing. Why didn't I just use this as a solution? Well
http_mock_adapter
is a simple to use mocking package for Dio intended to be used in tests.
I thought it best to just listen to what I was told!
lib/networking/interceptors/mock_interceptor.dart
import 'package:dio/dio.dart';
class MockInterceptor extends Interceptor {
MockInterceptor();
static final List<InterceptorMock> _mocks = [];
/// Add one or more [InterceptorMock] to the List of available mocks
static add(List<InterceptorMock> mocks) {
_mocks.addAll(mocks);
}
static remove(String name) {
_mocks.removeWhere((element) => element.name == name);
}
/// Clear all mocks
static empty() {
_mocks.clear();
}
/// Get all mocks
static List<InterceptorMock> getAll() {
return _mocks;
}
/// Get the number of available mocks
static int length() {
return _mocks.length;
}
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
if (MockInterceptor.length() > 0) {
List<InterceptorMock> all = MockInterceptor.getAll();
// all.cast<InterceptorMock?> allows us to use firstWhere with an orElse of null
InterceptorMock? mock = all.cast<InterceptorMock?>().firstWhere(
(element) => element!.matcher(options),
orElse: () => null,
);
if (mock != null) {
// If we have found a matching mock, resolve the network call with the mock data
handler.resolve(
Response(
requestOptions: options,
data: mock.returnData is Function
? mock.returnData()
: mock.returnData,
statusCode: mock.statusCode,
),
);
return;
}
}
super.onRequest(options, handler);
}
}
/// Class to be used in conjunction with [MockInterceptor] Interceptor.
/// I.e. MockInterceptor.add([InterceptorMock(...), ...])
class InterceptorMock {
const InterceptorMock({
required this.name,
required this.matcher,
required this.returnData,
this.statusCode,
});
/// The name of the mock so that it can be matched against other things
final String name;
/// A function that is passed a [RequestOptions] object which can be used to
/// to see if the mock can be applied
final bool Function(RequestOptions options) matcher;
/// The data to return from the mock. If a function is passed, the return value of
/// the function is used
final dynamic returnData;
/// An optional statusCode to return
final int? statusCode;
}
Create A Mock
Next, we'll have to actually create a mock. Note, returnData can be anything that you want it to be - you know your application - but if a function is passed, the return data of the function will be sent back to your application.
lib/networking/mocks/example_mock.dart
final exampleMock = InterceptorMock(
name: 'example',
matcher: (RequestOptions options) => options.path.contains('example/path'),
returnData: exampleFixture,
);
Add The Interceptor
I opted to do this at the top of my application, meaning that I also wouldn't need to set it up for my tests.
lib/main.dart
void main() async {
...
dio.interceptors.addAll([
...
MockInterceptor(),
]);
if (env('USE_OFFLINE_MOCKS_ONLY') == '1') {
initOfflineMode();
}
...
}
MockInterceptor
won't actually do anything until mocks are added, meaning that it is safe to add it here. However, I've added an environment variable check to determine whether I want to use the network, or to use my mocks.initOfflineMode
initOfflineMode() {
MockInterceptor.add([
...
exampleMock,
]);
}
~
And that's it! I can now develop my app completely offline as I get distracted by the happy sheep and cows flashing past my train window!