Skip to content

pl_api_helper is a Flutter plugin that simplifies API calls, caching, and data management with support for both Dio and HTTP clients. It features intelligent caching, automatic token refresh, comprehensive error handling, and full type safety with generic response mapping. The plugin provides performance benefits through reduced network calls, fast

License

Notifications You must be signed in to change notification settings

NexPlugs/pl_api_helper

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

25 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

pl_api_helper icon

pl_api_helper

A comprehensive Flutter plugin for simplified API calls, caching, and model mapping with support for both Dio and standard HTTP clients.

pub package License: MIT Flutter


πŸ“‹ Table of Contents


πŸš€ Features

πŸ”₯ Core Functionality

  • πŸ”„ Dual HTTP Client Support - Choose between Dio (advanced) or standard HTTP client
  • 🧠 Intelligent Caching - Memory and disk caching with network awareness
  • πŸ›‘οΈ Generic Type Safety - Full type safety with generic response mapping
  • ⚑ Multi-threading Support - Background parsing to prevent UI blocking
  • 🎯 Comprehensive Error Handling - Detailed error classification and handling

πŸ”§ Advanced Features

  • πŸ” Automatic Token Management - Built-in token refresh and authentication
  • πŸ”— Request/Response Interceptors - Customizable request and response processing
  • πŸ“‘ Network Awareness - Smart caching based on connectivity status
  • 🌊 Stream Support - Real-time data streaming capabilities
  • πŸ“Š GraphQL Integration - Built-in GraphQL client support

πŸ‘¨β€πŸ’» Developer Experience

  • πŸ—οΈ Singleton Pattern - Global access to API helpers
  • βš™οΈ Flexible Configuration - Easy setup with sensible defaults
  • πŸ“ Comprehensive Logging - Built-in logging with release mode optimization
  • 🎨 Widget Integration - Ready-to-use widgets for common scenarios

πŸ“¦ Installation

Add to your pubspec.yaml:

dependencies:
  pl_api_helper: ^0.0.1

Then run:

flutter pub get

πŸš€ Quick Start

1. Initialize DioApiHelper (Recommended)

import 'package:dio/dio.dart';
import 'package:pl_api_helper/pl_api_helper.dart';

void main() {
  // Initialize DioApiHelper with configuration
  DioApiHelper.init(
    dio: Dio(
      BaseOptions(
        baseUrl: 'https://your-api-domain.com',
        contentType: Headers.jsonContentType,
        headers: {
          'Content-Type': 'application/json',
          'Accept': 'application/json',
          'Authorization': 'Bearer <your-token>',
        },
        connectTimeout: Duration(seconds: 30),
        receiveTimeout: Duration(seconds: 30),
      ),
    ),
    baseUrl: 'https://your-api-domain.com',
  );
}

2. Alternative: Initialize HttpHelper (Lightweight)

import 'package:pl_api_helper/pl_api_helper.dart';

void main() {
  // Initialize HttpHelper for simple HTTP requests
  HttpHelper.init(
    baseUrl: 'https://your-api-domain.com',
    apiConfig: ApiConfig(
      baseUrl: 'https://your-api-domain.com',
      defaultHeaders: {
        'Content-Type': 'application/json',
        'Accept': 'application/json',
      },
      timeout: Duration(seconds: 30),
    ),
  );
}

πŸ“‘ Making API Calls

Basic API Calls

// GET request with caching
final result = await DioApiHelper.instance.get<User>(
  url: '/api/users/123',
  cacheConfig: CacheConfig(duration: Duration(minutes: 5)),
  mapper: (data) => User.fromJson(data),
);

// POST request
final newUser = await DioApiHelper.instance.post<User>(
  url: '/api/users',
  request: {'name': 'John Doe', 'email': '[email protected]'},
  mapper: (data) => User.fromJson(data),
);

// PUT request
final updatedUser = await DioApiHelper.instance.put<User>(
  url: '/api/users/123',
  request: {'name': 'Jane Doe'},
  mapper: (data) => User.fromJson(data),
);

