dio_flow 1.1.10 copy "dio_flow: ^1.1.10" to clipboard
dio_flow: ^1.1.10 copied to clipboard

A powerful and flexible HTTP client for Flutter applications, built on top of Dio with advanced features like request queueing, caching, and automatic token management.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:dio_flow/dio_flow.dart';

void main() {
  // Initialize the API client when the app starts
  _initApiClient();
  runApp(const MyApp());
}

// Initialize the API client with configuration
void _initApiClient() {
  // Configure the API client with base URL and timeouts
  DioFlowConfig.initialize(
    baseUrl: 'https://jsonplaceholder.typicode.com',
    connectTimeout: const Duration(seconds: 5),
    receiveTimeout: const Duration(seconds: 3),
    sendTimeout: const Duration(seconds: 3),
  );

  // Register endpoints that will be used in the app
  _registerApiEndpoints();
}

// Register API endpoints
void _registerApiEndpoints() {
  // Register individual endpoints
  EndpointProvider.instance.register('posts', '/posts');
  EndpointProvider.instance.register('users', '/users');
  EndpointProvider.instance.register('post', '/posts/{id}');
  EndpointProvider.instance.register('user', '/users/{id}');
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Dio Flow Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: const MyHomePage(title: 'Dio Flow Demo'),
    );
  }
}

class MyHomePage extends StatefulWidget {
  const MyHomePage({super.key, required this.title});

  final String title;

  @override
  State<MyHomePage> createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  final List<Tab> _tabs = const [
    Tab(text: 'Posts', icon: Icon(Icons.article)),
    Tab(text: 'Users', icon: Icon(Icons.people)),
    Tab(text: 'JSON Utils', icon: Icon(Icons.data_object)),
  ];

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: _tabs.length,
      child: Scaffold(
        appBar: AppBar(
          backgroundColor: Theme.of(context).colorScheme.inversePrimary,
          title: Text(widget.title),
          bottom: TabBar(tabs: _tabs),
        ),
        body: TabBarView(children: [PostsPage(), UsersPage(), JsonUtilsPage()]),
      ),
    );
  }
}

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

  @override
  State createState() => _PostsPageState();
}

class _PostsPageState extends State<PostsPage> {
  List<Post> _posts = [];
  bool _isLoading = false;
  String _error = '';
  int _currentPage = 1;
  bool _hasMorePages = true;
  final int _pageSize = 10;

  @override
  void initState() {
    super.initState();
    _fetchInitialPosts();
  }

  Future<void> _fetchInitialPosts() async {
    setState(() {
      _isLoading = true;
      _error = '';
    });

    try {
      // Use dio_flow to fetch posts
      final response = await DioRequestHandler.get(
        'posts',
        parameters: {'_page': _currentPage, '_limit': _pageSize},
      );

      // Handle the response using ResponseModel
      if (response is SuccessResponseModel) {
        final data = response.data as List;
        setState(() {
          _posts =
              data
                  .map((json) => Post.fromJson(json as Map<String, dynamic>))
                  .toList();
          _isLoading = false;

          // For JSONPlaceholder API, we use a workaround to get pagination info
          // In a real app, you would use response.meta for pagination
          _hasMorePages = data.length >= _pageSize;
        });
      } else {
        // Handle error response
        final errorResponse = response as FailedResponseModel;
        throw Exception('Failed to load posts: ${errorResponse.message}');
      }
    } catch (e) {
      setState(() {
        _error = e.toString();
        _isLoading = false;
      });
    }
  }

