Flutter Fixtures sits between your app and its data. The requests you already make through Dio or sqflite are answered from JSON fixture files instead of a live backend, and each fixture can hold several responses, so you can switch between success, empty and error states from a dialog inside the running app.
Use it to build screens before the API exists, demo without a network, and reproduce the edge cases that are hard to hit for real.
The example app: one request, three possible answers, chosen at runtime.
Quick start
1. Add the package. The meta-package bundles the Dio interceptor and the pick dialog.
flutter pub add flutter_fixtures
2. Register the interceptor on the Dio instance your app already uses. The pipeline is where fixtures come from, how one is chosen, and who asks. Build it once, next to the Dio instance, so remembered choices survive.
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_fixtures/flutter_fixtures.dart';
// The dialog needs a context. Hand this key to MaterialApp(navigatorKey: ...).
final navigatorKey = GlobalKey<NavigatorState>();
final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'))
..interceptors.add(
FixturesInterceptor(
pipeline: FixturePipeline(
source: HttpFileFixtureSource(),
selector: DataSelectorType.pick,
view: FixturesDialogView(
contextProvider: () => navigatorKey.currentContext!,
),
),
),
);
3. Write a fixture. Create assets/fixtures/GET_users.json and register the folder in pubspec.yaml.
{
"description": "Users List",
"values": [
{ "identifier": "Success", "description": "200", "default": true,
"data": { "users": [{ "id": 1, "name": "Alice" }] } },
{ "identifier": "Empty", "description": "200", "data": { "users": [] } },
{ "identifier": "Error", "description": "500", "data": { "error": "Internal Server Error" } }
]
}
flutter:
assets:
- assets/fixtures/
4. Make the request as usual. A dialog asks which response to return. Pick Error and your error screen shows up.
final response = await dio.get('/users');
Tip: Use
DataSelectorType.defaultValuefor automated tests and CI, andDataSelectorType.randomto shake loose state bugs during development. The dialog is for humans.
How it works
1. The request becomes a file name. GET /users looks for GET_users.json; slashes become underscores. Query values are tried in sorted-key order: GET_search_2_test.json for /search?q=test&page=2, then wildcards GET_search_*_*.json or GET_search_{{page}}_{{q}}.json. The first file that exists wins.
2. Sources are consulted in order. Fixture files first, then an OpenAPI spec if you add one, then any FixtureSource of your own, combined with HttpFixtureSources. The first source that resolves the request provides the collection.
3. One response is selected.
| Strategy | What happens |
|---|---|
DataSelectorType.defaultValue |
Returns the entry marked "default": true |
DataSelectorType.random |
Returns any entry at random |
DataSelectorType.pick |
Shows a dialog and returns what you chose. Choices can be remembered per request. |
4. The answer is delayed, then returned. Pass delay to the pipeline to test loading states: the presets DataSelectorDelay.instant (default), fast (~100 ms), moderate (~500 ms) and slow (~2 s), or any Duration.
Anatomy of a fixture file
| Field | Meaning |
|---|---|
description |
Title of the collection, shown in the pick dialog |
values[] |
The possible responses |
values[].identifier |
Label of one response |
values[].description |
Its status: the first three characters become the HTTP status code |
values[].default |
Marks the response used by defaultValue |
values[].data |
The inline payload |
values[].dataPath |
A JSON file relative to the fixtures folder, for large payloads. Use instead of data, and list its subfolder (for example assets/fixtures/data/) in pubspec.yaml too. |
Beyond HTTP
SQLite with sqflite
Code your repositories against DatabaseAdapter, then decide at startup whether it talks to a real database or to fixtures. Nothing in the repository changes.
import 'package:flutter_fixtures_sqflite/flutter_fixtures_sqflite.dart';
// Development: answer queries from assets/fixtures/database/
final db = FixtureDatabaseAdapter(
pipeline: FixturePipeline(
source: SqfliteFileFixtureSource(),
selector: DataSelectorType.pick,
view: FixturesDialogView.of(context),
),
);
// Production: the real thing
// final db = RealDatabaseAdapter(await openDatabase('app.db'));
final users = await db.query('users'); // -> query_users.json
Fixture files are named {operation}_{table}.json, so db.insert('orders', …) reads insert_orders.json. See the sqflite package for the full adapter API.
OpenAPI specs
If your API ships an OpenAPI 3.x document, drop it in your assets and every documented response becomes a selectable fixture, labelled by status code and description, with payloads taken from the spec's examples or generated from its schema. Hand-written files still win when both exist.
FixturesInterceptor(
pipeline: FixturePipeline(
source: HttpFixtureSources([
HttpFileFixtureSource(),
OpenApiFixtureSource(specPath: 'assets/fixtures/openapi.json'),
]),
selector: DataSelectorType.pick,
),
)
Details live in the Dio package.
Record and replay
Capture real traffic once, then replay it later in the same order, with no network at all. One recorder covers every wired source, so an HTTP call and a SQL query recorded in the same session come back together. It ships with a toolbar and a sessions sheet, and every control is also on the public API.
import 'package:flutter_fixtures_recorder/flutter_fixtures_recorder.dart';
final recorder = FixtureRecorder(
store: sessionStoreForDirectory(() async =>
'${(await getApplicationDocumentsDirectory()).path}/fixture_recordings'),
);
dio.interceptors.add(RecorderInterceptor(recorder: recorder)); // HTTP
final db = RecorderDatabaseAdapter(inner: realDb, recorder: recorder); // SQL
RecorderToolbar(recorder: recorder) // anywhere in your debug UI
Place RecorderInterceptor before FixturesInterceptor and the two chain: fixture picks are recorded too, and a replay serves them back without showing a dialog. Ordering, miss policies and custom storage are covered in the recorder package.
Record two picks, save, replay: the same answers come back in order, tagged REPLAY.
Packages
| Package | What it adds | |
|---|---|---|
flutter_fixtures |
Core + Dio + UI in one dependency | |
flutter_fixtures_core |
Models, fixture sources, selection flow, OpenAPI, the recorder seam | |
flutter_fixtures_dio |
FixturesInterceptor and RecorderInterceptor for Dio |
|
flutter_fixtures_ui |
The pick dialog | |
flutter_fixtures_sqflite |
DatabaseAdapter with real, fixture and recording implementations |
|
flutter_fixtures_recorder |
Record & replay engine, toolbar and sessions sheet |
Pick only what you need:
dependencies:
flutter_fixtures_core: ^0.3.0 # always
flutter_fixtures_dio: ^0.3.0 # HTTP via Dio
flutter_fixtures_ui: ^0.3.0 # the pick dialog
flutter_fixtures_sqflite: ^0.3.0 # SQLite
flutter_fixtures_recorder: ^0.3.0 # record & replay
To support another data source, depend on core alone: every domain shares one FixtureSource<TRequest> seam, file-backed sources reuse FixtureFileSource with their own naming convention, and a FixturePipeline drives it. The core package walks through a custom source, and a custom selector UI is one method:
class MySelectorView implements DataSelectorView {
@override
Future<FixtureChoice?> pick(FixtureCollection fixture) async {
// show your UI; return the choice, or null if cancelled
}
}
Example app
The example has four tabs: Basic, Advanced (query wildcards and OpenAPI), SQLite, and Recorder.
cd example && flutter run
Roadmap
What exists and what is planned
HTTP clients
xDiohttp packageChopperRetrofitGraphQL
Database providers
xSQLite (sqflite)HiveIsarObjectBoxRealm
UI selectors
xDialogBottom sheetDropdownNotification with actionsSidebar panel
Other
xOpenAPI-driven fixturesxResponse delay simulationxRecord & replayFixture validationNetwork condition simulation
Want to take one? Open an issue first so we can agree on the approach.
Development
The repository is a Dart workspace managed with Melos. Setup and the test, format and analyze commands are in the repository README.
Contributing
Contributions are welcome on GitHub. Read CONTRIBUTING.md, then open a pull request. For a new implementation from the roadmap, open an issue first to discuss the approach.
Support
If this library saves you time, consider supporting its development.
License
MIT. See LICENSE.