// DELETE request
await DioApiHelper.instance.delete<void>(
  url: '/api/users/123',
  mapper: (data) => null,
);

Advanced Caching Configuration

// Custom cache configuration
final cacheConfig = CacheConfig(
  duration: Duration(hours: 1),
  useMemoryCache: true,
  useDiskCache: true,
  maxCacheSize: 50 * 1024 * 1024, // 50MB
  onlyGetWhenDisconnected: false,
);

final result = await DioApiHelper.instance.get<List<Post>>(
  url: '/api/posts',
  cacheConfig: cacheConfig,
  mapper: (data) => (data as List).map((json) => Post.fromJson(json)).toList(),
);

πŸ” Authentication & Token Management

Automatic Token Refresh

// Step 1: Implement TokenDelegation
class MyTokenDelegate implements TokenDelegation {
  @override
  Future<String> getAccessToken() async {
    // Load from secure storage
    return await SecureStorage.read('access_token') ?? '';
  }
  
  @override
  Future<String> getRefreshToken() async {
    return await SecureStorage.read('refresh_token') ?? '';
  }
  
  @override
  Future<void> saveAccessToken(String token) async {
    await SecureStorage.write('access_token', token);
  }
  
  @override
  Future<void> saveRefreshToken(String token) async {
    await SecureStorage.write('refresh_token', token);
  }
  
  @override
  Future<void> saveTokens(String accessToken, String? refreshToken) async {
    await saveAccessToken(accessToken);
    if (refreshToken != null) await saveRefreshToken(refreshToken);
  }
  
  @override
  Future<void> deleteToken() async {
    await SecureStorage.delete('access_token');
    await SecureStorage.delete('refresh_token');
  }
}

// Step 2: Add Token Interceptor
DioApiHelper.instance.addInterceptor(
  DioTokenInterceptor(
    baseUrl: 'https://api.example.com',
    refreshEndpoint: '/auth/refresh',
    refreshPayloadBuilder: (refreshToken) => {
      'refreshToken': refreshToken,
    },
    tokenDelegate: MyTokenDelegate(),
    onUnauthenticated: () {
      // Handle logout or navigation to login
      Navigator.pushReplacementNamed(context, '/login');
    },
  ),
);

πŸ“₯ File Downloading with DownloadManager

The DownloadManager provides chunked file downloading with pause/cancel and progress streaming.

Initialize

import 'package:dio/dio.dart';
import 'package:pl_api_helper/pl_api_helper.dart';

void main() {
  // Optional: customize chunk behavior
  final config = DownloadConfig(
    chunkSize: 1024 * 1024 * 10, // 10MB per chunk
    chunkTimeout: 10000,         // 10s timeout per chunk
    chunkRetries: 3,             // retry count per chunk
    chunkRetryDelay: 1000,       // 1s delay between retries
  );

  DownloadManager.init(
    dio: Dio(),
    downloadConfig: config,
  );
}

Start a download

final int fileId = 1; // unique id for your task
final String url = 'https://example.com/files/report.pdf';
final String fileName = 'report.pdf';

// Kicks off the download and returns a Future<DownloadResult>
final futureResult = DownloadManager.instance.downloadFile(
  fileId,
  url,
  fileName,
);

// Optionally await the final result
final result = await futureResult;
if (result.isSuccess) {
  print('Downloaded to: ${result.filePath}');
} else {
  print('Download failed: ${result.error}');
}

Listen to progress

final sub = DownloadManager.instance.watchProcess(fileId).listen((model) {
  // model: DownloadModel
  print('Status: ${model.status}');
  print('Bytes: ${model.bytesDownloaded}/${model.totalSize}');
  print('Progress: ${(model.progress * 100).toStringAsFixed(1)}%');
});

// Remember to cancel the subscription when no longer needed
// await sub.cancel();

Pause and cancel

// Pause (keeps task state as pending)
await DownloadManager.instance.pause(fileId);

// Cancel (marks as failed and cleans up)
await DownloadManager.instance.cancel(fileId);

Inspect or cleanup

final model = await DownloadManager.instance.getDownloadModel(fileId);
print('Current status: ${model.status}');