  Future<void> _loadMorePosts() async {
    if (!_hasMorePages || _isLoading) return;

    setState(() {
      _isLoading = true;
    });

    try {
      _currentPage++;

      // Use dio_flow to fetch the next page
      final response = await DioRequestHandler.get(
        'posts',
        parameters: {'_page': _currentPage, '_limit': _pageSize},
      );

      if (response is SuccessResponseModel) {
        final data = response.data as List;
        final newPosts =
            data
                .map((json) => Post.fromJson(json as Map<String, dynamic>))
                .toList();

        setState(() {
          _posts = [..._posts, ...newPosts];
          _isLoading = false;

          // For JSONPlaceholder API, determine if there are more pages based on returned data
          _hasMorePages = newPosts.length >= _pageSize;
        });
      } else {
        // Handle error response
        final errorResponse = response as FailedResponseModel;
        throw Exception('Failed to load more posts: ${errorResponse.message}');
      }
    } catch (e) {
      setState(() {
        _error = e.toString();
        _isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_error.isNotEmpty) {
      return Center(
        child: Text('Error: $_error', style: TextStyle(color: Colors.red)),
      );
    }

    return NotificationListener<ScrollNotification>(
      onNotification: (ScrollNotification scrollInfo) {
        if (scrollInfo.metrics.pixels == scrollInfo.metrics.maxScrollExtent) {
          _loadMorePosts();
        }
        return true;
      },
      child: ListView.builder(
        itemCount: _posts.length + (_isLoading ? 1 : 0),
        itemBuilder: (context, index) {
          if (index == _posts.length) {
            return const Center(child: CircularProgressIndicator());
          }

          final post = _posts[index];
          return Card(
            margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
            elevation: 2,
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    post.title,
                    style: Theme.of(context).textTheme.titleMedium?.copyWith(
                      fontWeight: FontWeight.bold,
                    ),
                  ),
                  const SizedBox(height: 8),
                  Text(post.body),
                  const SizedBox(height: 4),
                  Align(
                    alignment: Alignment.bottomRight,
                    child: Text(
                      'Post ID: ${post.id}',
                      style: Theme.of(context).textTheme.bodySmall,
                    ),
                  ),
                ],
              ),
            ),
          );
        },
      ),
    );
  }
}

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

  @override
  State createState() => _UsersPageState();
}

class _UsersPageState extends State<UsersPage> {
  List<User> _users = [];
  bool _isLoading = true;
  String _error = '';

  @override
  void initState() {
    super.initState();
    _fetchUsers();
  }

  Future<void> _fetchUsers() async {
    try {
      // Use dio_flow to fetch users
      final response = await DioRequestHandler.get('users');

      if (response is SuccessResponseModel) {
        final data = response.data as List;

        setState(() {
          // Convert JSON to models
          _users = List<User>.from(
            data.map((item) => User.fromJson(item as Map<String, dynamic>)),
          );
          _isLoading = false;
        });
      } else {
        // Handle error response
        final errorResponse = response as FailedResponseModel;
        throw Exception('Failed to load users: ${errorResponse.message}');
      }
    } catch (e) {
      setState(() {
        _error = e.toString();
        _isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_isLoading) {
      return const Center(child: CircularProgressIndicator());
    }

    if (_error.isNotEmpty) {
      return Center(
        child: Text('Error: $_error', style: TextStyle(color: Colors.red)),
      );
    }

    return ListView.builder(
      itemCount: _users.length,
      itemBuilder: (context, index) {
        final user = _users[index];
        return Card(
          margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
          child: ListTile(
            leading: CircleAvatar(child: Text(user.name[0])),
            title: Text(user.name),
            subtitle: Text(user.email),
            trailing: Text('ID: ${user.id}'),
          ),
        );
      },
    );
  }
}

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

  @override
  State createState() => _JsonUtilsPageState();
}

class _JsonUtilsPageState extends State<JsonUtilsPage> {
  // Sample data for demos
  final _sampleJson = '''
  {
    "user": {
      "profile": {
        "name": "John Doe",
        "age": 30,
        "address": {
          "street": "123 Main St",
          "city": "New York",
          "zip": "10001"
        }
      },
      "settings": {
        "notifications": true,
        "theme": "dark"
      }
    },
    "stats": {
      "visits": 243,
      "favorites": 15,
      "last_login": "2023-05-20T14:30:00Z"
    }
  }
  ''';

  late Map<String, dynamic> _parsedJson;
  String _extractedValue = '';
  String _normalizedResult = '';

  @override
  void initState() {
    super.initState();
    // Parse JSON safely using dio_flow's JsonUtils
    _parsedJson = JsonUtils.tryParseJson(_sampleJson) ?? {};
  }

  void _extractNestedValue(String path) {
    // Use dio_flow's JsonUtils for nested value extraction
    final value = JsonUtils.getNestedValue(_parsedJson, path, 'Not found');

    setState(() {
      _extractedValue = value.toString();
    });
  }

  void _demonstrateNormalization() {
    final weirdKeys = {
      'First_Name': 'Alice',
      'LAST-NAME': 'Smith',
      'phone_NUMBER': '123-456-7890',
      'EMAIL_ADDRESS': 'alice.smith@example.com',
    };

    // Use dio_flow's JsonUtils for key normalization
    final normalized = JsonUtils.normalizeJsonKeys(
      weirdKeys,
      keysToLowerCase: true,
    );

    setState(() {
      _normalizedResult = normalized.toString();
    });
  }

