flutter_network_core 0.1.0 copy "flutter_network_core: ^0.1.0" to clipboard
flutter_network_core: ^0.1.0 copied to clipboard

Cross-platform Dio network layer for Flutter. HTTP methods, connectivity, timeouts, queued access/refresh tokens, and typed exceptions.

example/lib/main.dart

import 'dart:convert';
import 'dart:typed_data';

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:flutter_network_core/flutter_network_core.dart';

void main() {
  runApp(const NetworkCoreExampleApp());
}

class NetworkCoreExampleApp extends StatefulWidget {
  const NetworkCoreExampleApp({super.key});

  @override
  State<NetworkCoreExampleApp> createState() => _NetworkCoreExampleAppState();
}

class _NetworkCoreExampleAppState extends State<NetworkCoreExampleApp> {
  late final MockApiAdapter _adapter;
  late final MemoryTokenStorage _storage;
  late final ManualConnectivityChecker _connectivity;
  late final NetworkClient _network;
  final _log = <String>[];
  var _pending = 0;
  var _sessionExpired = false;
  var _online = true;

  @override
  void initState() {
    super.initState();
    _adapter = MockApiAdapter();
    _storage = MemoryTokenStorage();
    _connectivity = ManualConnectivityChecker();
    _network = NetworkClient(
      config: NetworkConfig(
        baseUrl: 'https://api.example.com',
        enableLogging: false,
        retry: const RetryConfig(maxExtraAttempts: 0),
      ),
      tokenStorage: _storage,
      connectivity: _connectivity,
      httpClientAdapter: _adapter,
      usePluginConnectivity: false,
      refreshDelegateBuilder: (plain) => HttpTokenRefreshDelegate(
        client: plain,
        path: '/auth/refresh',
        bodyBuilder: (token) => {'refreshToken': token},
        parser: (data) {
          final map = Map<String, dynamic>.from(data as Map);
          return TokenPair(
            accessToken: map['accessToken'] as String,
            refreshToken: map['refreshToken'] as String,
          );
        },
      ),
      onSessionExpired: () {
        setState(() => _sessionExpired = true);
        _append('SessionExpiredException → sign in again');
      },
    );
    _network.pendingRequestCount.listen((count) {
      if (mounted) {
        setState(() => _pending = count);
      }
    });
  }

  @override
  void dispose() {
    _network.close();
    _connectivity.dispose();
    super.dispose();
  }

  void _append(String line) {
    setState(() => _log.insert(0, line));
  }