// Remove task state and close its stream
DownloadManager.instance.deleteDownloadModel(fileId);

Notes:

  • Import package:pl_api_helper/pl_api_helper.dart to access DownloadManager, DownloadConfig, DownloadModel and DownloadResult.
  • Progress is emitted via watchProcess(taskId) as DownloadModel updates.
  • Customize chunking behavior via DownloadConfig during DownloadManager.init.

🎯 Model Examples

User Model

class User {
  final String id;
  final String name;
  final String email;
  final DateTime createdAt;

  User({
    required this.id,
    required this.name,
    required this.email,
    required this.createdAt,
  });

  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'] as String,
      name: json['name'] as String,
      email: json['email'] as String,
      createdAt: DateTime.parse(json['createdAt'] as String),
    );
  }

  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'email': email,
      'createdAt': createdAt.toIso8601String(),
    };
  }
}

Post Model with Comments

class Post {
  final String id;
  final String title;
  final String content;
  final String authorId;
  final List<Comment> comments;
  final DateTime createdAt;

  Post({
    required this.id,
    required this.title,
    required this.content,
    required this.authorId,
    required this.comments,
    required this.createdAt,
  });

  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
      id: json['id'] as String,
      title: json['title'] as String,
      content: json['content'] as String,
      authorId: json['authorId'] as String,
      comments: (json['comments'] as List<dynamic>?)
          ?.map((e) => Comment.fromJson(e as Map<String, dynamic>))
          .toList() ?? [],
      createdAt: DateTime.parse(json['createdAt'] as String),
    );
  }
}

class Comment {
  final String id;
  final String content;
  final String authorId;
  final DateTime createdAt;

  Comment({
    required this.id,
    required this.content,
    required this.authorId,
    required this.createdAt,
  });

  factory Comment.fromJson(Map<String, dynamic> json) {
    return Comment(
      id: json['id'] as String,
      content: json['content'] as String,
      authorId: json['authorId'] as String,
      createdAt: DateTime.parse(json['createdAt'] as String),
    );
  }
}

🎨 Widget Integration

Complete Example App

class MyApp extends StatefulWidget {
  @override
  State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  List<Post> posts = [];
  bool isLoading = false;
  String? errorMessage;

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

  Future<void> _loadPosts() async {
    setState(() {
      isLoading = true;
      errorMessage = null;
    });

    try {
      final result = await DioApiHelper.instance.get<List<Post>>(
        url: '/api/posts',
        cacheConfig: CacheConfig(duration: Duration(minutes: 10)),
        mapper: (data) => (data as List)
            .map((json) => Post.fromJson(json as Map<String, dynamic>))
            .toList(),
      );
      
      setState(() {
        posts = result;
        isLoading = false;
      });
    } catch (e) {
      setState(() {
        isLoading = false;
        if (e is ApiError) {
          errorMessage = 'Error: ${e.message} (${e.type})';
        } else {
          errorMessage = 'Unknown error occurred';
        }
      });
    }
  }

  Future<void> _createPost() async {
    try {
      final newPost = await DioApiHelper.instance.post<Post>(
        url: '/api/posts',
        request: {
          'title': 'New Post',
          'content': 'This is a new post created via API',
        },
        mapper: (data) => Post.fromJson(data as Map<String, dynamic>),
      );
      
      setState(() {
        posts = [newPost, ...posts];
      });
    } catch (e) {
      setState(() {
        if (e is ApiError) {
          errorMessage = 'Failed to create post: ${e.message}';
        }
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Posts App'),
          actions: [
            IconButton(
              onPressed: _loadPosts,
              icon: const Icon(Icons.refresh),
            ),
          ],
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: _createPost,
          child: const Icon(Icons.add),
        ),
        body: _buildBody(),
      ),
    );
  }