  Future<void> _demonstratePagination() async {
    try {
      if (!mounted) return;

      showDialog(
        context: context,
        barrierDismissible: false,
        builder: (BuildContext context) {
          return const AlertDialog(
            content: Column(
              mainAxisSize: MainAxisSize.min,
              children: [
                CircularProgressIndicator(),
                SizedBox(height: 16),
                Text('Fetching all pages...'),
              ],
            ),
          );
        },
      );

      // Use dio_flow's PaginationUtils to fetch all posts
      final response = await PaginationUtils.fetchAllPages(
        'posts',
        parameters: {'_limit': 20},
        pageParamName: '_page',
        perPageParamName: '_limit',
        startPage: 1,
      );

      if (!mounted) return;

      if (response is SuccessResponseModel) {
        final data = response.data as List;
        final allPosts =
            data
                .map((item) => Post.fromJson(item as Map<String, dynamic>))
                .toList();

        Navigator.of(context).pop(); // Close loading dialog

        // Show result dialog
        if (mounted) {
          showDialog(
            context: context,
            builder: (BuildContext context) {
              return AlertDialog(
                title: const Text('Pagination Complete'),
                content: Text(
                  'Successfully fetched ${allPosts.length} posts across multiple pages.',
                ),
                actions: [
                  TextButton(
                    onPressed: () => Navigator.of(context).pop(),
                    child: const Text('OK'),
                  ),
                ],
              );
            },
          );
        }
      } else {
        throw Exception('Failed to fetch all pages');
      }
    } catch (e) {
      if (!mounted) return;

      Navigator.of(context).pop(); // Close loading dialog

      // Show error dialog
      if (mounted) {
        showDialog(
          context: context,
          builder: (BuildContext context) {
            return AlertDialog(
              title: const Text('Error'),
              content: Text('Failed to fetch all pages: $e'),
              actions: [
                TextButton(
                  onPressed: () => Navigator.of(context).pop(),
                  child: const Text('OK'),
                ),
              ],
            );
          },
        );
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      padding: const EdgeInsets.all(16.0),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Text(
            'JSON Utilities Demo',
            style: Theme.of(context).textTheme.headlineSmall,
          ),
          const SizedBox(height: 16),

          // Sample JSON display
          Text('Sample JSON:', style: Theme.of(context).textTheme.titleMedium),
          Container(
            padding: const EdgeInsets.all(12),
            decoration: BoxDecoration(
              color: Colors.grey.shade200,
              borderRadius: BorderRadius.circular(8),
            ),
            width: double.infinity,
            child: Text(_sampleJson),
          ),
          const SizedBox(height: 24),

          // Nested value extraction
          Text(
            'Extract Nested Values:',
            style: Theme.of(context).textTheme.titleMedium,
          ),
          const SizedBox(height: 8),
          Wrap(
            spacing: 8,
            runSpacing: 8,
            children: [
              ActionChip(
                label: const Text('User Name'),
                onPressed: () => _extractNestedValue('user.profile.name'),
              ),
              ActionChip(
                label: const Text('City'),
                onPressed:
                    () => _extractNestedValue('user.profile.address.city'),
              ),
              ActionChip(
                label: const Text('Theme'),
                onPressed: () => _extractNestedValue('user.settings.theme'),
              ),
              ActionChip(
                label: const Text('Visit Count'),
                onPressed: () => _extractNestedValue('stats.visits'),
              ),
              ActionChip(
                label: const Text('Non-existent'),
                onPressed: () => _extractNestedValue('user.profile.phone'),
              ),
            ],
          ),
          const SizedBox(height: 8),
          if (_extractedValue.isNotEmpty)
            Container(
              padding: const EdgeInsets.all(12),
              decoration: BoxDecoration(
                color: Colors.blue.shade100,
                borderRadius: BorderRadius.circular(8),
              ),
              width: double.infinity,
              child: Text('Extracted value: $_extractedValue'),
            ),
          const SizedBox(height: 24),

          // Key normalization
          Text(
            'JSON Key Normalization:',
            style: Theme.of(context).textTheme.titleMedium,
          ),
          const SizedBox(height: 8),
          ElevatedButton(
            onPressed: _demonstrateNormalization,
            child: const Text('Normalize Inconsistent Keys'),
          ),
          const SizedBox(height: 8),
          if (_normalizedResult.isNotEmpty)
            Container(
              padding: const EdgeInsets.all(12),
              decoration: BoxDecoration(
                color: Colors.green.shade100,
                borderRadius: BorderRadius.circular(8),
              ),
              width: double.infinity,
              child: Text('Normalized result: $_normalizedResult'),
            ),

          const SizedBox(height: 24),

          // Information about dio_flow
          Card(
            color: Colors.purple.shade50,
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'About dio_flow Package',
                    style: Theme.of(context).textTheme.titleMedium,
                  ),
                  const SizedBox(height: 8),
                  Text(
                    'dio_flow provides powerful utilities for API integration:',
                    style: Theme.of(context).textTheme.bodyMedium,
                  ),
                  const SizedBox(height: 8),
                  _buildFeatureRow(
                    context,
                    'API Client',
                    'Simplified API requests with endpoint registration',
                  ),
                  _buildFeatureRow(
                    context,
                    'JSON Utilities',
                    'Safe parsing, dot notation access, key normalization',
                  ),
                  _buildFeatureRow(
                    context,
                    'Pagination Helpers',
                    'Fetch all pages, handle metadata, process paginated responses',
                  ),
                  _buildFeatureRow(
                    context,
                    'Model Extensions',
                    'Type-safe conversions from JSON to models and collections',
                  ),
                  _buildFeatureRow(
                    context,
                    'Response Handling',
                    'Standardized response models with clear success and error states',
                  ),
                ],
              ),
            ),
          ),

