SmartFetch

pub package License: MIT

A Flutter package that provides beautiful full-screen error pages and intelligent handling for server-side errors, no internet connection states, and timeouts. SmartFetch simplifies error handling in your Flutter applications with ready-to-use widgets and utilities.

Screenshots

No Internet

No Internet Screen

Server Error

Server Error Screen

Features

Automatic Error Detection: Distinguishes between no internet, server errors, and timeouts
🎨 Beautiful Error Screens: Pre-built, animated full-screen error pages using Lottie animations
🔄 Easy Integration: Simple API with both programmatic and widget-based approaches
🎯 Type-Safe Results: Strongly-typed result objects for clean error handling
📱 Accessible Error Screens: Use predefined screens directly or via helper methods
🌐 Connectivity Aware: Real-time internet connectivity checking
🚀 Flexible Usage: Three ways to use error screens - automatic, helper methods, or direct access

Getting Started

Step 1: Add Dependency

Add smart_fetch to your pubspec.yaml:

dependencies:
   smart_fetch: ^1.0.7
   http: ^1.6.0

Step 2: Declare Assets (Required for Lottie Animations)

The package includes Lottie animations for error screens. You must declare the assets in your pubspec.yaml:

flutter:
   assets:
      - packages/smart_fetch/assets/animations/

Important: Without this declaration, the Lottie animations won't load and you'll see asset loading errors.

Step 3: Install

Run:

flutter pub get

Note: The assets are bundled with the package, so you only need to declare them in your app's pubspec.yaml - you don't need to copy the files.

Quick Start

Method 1: Automatic Error Screens (Easiest)

Error screens are shown automatically with SmartFetch.builder():

import 'package:smart_fetch/smart_fetch.dart';
import 'package:http/http.dart' as http;

SmartFetch.builder(
  future: () => http.get(Uri.parse('https://api.example.com/data')),
  onSuccess: (context, response) => Text('Success: ${response.body}'),
  loadingWidget: CircularProgressIndicator(),
)
// Error screens show automatically!

Use SmartFetchNavigator for easy navigation to error screens:

import 'package:smart_fetch/smart_fetch.dart';
import 'package:http/http.dart' as http;

final result = await SmartFetch.call(
  () => http.get(Uri.parse('https://api.example.com/data')),
);

// One line to show the correct error screen
SmartFetchNavigator.showErrorScreen(context, result);

// Or show specific screens
SmartFetchNavigator.showNoInternet(context);
SmartFetchNavigator.showServerError(context);
SmartFetchNavigator.showTimeout(context);

Method 3: Direct Screen Access

Import and use error screens directly:

import 'package:smart_fetch/smart_fetch.dart';

Navigator.push(
  context,
  MaterialPageRoute(builder: (_) => const NoInternetScreen()),
);

Usage Examples

Basic API Call with Error Handling

Option 1: When you have BuildContext (in a Widget)

import 'package:smart_fetch/smart_fetch.dart';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';

class MyWidget extends StatelessWidget {
  Future<void> fetchData(BuildContext context) async {
    final result = await SmartFetch.call(
      () => http.get(Uri.parse('https://api.example.com/data')),
      timeout: const Duration(seconds: 10),
      context: context,
      errorSnackBar: const SnackBar(
        content: Text('Request failed. Please try again.'),
        backgroundColor: Colors.red,
      ),
    );

    if (result.isSuccess) {
      print('Success: ${result.response?.body}');
    } else {
      // Use helper method - automatically shows correct screen
      SmartFetchNavigator.showErrorScreen(context, result);
    }
  }

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () => fetchData(context),
      child: Text('Fetch Data'),
    );
  }
}

Option 2: When you don't have BuildContext (Service Classes)

Use a global navigator key and pass it to the helper methods:

// In main.dart
final navigatorKey = GlobalKey<NavigatorState>();

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: navigatorKey, // Add this
      home: HomeScreen(),
    );
  }
}

// In your service class
import 'package:smart_fetch/smart_fetch.dart';
import 'package:http/http.dart' as http;
import 'main.dart'; // Import to access navigatorKey