  Future<void> _run(String label, Future<void> Function() action) async {
    _append('▶ $label');
    try {
      await action();
    } on AppNetworkException catch (error) {
      _append('✖ ${error.runtimeType}: ${error.message}');
    } catch (error) {
      _append('✖ $error');
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'flutter_network_core',
      home: Scaffold(
        appBar: AppBar(
          title: const Text('flutter_network_core'),
          actions: [
            if (_pending > 0)
              const Padding(
                padding: EdgeInsets.all(16),
                child: SizedBox(
                  width: 18,
                  height: 18,
                  child: CircularProgressIndicator(strokeWidth: 2),
                ),
              ),
          ],
        ),
        body: Column(
          children: [
            if (_sessionExpired)
              const MaterialBanner(
                content: Text('Session expired'),
                actions: [SizedBox.shrink()],
              ),
            SwitchListTile(
              title: const Text('Online'),
              value: _online,
              onChanged: (value) {
                _connectivity.setOnline(value);
                setState(() => _online = value);
                _append(value ? 'Connectivity: online' : 'Connectivity: offline');
              },
            ),
            Wrap(
              spacing: 8,
              runSpacing: 8,
              children: [
                FilledButton(
                  onPressed: () => _run('Login', () async {
                    _sessionExpired = false;
                    final data =
                        await _network.unauthenticated.post<Map<String, dynamic>>(
                      '/auth/login',
                      data: {'email': 'user@example.com', 'password': 'secret'},
                    );
                    await _storage.write(
                      TokenPair(
                        accessToken: data['accessToken'] as String,
                        refreshToken: data['refreshToken'] as String,
                      ),
                    );
                    _append('Logged in');
                  }),
                  child: const Text('Login'),
                ),
                FilledButton.tonal(
                  onPressed: () => _run('GET /users/me', () async {
                    final data = await _network.get<Map<String, dynamic>>(
                      '/users/me',
                    );
                    _append('Hello ${data['name']}');
                  }),
                  child: const Text('Profile'),
                ),
                OutlinedButton(
                  onPressed: () {
                    _adapter.expireAccessToken();
                    _append('Access token marked expired (next call 401)');
                  },
                  child: const Text('Force 401'),
                ),
                OutlinedButton(
                  onPressed: () {
                    _adapter.killRefresh();
                    _append('Refresh endpoint will fail');
                  },
                  child: const Text('Kill refresh'),
                ),
                OutlinedButton(
                  onPressed: () => _run('Forbidden', () async {
                    await _network.get<Map<String, dynamic>>('/admin');
                  }),
                  child: const Text('403'),
                ),
                OutlinedButton(
                  onPressed: () => _run('Bad login', () async {
                    await _network.unauthenticated.post<Map<String, dynamic>>(
                      '/auth/login',
                      data: {'email': 'x', 'password': 'wrong'},
                    );
                  }),
                  child: const Text('Bad login'),
                ),
              ],
            ),
            const Divider(),
            Expanded(
              child: ListView.builder(
                padding: const EdgeInsets.all(16),
                itemCount: _log.length,
                itemBuilder: (context, index) => Text(_log[index]),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

class MockApiAdapter implements HttpClientAdapter {
  var _accessValid = true;
  var _refreshValid = true;
  var _issued = 0;

  void expireAccessToken() => _accessValid = false;

  void killRefresh() => _refreshValid = false;

  @override
  Future<ResponseBody> fetch(
    RequestOptions options,
    Stream<Uint8List>? requestStream,
    Future<void>? cancelFuture,
  ) async {
    final path = options.uri.path;
    if (path.endsWith('/auth/login')) {
      final body = await _readJson(requestStream);
      if (body['password'] != 'secret') {
        return _json({'message': 'invalid credentials'}, 401);
      }
      _accessValid = true;
      _refreshValid = true;
      _issued += 1;
      return _json({
        'accessToken': 'access-$_issued',
        'refreshToken': 'refresh-$_issued',
      });
    }
    if (path.endsWith('/auth/refresh')) {
      if (!_refreshValid) {
        return _json({'message': 'refresh expired'}, 401);
      }
      _accessValid = true;
      _issued += 1;
      return _json({
        'accessToken': 'access-$_issued',
        'refreshToken': 'refresh-$_issued',
      });
    }
    if (path.endsWith('/admin')) {
      return _json({'message': 'forbidden'}, 403);
    }
    if (path.endsWith('/users/me')) {
      final auth = options.headers['Authorization'] as String?;
      if (auth == null || !_accessValid) {
        return _json({'message': 'expired'}, 401);
      }
      return _json({'name': 'Ghanashyam', 'token': auth});
    }
    return _json({'message': 'not found'}, 404);
  }

  @override
  void close({bool force = false}) {}

  Future<Map<String, dynamic>> _readJson(Stream<Uint8List>? stream) async {
    if (stream == null) {
      return {};
    }
    final chunks = await stream.toList();
    if (chunks.isEmpty) {
      return {};
    }
    final bytes = chunks.expand((chunk) => chunk).toList();
    return jsonDecode(utf8.decode(bytes)) as Map<String, dynamic>;
  }

  ResponseBody _json(Object data, [int status = 200]) {
    return ResponseBody.fromString(
      jsonEncode(data),
      status,
      headers: {
        Headers.contentTypeHeader: [Headers.jsonContentType],
      },
    );
  }
}
0
likes
110
points
81
downloads

Documentation

Documentation
API reference

Publisher

unverified uploader

Weekly Downloads

Cross-platform Dio network layer for Flutter. HTTP methods, connectivity, timeouts, queued access/refresh tokens, and typed exceptions.

Repository (GitHub)
View/report issues

Topics

#dio #network #http #interceptor #authentication

License

BSD-3-Clause (license)

Dependencies

connectivity_plus, dio, flutter, flutter_secure_storage, meta

More

Packages that depend on flutter_network_core