          const SizedBox(height: 24),

          // Add a demonstration of pagination helper
          Card(
            color: Colors.amber.shade50,
            child: Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'Pagination Utils Usage',
                    style: Theme.of(context).textTheme.titleMedium,
                  ),
                  const SizedBox(height: 8),
                  Text(
                    'PaginationUtils can be used to automatically fetch all pages:',
                    style: Theme.of(context).textTheme.bodyMedium,
                  ),
                  const SizedBox(height: 8),
                  ElevatedButton(
                    onPressed: () => _demonstratePagination(),
                    child: const Text('Demo Pagination'),
                  ),
                  const SizedBox(height: 8),
                  Text(
                    '''
// Example code:
final response = await PaginationUtils.fetchAllPages(
  'posts',
  parameters: {
    '_limit': 20,
  },
  pageParamName: '_page',
  perPageParamName: '_limit',
  startPage: 1,
);

if (response is SuccessResponseModel) {
  final data = response.data as List;
  final allPosts = data.map((item) => Post.fromJson(item as Map<String, dynamic>)).toList();
  print('Fetched \${allPosts.length} posts across multiple pages');
}
''',
                    style: Theme.of(context).textTheme.bodySmall?.copyWith(
                      fontFamily: 'monospace',
                      backgroundColor: Colors.grey.shade200,
                    ),
                  ),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildFeatureRow(
    BuildContext context,
    String title,
    String description,
  ) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4.0),
      child: Row(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Icon(Icons.check_circle, color: Colors.green, size: 16),
          const SizedBox(width: 8),
          Expanded(
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  title,
                  style: Theme.of(
                    context,
                  ).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold),
                ),
                Text(description, style: Theme.of(context).textTheme.bodySmall),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

// Model classes
class Post {
  final int id;
  final int userId;
  final String title;
  final String body;

  Post({
    required this.id,
    required this.userId,
    required this.title,
    required this.body,
  });

  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
      id: json['id'],
      userId: json['userId'],
      title: json['title'],
      body: json['body'],
    );
  }
}

class User {
  final int id;
  final String name;
  final String username;
  final String email;
  final String phone;
  final String website;

  User({
    required this.id,
    required this.name,
    required this.username,
    required this.email,
    required this.phone,
    required this.website,
  });

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'],
      name: json['name'],
      username: json['username'],
      email: json['email'],
      phone: json['phone'] ?? '',
      website: json['website'] ?? '',
    );
  }
}
3
likes
140
points
58
downloads

Publisher

verified publishermoeinmoradi.ir

Weekly Downloads

A powerful and flexible HTTP client for Flutter applications, built on top of Dio with advanced features like request queueing, caching, and automatic token management.

Repository (GitHub)
View/report issues

Documentation

Documentation
API reference

License

MIT (license)

Dependencies

colorful_log_plus, dio, flutter, log_curl_request, shared_preferences

More

Packages that depend on dio_flow