class ApiService {
  static Future<void> fetchData() async {
    final result = await SmartFetch.call(
      () => http.get(Uri.parse('https://api.example.com/data')),
    );

    if (result.isSuccess) {
      print('Success: ${result.response?.body}');
    } else {
      // Use navigator key to show error screen
      SmartFetchNavigator.showErrorScreenWithKey(navigatorKey, result);
    }
  }
}

Widget-Based Integration with Custom Error Handling

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:smart_fetch/smart_fetch.dart';
import 'package:http/http.dart' as http;

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

  @override
  State<LoginPage> createState() => _LoginPageState();
}

class _LoginPageState extends State<LoginPage> {
  final emailController = TextEditingController();
  final passwordController = TextEditingController();
  bool shouldLogin = false;
  int loginTrigger = 0;

  @override
  void dispose() {
    emailController.dispose();
    passwordController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Login')),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            TextField(
              controller: emailController,
              decoration: const InputDecoration(
                labelText: 'Email',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 16),
            TextField(
              controller: passwordController,
              obscureText: true,
              decoration: const InputDecoration(
                labelText: 'Password',
                border: OutlineInputBorder(),
              ),
            ),
            const SizedBox(height: 30),
            if (!shouldLogin)
              SizedBox(
                width: double.infinity,
                height: 50,
                child: ElevatedButton(
                  onPressed: () {
                    setState(() {
                      shouldLogin = true;
                      loginTrigger++;
                    });
                  },
                  child: const Text('Login'),
                ),
              )
            else
              SizedBox(
                height: 200,
                child: SmartFetch.builder(
                  key: ValueKey('login_fetch_$loginTrigger'),
                  future: () {
                    final body = {
                      "email": emailController.text,
                      "password": passwordController.text,
                    };
                    final headers = {
                      "Content-Type": "application/json",
                      "Accept": "application/json",
                    };
                    return http.post(
                      Uri.parse('https://api.example.com/login'),
                      headers: headers,
                      body: jsonEncode(body),
                    );
                  },
                  loadingWidget: const Center(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        CircularProgressIndicator(),
                        SizedBox(height: 16),
                        Text('Logging in...'),
                      ],
                    ),
                  ),
                  onSuccess: (context, response) {
                    if (response.statusCode == 200) {
                      // Handle successful login
                      return const Center(
                        child: Icon(
                          Icons.check_circle,
                          color: Colors.green,
                          size: 48,
                        ),
                      );
                    } else {
                      // Handle error response
                      setState(() {
                        shouldLogin = false;
                      });
                      return Center(
                        child: Text('Login failed: ${response.statusCode}'),
                      );
                    }
                  },
                  timeout: const Duration(seconds: 10),
                  errorSnackBar: const SnackBar(
                    content: Text('Login failed. Please check your connection and try again.'),
                    backgroundColor: Colors.orange,
                    duration: Duration(seconds: 4),
                  ),
                  onError: (context, error) {
                    WidgetsBinding.instance.addPostFrameCallback((_) async {
                      final errorScreen = error.isTimeout
                          ? const TimeoutScreen()
                          : error.isNoInternet
                              ? const NoInternetScreen()
                              : const ServerErrorScreen();

                      await Navigator.push(
                        context,
                        MaterialPageRoute(builder: (_) => errorScreen),
                      );

                      if (mounted) {
                        setState(() {
                          shouldLogin = false;
                        });
                      }
                    });
                    return const SizedBox.shrink();
                  },
                ),
              ),
          ],
        ),
      ),
    );
  }
}

Using Error Screens in Service Classes (No Context Needed!)

Perfect for service classes where you don't have BuildContext:

Step 1: Create navigator key in main.dart

final navigatorKey = GlobalKey<NavigatorState>();

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: navigatorKey,
      home: HomeScreen(),
    );
  }
}

Step 2: Use in service class with navigator key

// In your service class
import 'main.dart'; // Import to access navigatorKey

final result = await SmartFetch.call(
  () => http.post(Uri.parse(apiUrl), body: body),
);

if (result.isSuccess) {
  // Handle success
} else {
  // Show error screen using navigator key
  SmartFetchNavigator.showErrorScreenWithKey(navigatorKey, result);
}

Manual Connectivity Check

import 'package:smart_fetch/smart_fetch.dart';

final hasInternet = await InternetChecker.hasInternet();
if (hasInternet) {
  // Proceed with API call
} else {
  SmartFetchNavigator.showNoInternet(context);
}

Asset Configuration

Lottie Animations

The package includes Lottie animations for error screens. You must declare the assets in your app's pubspec.yaml:

flutter:
   assets:
      - packages/smart_fetch/assets/animations/

Important Notes:

  • Use the packages/smart_fetch/ prefix to access package assets
  • The assets are bundled with the package - you don't need to copy them
  • After adding assets, run flutter pub get and restart your app
  • If assets fail to load, the screens will show fallback icons automatically

Troubleshooting Asset Loading

If you see "Unable to load asset" or "Asset not found" errors:

  1. Check your pubspec.yaml - Make sure assets are declared:

    flutter:
       assets:
          - packages/smart_fetch/assets/animations/
    
  2. Run flutter pub get after adding assets

  3. Restart your app completely (hot reload won't pick up asset changes)

  4. Try flutter clean if issues persist:

    flutter clean
    flutter pub get
    flutter run
    

The screens include fallback icons if Lottie assets fail to load, so your app will still work even if there are asset issues.

Available Error Screens

The package provides three pre-built error screens:

  • NoInternetScreen - Shown when device has no internet connectivity
  • ServerErrorScreen - Shown when server returns an error
  • TimeoutScreen - Shown when request exceeds timeout duration

All screens include:

  • Beautiful Lottie animations
  • Clear error messages
  • "Try Again" button for easy retry

API Reference

SmartFetch.call()

Makes an HTTP request with automatic error handling.

Parameters:

  • request: A function that returns a Future of http.Response
  • timeout: Optional timeout duration (default: 10 seconds)
  • context: Optional BuildContext to show error SnackBar when error occurs
  • errorSnackBar: Optional SnackBar widget to show when error occurs (requires context)

Returns: SmartFetchResult with status and response data

SmartFetch.builder()

A widget builder that automatically handles loading, success, and error states.

Parameters:

  • key: Optional key for the FutureBuilder widget (useful for rebuilding)
  • future: A function that returns a Future of http.Response
  • onSuccess: Widget builder called when request succeeds
  • loadingWidget: Optional custom loading widget
  • timeout: Optional timeout duration (default: 10 seconds)
  • showErrorOnSamePage: If true, shows error screens inline on the same page. If false (default), uses onError callback to navigate to error screen
  • onError: Callback called when an error occurs and showErrorOnSamePage is false (default behavior)
  • errorSnackBar: Optional SnackBar widget to show when error occurs
  • noInternetTitle: Optional custom title for no internet screen
  • noInternetDescription: Optional custom description for no internet screen
  • timeoutTitle: Optional custom title for timeout screen
  • timeoutDescription: Optional custom description for timeout screen
  • serverErrorTitle: Optional custom title for server error screen
  • serverErrorDescription: Optional custom description for server error screen

SmartFetchNavigator

Helper class with static methods for navigating to error screens:

  • showNoInternet(context) - Show no internet screen
  • showServerError(context) - Show server error screen
  • showTimeout(context) - Show timeout screen
  • showErrorScreen(context, result) - Automatically show correct screen
  • showErrorScreenWithKey(navigatorKey, result) - Use with global navigator key

All methods support an optional replace parameter to replace the current route.

Error Screens

Direct access to error screen widgets:

  • NoInternetScreen() - No internet connection screen
  • ServerErrorScreen() - Server error screen
  • TimeoutScreen() - Timeout screen

Example

Check out the complete example in the /example folder:

cd example
flutter run

License

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgments

  • Uses Lottie for beautiful animations
  • Uses connectivity_plus for network detection
  • Built with ❤️ for the Flutter community

Libraries

smart_fetch
A Flutter package for handling network requests with beautiful error screens.