  Widget _buildBody() {
    if (isLoading) {
      return const Center(child: CircularProgressIndicator());
    }

    if (errorMessage != null) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(errorMessage!, style: const TextStyle(color: Colors.red)),
            ElevatedButton(
              onPressed: _loadPosts,
              child: const Text('Retry'),
            ),
          ],
        ),
      );
    }

    if (posts.isEmpty) {
      return const Center(child: Text('No posts available'));
    }

    return ListView.builder(
      itemCount: posts.length,
      itemBuilder: (context, index) {
        final post = posts[index];
        return Card(
          margin: const EdgeInsets.all(8.0),
          child: ListTile(
            title: Text(post.title),
            subtitle: Text(post.content),
            trailing: Text(post.createdAt.toString().split(' ')[0]),
          ),
        );
      },
    );
  }
}

πŸ› οΈ Advanced Features

Custom Interceptors

class LoggingInterceptor extends BaseInterceptor {
  @override
  Future<void> onRequest({
    required String method,
    required String url,
    Map<String, dynamic>? headers,
    Map<String, dynamic>? queryParameters,
    Map<String, dynamic>? body,
  }) async {
    Logger.d('API Request', '$method $url');
    Logger.d('API Headers', headers.toString());
    Logger.d('API Body', body.toString());
  }

  @override
  Future<void> onResponse(http.Response response) async {
    Logger.d('API Response', '${response.statusCode} ${response.body}');
  }
}

// Add to your helper
HttpHelper.instance.addInterceptor(LoggingInterceptor());

Error Handling

try {
  final result = await DioApiHelper.instance.get<User>('/api/user');
} on ApiError catch (e) {
  switch (e.type) {
    case ApiErrorType.noInternet:
      // Handle no internet
      break;
    case ApiErrorType.unauthorized:
      // Handle unauthorized
      break;
    case ApiErrorType.timeout:
      // Handle timeout
      break;
    default:
      // Handle other errors
      break;
  }
}

Cache Management

// Clear all cache
await CacherManager.instance.clear();

// Get cache size
final size = await CacherManager.instance.getCacheSize();
print('Cache size: ${size / 1024 / 1024} MB');

// Remove specific cache
await CacherManager.instance.removeData('/api/posts');

πŸ“š API Reference

DioApiHelper

Method Description
get<T>() GET request with caching support
post<T>() POST request
put<T>() PUT request
delete<T>() DELETE request
uploadFile<T>() File upload (not implemented)
addInterceptor() Add Dio interceptors

HttpHelper

Method Description
get<T>() GET request with caching support
post<T>() POST request
put<T>() PUT request
delete<T>() DELETE request
addInterceptor() Add HTTP interceptors

CacheConfig

Property Type Description
duration Duration Cache expiration time
useMemoryCache bool Enable memory caching
useDiskCache bool Enable disk caching
maxCacheSize int Maximum cache size in bytes
onlyGetWhenDisconnected bool Use cache only when offline

ApiError

Property Type Description
type ApiErrorType Error type (noInternet, timeout, unauthorized, etc.)
message String? Human-readable error message
statusCode int? HTTP status code
errorCode String? Application-specific error code

🀝 Contributing

We welcome contributions! Please see our contributing guidelines:

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Development Setup

# Clone the repository
git clone https://github.com/your-username/pl_api_helper.git

# Navigate to the plugin directory
cd pl_api_helper

# Install dependencies
flutter pub get

# Run tests
flutter test

# Run example app
cd example
flutter run

πŸ“„ License

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


πŸ™ Acknowledgments

We'd like to thank the following projects and contributors:

  • Dio - Powerful HTTP client for Dart
  • HTTP - A composable, multi-platform, Future-based library for making HTTP requests
  • Shared Preferences - Flutter plugin for reading and writing simple key-value pairs
  • Connectivity Plus - Flutter plugin for discovering the state of the network connectivity

Made with ❀️ for the Flutter community

⭐ Star this repo β€’ πŸ› Report Bug β€’ πŸ’‘ Request Feature

About

pl_api_helper is a Flutter plugin that simplifies API calls, caching, and data management with support for both Dio and HTTP clients. It features intelligent caching, automatic token refresh, comprehensive error handling, and full type safety with generic response mapping. The plugin provides performance benefits through reduced network calls, fast

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages