flutter_api_mock_server 0.0.1
flutter_api_mock_server: ^0.0.1 copied to clipboard
A Flutter package to simulate mock API responses and network conditions like errors and latency.
import 'package:flutter/material.dart';
import 'package:flutter_api_mock_server/flutter_api_mock_server.dart';
void main() {
runApp(MockServerDemoApp());
}
class MockServerDemoApp extends StatelessWidget {
const MockServerDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter API Mock Server',
theme: ThemeData(primarySwatch: Colors.blue),
home: MockServerHome(),
);
}
}
class MockServerHome extends StatefulWidget {
const MockServerHome({super.key});
@override
State<MockServerHome> createState() => _MockServerHomeState();
}
class _MockServerHomeState extends State<MockServerHome> with TickerProviderStateMixin {
final MockServer mockServer = MockServer();
final List<String> logs = [];
late TabController tabController;
// Controllers and state for Manage Mocks tab
final pathController = TextEditingController();
final responseController = TextEditingController();
final statusController = TextEditingController(text: '200');
final delayController = TextEditingController(text: '0');
HttpMethod selectedMethod = HttpMethod.get;
List<Map<String, dynamic>> activeMocks = [];
// Separate response text for each tab
String crudResponseText = '';
String errorDelayResponseText = '';
// Predefined mocks for CRUD and error/delay
final List<Map<String, dynamic>> _predefinedMocks = [
{'path': '/users', 'method': HttpMethod.get},
{'path': '/users', 'method': HttpMethod.post},
{'path': '/users/1', 'method': HttpMethod.put},
{'path': '/users/2', 'method': HttpMethod.delete},
{'path': '/users/1', 'method': HttpMethod.get},
{'path': '/error', 'method': HttpMethod.get},
{'path': '/delayed', 'method': HttpMethod.get},
];
@override
void initState() {
super.initState();
tabController = TabController(length: 4, vsync: this);
_addInitialMocks();
_refreshActiveMocks();
}
void _addInitialMocks() {
mockServer.clear();
mockServer.addMockResponse(
path: '/users',
method: HttpMethod.get,
response: '[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]',
delay: Duration(milliseconds: 500),
statusCode: 200,
);
mockServer.addMockResponse(
path: '/users',
method: HttpMethod.post,
response: '{"id":3,"name":"Charlie"}',
delay: Duration(milliseconds: 300),
statusCode: 201,
);
mockServer.addMockResponse(
path: '/users/1',
method: HttpMethod.put,
response: '{"id":1,"name":"Alice Updated"}',
delay: Duration(milliseconds: 200),
statusCode: 200,
);
mockServer.addMockResponse(
path: '/users/2',
method: HttpMethod.delete,
response: '',
delay: Duration(milliseconds: 100),
statusCode: 204,
);
mockServer.addMockResponse(
path: '/users/1',
method: HttpMethod.get,
response: '{"id":1,"name":"Alice"}',
delay: Duration(milliseconds: 400),
statusCode: 200,
);
mockServer.addMockResponse(
path: '/error',
method: HttpMethod.get,
response: 'Internal Server Error',
delay: Duration(milliseconds: 100),
statusCode: 500,
);
mockServer.addMockResponse(
path: '/delayed',
method: HttpMethod.get,
response: 'This response is delayed!',
delay: Duration(seconds: 2),
statusCode: 200,
);
_refreshActiveMocks();
}
Future<void> _callApi(String path, HttpMethod method, {required int tabIndex}) async {
setState(() {
if (tabIndex == 0) {
crudResponseText = 'Loading...';
} else if (tabIndex == 1) {
errorDelayResponseText = 'Loading...';
}
});
final result = await mockServer.fetchMockData(path, method);
setState(() {
if (tabIndex == 0) {
crudResponseText = 'Status: ${result.statusCode}\nResponse: ${result.response}';
} else if (tabIndex == 1) {
errorDelayResponseText = 'Status: ${result.statusCode}\nResponse: ${result.response}';
}
logs.add('${DateTime.now().toIso8601String()} | $method $path => ${result.statusCode}');
});
}
void _addMock(String path, HttpMethod method, String response, int status, int delayMs) {
// Prevent adding predefined mocks from Manage Mocks tab
if (_predefinedMocks.any((m) => m['path'] == path && m['method'] == method)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Predefined mocks cannot be added or overwritten here.')),
);
return;
}
// Remove any existing user mock for this path/method before adding new one
mockServer.removeMockResponse(path, method);
mockServer.addMockResponse(
path: path,
method: method,
response: response,
statusCode: status,
delay: Duration(milliseconds: delayMs),
);
setState(() {
logs.add('${DateTime.now().toIso8601String()} | Added mock: $method $path');
_refreshActiveMocks();
});
}
void _removeMock(String path, HttpMethod method) {
// Prevent removing predefined mocks from Manage Mocks tab
if (_predefinedMocks.any((m) => m['path'] == path && m['method'] == method)) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Predefined mocks cannot be removed.')),
);
return;
}
mockServer.removeMockResponse(path, method);
setState(() {
logs.add('${DateTime.now().toIso8601String()} | Removed mock: $method $path');
_refreshActiveMocks();
});
}
void _clearMocks() {
// Only clear user-added mocks, not predefined
for (final mock in activeMocks) {
if (!_predefinedMocks.any((m) => m['path'] == mock['path'] && m['method'] == mock['method'])) {
mockServer.removeMockResponse(mock['path'], mock['method']);
}
}
setState(() {
logs.add('${DateTime.now().toIso8601String()} | Cleared all user mocks');
_refreshActiveMocks();
});
}
void _refreshActiveMocks() {
activeMocks.clear();
mockServer.mockResponses.forEach((path, responses) {
for (var r in responses) {
activeMocks.add({
'path': path,
'method': r.method,
'status': r.statusCode,
'delay': r.delay.inMilliseconds,
'response': r.response,
});
}
});
}
bool get hasCrudMocks {
final paths = ['/users', '/users/1', '/users/2'];
final methods = [HttpMethod.get, HttpMethod.post, HttpMethod.put, HttpMethod.delete];
for (final path in paths) {
for (final method in methods) {
if (mockServer.mockResponses[path]?.any((r) => r.method == method) ?? false) {
return true;
}
}
}
return false;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Flutter API Mock Server Example'),
bottom: TabBar(
tabAlignment: TabAlignment.start,
isScrollable: true,
controller: tabController,
tabs: [
Tab(text: 'CRUD'),
Tab(text: 'Error & Delay'),
Tab(text: 'Manage Mocks'),
Tab(text: 'Logs'),
],
),
),
body: TabBarView(
controller: tabController,
children: [
_buildCrudTab(),
_buildErrorDelayTab(),
_buildManageMocksTab(),
_buildLogsTab(),
],
),
);
}
Widget _buildCrudTab() {
// Collect all unique path/method pairs from current mocks
final allMocks = <Map<String, dynamic>>[];
mockServer.mockResponses.forEach((path, responses) {
for (var r in responses) {
allMocks.add({
'path': path,
'method': r.method,
'label': '${r.method.toString().split('.').last.toUpperCase()} $path',
});
}
});
// If no mocks, show a message
if (allMocks.isEmpty) {
return ListView(
padding: EdgeInsets.all(16),
children: [
Text('No mocks available. Add mocks in the Manage Mocks tab.'),
],
);
}
return ListView(
padding: EdgeInsets.all(16),
children: [
Text('Available Mock Endpoints', style: Theme.of(context).textTheme.titleLarge),
SizedBox(height: 8),
Text('All currently registered mocks are shown below. Add new ones in Manage Mocks.'),
SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: allMocks.map((endpoint) {
return ElevatedButton(
onPressed: () => _callApi(endpoint['path'] as String, endpoint['method'] as HttpMethod, tabIndex: 0),
child: Text(endpoint['label'] as String),
);
}).toList(),
),
SizedBox(height: 24),
Text('Response:', style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: Text(crudResponseText),
),
],
);
}
Widget _buildErrorDelayTab() {
return ListView(
padding: EdgeInsets.all(16),
children: [
Text('Error & Delay Simulation', style: Theme.of(context).textTheme.titleLarge),
SizedBox(height: 8),
Text('Test error responses and network delays.'),
SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ElevatedButton(
onPressed: () => _callApi('/error', HttpMethod.get, tabIndex: 1),
child: Text('GET /error (500)'),
),
ElevatedButton(
onPressed: () => _callApi('/unknown', HttpMethod.get, tabIndex: 1),
child: Text('GET /unknown (404)'),
),
ElevatedButton(
onPressed: () => _callApi('/delayed', HttpMethod.get, tabIndex: 1),
child: Text('GET /delayed (2s)'),
),
],
),
SizedBox(height: 24),
Text('Response:', style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 8),
Container(
padding: EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: Text(errorDelayResponseText),
),
],
);
}
Widget _buildManageMocksTab() {
// Filter out predefined mocks from the active mocks list
final userMocks = activeMocks.where((mock) =>
!_predefinedMocks.any((m) => m['path'] == mock['path'] && m['method'] == mock['method'])).toList();
return ListView(
padding: EdgeInsets.all(16),
children: [
Text('Manage Mock Responses', style: Theme.of(context).textTheme.titleLarge),
SizedBox(height: 8),
Text('Add, remove, or clear mock responses dynamically. Predefined mocks cannot be changed here.'),
SizedBox(height: 16),
DropdownButton<HttpMethod>(
value: selectedMethod,
items: HttpMethod.values.map((m) => DropdownMenuItem(
value: m,
child: Text(m.toString().split('.').last.toUpperCase()),
)).toList(),
onChanged: (m) {
setState(() {
selectedMethod = m!;
});
},
),
TextField(
controller: pathController,
decoration: InputDecoration(labelText: 'Path (e.g. /custom)'),
),
TextField(
controller: responseController,
decoration: InputDecoration(labelText: 'Response'),
),
TextField(
controller: statusController,
decoration: InputDecoration(labelText: 'Status Code'),
keyboardType: TextInputType.number,
),
TextField(
controller: delayController,
decoration: InputDecoration(labelText: 'Delay (ms)'),
keyboardType: TextInputType.number,
),
SizedBox(height: 8),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
ElevatedButton(
onPressed: () {
_addMock(
pathController.text,
selectedMethod,
responseController.text,
int.tryParse(statusController.text) ?? 200,
int.tryParse(delayController.text) ?? 0,
);
},
child: Text('Add Mock'),
),
SizedBox(width: 8),
ElevatedButton(
onPressed: () {
_removeMock(pathController.text, selectedMethod);
},
child: Text('Remove Mock'),
),
SizedBox(width: 8),
ElevatedButton(
onPressed: _clearMocks,
child: Text('Clear All (user mocks)'),
),
],
),
),
SizedBox(height: 24),
Text('User-added Mocks:', style: TextStyle(fontWeight: FontWeight.bold)),
SizedBox(height: 8),
...userMocks.map((mock) => Card(
child: ListTile(
title: Text('${mock['method'].toString().split('.').last.toUpperCase()} ${mock['path']}'),
subtitle: Text('Status: ${mock['status']}, Delay: ${mock['delay']}ms\nResponse: ${mock['response']}'),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => _removeMock(mock['path'], mock['method']),
tooltip: 'Remove',
),
),
)),
],
);
}
Widget _buildLogsTab() {
return ListView.builder(
padding: EdgeInsets.all(16),
itemCount: logs.length,
itemBuilder: (context, i) => Text(logs[i]),
);
}
}