Flutter Next Base

A comprehensive Flutter package for interacting with Frappe/ERPNext REST APIs. This package provides easy-to-use methods for resource operations, doctype queries, and custom method calls.

Features

  • πŸ”„ Resource Operations: Full CRUD operations for any Frappe DocType
  • πŸ“„ DocType Operations: Get documents, field values, and update values
  • 🎯 Custom Methods: Call any custom Frappe server-side method
  • πŸ” Advanced Queries: Filter, sort, and paginate resource lists
  • πŸ›‘οΈ Type Safety: Strong typing with Dart generics
  • πŸ“ Comprehensive Logging: Built-in logging for debugging
  • πŸͺ Cookie Support: Seamless authentication cookie management
  • ⚑ Result Wrapper: Clean error handling with ApiResult<T>

Installation

Add this to your package's pubspec.yaml file:

dependencies:
  flutter_next_base:
    git:
      url: https://github.com/handoud/flutter_next_base.git

Or if published to pub.flutter-io.cn:

dependencies:
  flutter_next_base: ^0.1.0

Then run:

flutter pub get

Quick Start

Initialize the Client

import 'package:flutter_next_base/flutter_next_base.dart';

final client = FlutterNextBaseClient(
  baseUrl: 'https://your-frappe-site.com',
  cookieManager: yourCookieManager, // Optional
);

Resource Operations

Get a List of Resources

final result = await client.getResourceList(
  'Delivery Request',
  options: QueryOptions(
    filters: '[["status", "=", "Open"]]',
    fields: ['name', 'customer', 'delivery_date', 'status'],
    orderBy: 'creation desc',
    limitPageLength: 20,
    limitStart: 0,
  ),
);

if (result.isSuccess) {
  final data = result.data!['data'] as List;
  print('Found ${data.length} delivery requests');
} else {
  print('Error: ${result.error!.message}');
}

Get a Single Resource

final result = await client.getResource('Delivery Request', 'DR-00001');

if (result.isSuccess) {
  final doc = result.data!['data'];
  print('Customer: ${doc['customer']}');
}

Create a Resource

final result = await client.createResource('Delivery Request', {
  'customer': 'CUST-00001',
  'delivery_date': '2025-12-15',
  'status': 'Open',
});

if (result.isSuccess) {
  final newDoc = result.data!['data'];
  print('Created: ${newDoc['name']}');
}

Update a Resource

final result = await client.updateResource(
  'Delivery Request',
  'DR-00001',
  {'status': 'In Transit'},
);

Delete a Resource

final result = await client.deleteResource('Delivery Request', 'DR-00001');

DocType Operations

Get Complete Document

Use getDoc when you need full document details including child tables:

final result = await client.getDoc('Delivery Trip', 'DT-00001');

if (result.isSuccess) {
  final doc = result.data!['docs'][0];
  print('Trip: ${doc['name']}');
  print('Stops: ${doc['stops'].length}');
}

Get Field Value

final result = await client.getValue(
  'User',
  'user@example.com',
  'full_name',
);

if (result.isSuccess) {
  final value = result.data!['message']['full_name'];
  print('Name: $value');
}

Set Field Value

final result = await client.setValue(
  'Delivery Request',
  'DR-00001',
  'status',
  'Completed',
);

Custom Method Calls

For custom server-side methods created in your Frappe app:

// POST request
final result = await client.callMethod(
  'delivery.api.update_status',
  data: {
    'delivery_request': 'DR-00001',
    'status': 'In Transit',
    'notes': 'Out for delivery',
  },
);

// GET request
final result = await client.callMethodGet(
  'delivery.api.get_statistics',
  queryParams: {'date': '2025-12-14'},
);

Advanced Usage

Filtering Resources

Frappe uses JSON arrays for filters. Each filter is: [fieldname, operator, value]

// Single filter
filters: '[["status", "=", "Open"]]'

// Multiple filters (AND condition)
filters: '[["status", "=", "Open"], ["delivery_date", ">=", "2025-12-01"]]'

// Using different operators
filters: '[["amount", ">", 1000], ["customer", "like", "%Corp%"]]'

Available Operators: =, !=, >, <, >=, <=, like, not like, in, not in, is, is not

Pagination

final options = QueryOptions(
  limitPageLength: 20,  // Records per page
  limitStart: 0,        // Starting index (0 for first page, 20 for second, etc.)
);

// For second page:
limitStart: 20

Sorting

// Ascending
orderBy: 'creation asc'

// Descending
orderBy: 'creation desc'

// Multiple fields
orderBy: 'status asc, creation desc'

Field Selection

Reduce response size by selecting specific fields:

fields: ['name', 'customer', 'status']  // Only these fields will be returned

Error Handling

final result = await client.getResource('Delivery Request', 'DR-00001');

if (result.isSuccess) {
  // Handle success
  final doc = result.data!['data'];
  print(doc);
} else {
  // Handle error
  final error = result.error!;
  print('Error: ${error.message}');
  print('Status Code: ${error.statusCode}');
  print('Data: ${error.data}');
}

Result Mapping

Transform API responses easily:

final result = await client.getResource('Delivery Request', 'DR-00001');

final mappedResult = result.map((data) {
  final doc = data['data'] as Map<String, dynamic>;
  return DeliveryRequest.fromJson(doc);
});

if (mappedResult.isSuccess) {
  final deliveryRequest = mappedResult.data!;
  print(deliveryRequest.customer);
}

Implement the CookieManager interface for authentication:

class MyCookieManager implements CookieManager {
  final FlutterSecureStorage _storage;

  MyCookieManager(this._storage);

  @override
  Future<String> getCookies(String url) async {
    final sid = await _storage.read(key: 'sid');
    final userId = await _storage.read(key: 'user_id');
    
    if (sid != null && userId != null) {
      return 'sid=$sid; user_id=$userId';
    }
    return '';
  }

  @override
  Future<void> saveCookies(String url, List<String> cookies) async {
    for (final cookie in cookies) {
      if (cookie.startsWith('sid=')) {
        final sid = cookie.split(';')[0].split('=')[1];
        await _storage.write(key: 'sid', value: sid);
      }
      if (cookie.startsWith('user_id=')) {
        final userId = cookie.split(';')[0].split('=')[1];
        await _storage.write(key: 'user_id', value: userId);
      }
    }
  }
}

// Use it:
final client = FlutterNextBaseClient(
  baseUrl: 'https://your-site.com',
  cookieManager: MyCookieManager(secureStorage),
);

Logging

Enable logging to debug API calls:

import 'package:logging/logging.dart';

void setupLogging() {
  Logger.root.level = Level.ALL;
  Logger.root.onRecord.listen((record) {
    print('${record.level.name}: ${record.time}: ${record.message}');
  });
}

API Reference

FlutterNextBaseClient

Main client class for API operations.

Constructor:

  • baseUrl: Your Frappe site URL
  • cookieManager: Optional cookie manager for authentication
  • defaultHeaders: Optional default headers for all requests
  • client: Optional custom HTTP client

Resource Methods:

  • getResourceList(docType, {options}): Get list of resources
  • getResource(docType, name): Get single resource
  • createResource(docType, data): Create new resource
  • updateResource(docType, name, data): Update resource
  • deleteResource(docType, name): Delete resource

DocType Methods:

  • getDoc(docType, name): Get complete document with child tables
  • getValue(docType, name, fieldName): Get specific field value
  • setValue(docType, name, fieldName, value): Set field value

Custom Methods:

  • callMethod(methodPath, {data, queryParams}): POST to custom method
  • callMethodGet(methodPath, {queryParams}): GET to custom method

QueryOptions

Options for filtering and sorting resource queries.

Properties:

  • filters: JSON filter string
  • fields: List of field names to return
  • orderBy: Sort order
  • limitPageLength: Records per page
  • limitStart: Starting index for pagination

ApiResult

Result wrapper for API operations.

Properties:

  • data: The returned data (if successful)
  • error: The error (if failed)
  • isSuccess: Whether operation succeeded
  • isError: Whether operation failed

Methods:

  • map<R>(mapper): Transform the data to another type

FrappeException

Exception thrown when API request fails.

Properties:

  • message: Error message
  • statusCode: HTTP status code
  • data: Additional error data

Common Use Cases

1. Building a List Screen with Pagination

class DeliveryListScreen extends StatefulWidget {
  @override
  _DeliveryListScreenState createState() => _DeliveryListScreenState();
}

class _DeliveryListScreenState extends State<DeliveryListScreen> {
  List<Map<String, dynamic>> deliveries = [];
  int currentPage = 0;
  final pageSize = 20;
  
  Future<void> loadDeliveries() async {
    final result = await client.getResourceList(
      'Delivery Request',
      options: QueryOptions(
        filters: '[["status", "!=", "Cancelled"]]',
        fields: ['name', 'customer', 'status', 'delivery_date'],
        orderBy: 'creation desc',
        limitPageLength: pageSize,
        limitStart: currentPage * pageSize,
      ),
    );
    
    if (result.isSuccess) {
      setState(() {
        deliveries.addAll(
          List<Map<String, dynamic>>.from(result.data!['data']),
        );
        currentPage++;
      });
    }
  }
}

2. Updating Status with Custom Method

Future<void> updateDeliveryStatus(String requestId, String status) async {
  final result = await client.callMethod(
    'delivery.api.update_status',
    data: {
      'delivery_request': requestId,
      'status': status,
      'timestamp': DateTime.now().toIso8601String(),
    },
  );
  
  if (result.isSuccess) {
    print('Status updated successfully');
  } else {
    print('Failed: ${result.error!.message}');
  }
}

3. Search with Filters

Future<List<Map<String, dynamic>>> searchCustomers(String query) async {
  final result = await client.getResourceList(
    'Customer',
    options: QueryOptions(
      filters: '[["customer_name", "like", "%$query%"]]',
      fields: ['name', 'customer_name', 'email'],
      limitPageLength: 10,
    ),
  );
  
  if (result.isSuccess) {
    return List<Map<String, dynamic>>.from(result.data!['data']);
  }
  return [];
}

Frappe API Documentation

For more information about Frappe's REST API:

Contributing

Contributions are welcome! Please feel free to submit a Pull Request to https://github.com/handoud/flutter_next_base.

License

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

Support

For issues, questions, or contributions, please visit: https://github.com/handoud/flutter_next_base/issues

Libraries

flutter_next_base
Flutter Next Base - A comprehensive client for Frappe/ERPNext APIs