flutter_api_mock_server 0.0.1 copy "flutter_api_mock_server: ^0.0.1" to clipboard
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.

example/main.dart

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]),
    );
  }
}
3
likes
150
points
17
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter package to simulate mock API responses and network conditions like errors and latency.

Repository (GitHub)
View/report issues

Documentation

API reference

License

MIT (license)

Dependencies

flutter

More

Packages that depend on flutter_api_mock_server