Skip to main content

Services Layer

The services layer provides a clean separation between UI and API logic, handling all backend communication and business logic.

Architecture

Services are pure Dart classes that encapsulate API calls and business logic. They have no Flutter dependencies and can be easily tested.

UI Layer (Widgets)

State Layer (Riverpod Providers)

Service Layer (Business Logic)

API Client (HTTP Communication)

Backend API

Service Pattern

All services follow a consistent pattern:

class ExampleService {
final ApiClient _api;

ExampleService(this._api);

Future<Model> getItem(String id) async {
final response = await _api.get<Map<String, dynamic>>('/items/$id');
return Model.fromJson(response.data!['item']);
}

Future<Model> createItem(Map<String, dynamic> data) async {
final response = await _api.post<Map<String, dynamic>>(
'/items',
data: data,
);
return Model.fromJson(response.data!['item']);
}

Future<void> deleteItem(String id) async {
await _api.delete('/items/$id');
}
}

Core Services

AuthService (auth_service.dart)

Handles authentication and user account management.

class AuthService {
final ApiClient _api;

AuthService(this._api);

// Guest session creation
Future<AuthResult> createGuest(String deviceId) async {
final response = await _api.post('/auth/guest', data: {'device_id': deviceId});
return AuthResult.fromJson(response.data);
}

// User registration
Future<AuthResult> signup(String email, String password, String displayName) async {
final response = await _api.post('/auth/signup', data: {
'email': email,
'password': password,
'display_name': displayName,
});
return AuthResult.fromJson(response.data);
}

// User login
Future<AuthResult> login(String email, String password) async {
final response = await _api.post('/auth/login', data: {
'email': email,
'password': password,
});
return AuthResult.fromJson(response.data);
}

// Token refresh
Future<TokenPair> refreshTokens(String refreshToken) async {
final response = await _api.post('/auth/refresh', data: {'refresh_token': refreshToken});
return TokenPair.fromJson(response.data);
}

// Guest to full account migration
Future<AuthResult> migrateGuest(String email, String password, String displayName) async {
final response = await _api.post('/auth/migrate-guest', data: {
'email': email,
'password': password,
'display_name': displayName,
});
return AuthResult.fromJson(response.data);
}

// Logout
Future<void> logout(String refreshToken) async {
await _api.post('/auth/logout', data: {'refresh_token': refreshToken});
}

// Profile management
Future<User> getProfile() async {
final response = await _api.get('/users/me');
return User.fromJson(response.data);
}

Future<User> updateProfile({String? displayName, String? username, String? avatarUrl}) async {
final response = await _api.put('/users/me', data: {
if (displayName != null) 'displayName': displayName,
if (username != null) 'username': username,
if (avatarUrl != null) 'avatarUrl': avatarUrl,
});
return User.fromJson(response.data);
}

Future<void> deleteAccount(String password) async {
await _api.delete('/users/me', data: {'password': password});
}
}

WishlistService (wishlist_service.dart)

Manages wishlists and wishlist items.

class WishlistService {
final ApiClient _api;

WishlistService(this._api);

Future<List<Wishlist>> getWishlists() async {
final response = await _api.get<Map<String, dynamic>>('/wishlists');
final wishlists = (response.data?['wishlists'] as List<dynamic>?)
?.map((json) => Wishlist.fromJson(json))
.toList() ?? [];
return wishlists;
}

Future<Wishlist> createWishlist(String name, {bool isPrimary = false, bool isPrivate = false}) async {
final response = await _api.post<Map<String, dynamic>>('/wishlists', data: {
'name': name,
'isPrimary': isPrimary,
'isPrivate': isPrivate,
});
return Wishlist.fromJson(response.data!['wishlist']);
}

Future<Wishlist> getWishlist(String id) async {
final response = await _api.get<Map<String, dynamic>>('/wishlists/$id');
return Wishlist.fromJson(response.data!['wishlist']);
}

Future<Wishlist> updateWishlist(String id, {String? name, bool? isPrivate}) async {
final response = await _api.put<Map<String, dynamic>>('/wishlists/$id', data: {
if (name != null) 'name': name,
if (isPrivate != null) 'isPrivate': isPrivate,
});
return Wishlist.fromJson(response.data!['wishlist']);
}

Future<void> deleteWishlist(String id) async {
await _api.delete('/wishlists/$id');
}

Future<WishlistItem> addItem(String wishlistId, Map<String, dynamic> book, {String priority = 'normal'}) async {
final isbn = book['isbn13'] ?? book['id'];
final response = await _api.post<Map<String, dynamic>>('/wishlists/$wishlistId/items', data: {
'isbn': isbn,
'bookData': book,
'priority': priority,
});
return WishlistItem.fromJson(response.data!['item']);
}

Future<WishlistItem> updateItem(String wishlistId, String itemId, {String? priority, String? status}) async {
final response = await _api.put<Map<String, dynamic>>('/wishlists/$wishlistId/items/$itemId', data: {
if (priority != null) 'priority': priority,
if (status != null) 'status': status,
});
return WishlistItem.fromJson(response.data!['item']);
}

Future<void> removeItem(String wishlistId, String itemId) async {
await _api.delete('/wishlists/$wishlistId/items/$itemId');
}
}

CartService (cart_service.dart)

Manages shopping cart operations.

class CartService {
final ApiClient _api;

CartService(this._api);

Future<CartWithTotals> getCartWithTotals() async {
final response = await _api.get('/cart');
return CartWithTotals.fromJson(response.data);
}

Future<void> addToCart(String bookId, int quantity, {String? storeId}) async {
await _api.post('/cart/items', data: {
'bookId': bookId,
'quantity': quantity,
if (storeId != null) 'storeId': storeId,
});
}

Future<void> updateCartItem(String itemId, int quantity) async {
await _api.put('/cart/items/$itemId', data: {'quantity': quantity});
}

Future<void> removeFromCart(String itemId) async {
await _api.delete('/cart/items/$itemId');
}

Future<CheckoutResult> checkout({
required String fulfillmentType,
String? shippingAddressId,
int? tradeCreditAppliedCents,
String? originContext,
}) async {
final response = await _api.post('/checkout', data: {
'fulfillmentType': fulfillmentType,
if (shippingAddressId != null) 'shippingAddressId': shippingAddressId,
if (tradeCreditAppliedCents != null) 'tradeCreditAppliedCents': tradeCreditAppliedCents,
if (originContext != null) 'originContext': originContext,
});
return CheckoutResult.fromJson(response.data);
}
}

BookService (book_service.dart)

Book search and information retrieval.

class BookService {
final ApiClient _api;

BookService(this._api);

Future<List<Book>> searchBooks(String query) async {
final response = await _api.get('/books/search', queryParameters: {'q': query});
return (response.data['books'] as List)
.map((json) => Book.fromJson(json))
.toList();
}

Future<Book> getBook(String id) async {
final response = await _api.get('/books/$id');
return Book.fromJson(response.data['book']);
}

Future<BookAvailability> checkAvailability(String isbn) async {
final response = await _api.get('/books/$isbn/availability');
return BookAvailability.fromJson(response.data);
}
}

Feature Services

StoreService (store_service.dart)

Bookstore management for store owners.

Key Operations:

  • Create/update store
  • Get store details
  • Manage home store preference
  • Upload store logo
  • Configure Square integration
  • Custom domain setup

InventoryService (inventory_service.dart)

Inventory management for bookstores.

Key Operations:

  • List inventory items
  • Add/update/delete books
  • Bulk import from CSV
  • Sync with Square
  • Track stock levels

POSService (pos_service.dart)

Point-of-sale operations.

Key Operations:

  • Create in-store orders
  • Process payments
  • Generate receipts
  • Track daily sales

SubscriptionService (subscription_service.dart)

Subscription and billing management.

Key Operations:

  • Get customer info (tier, entitlements)
  • Create checkout session
  • Manage subscriptions via RevenueCat
  • Handle platform-specific purchases (iOS/Android)

NotificationService (notification_service.dart)

Push notification handling.

Key Operations:

  • Register device token
  • Handle notification permissions
  • Mark notifications as read
  • Get notification list

API Client Integration

Services use ApiClient which provides:

  • Automatic token injection
  • Token refresh on 401
  • Error handling with ApiException
  • Request/response interceptors

Example error handling:

try {
final result = await service.performOperation();
return result;
} on ApiException catch (e) {
if (e.statusCode == 404) {
throw Exception('Resource not found');
} else if (e.statusCode == 403) {
throw Exception('Access denied');
}
rethrow;
} catch (e) {
throw Exception('Unexpected error: $e');
}

Service Providers

Services are exposed through Riverpod providers:


WishlistService wishlistService(Ref ref) {
return WishlistService(ApiClient());
}


CartService cartService(Ref ref) {
return CartService(ApiClient());
}

Usage in other providers:


class WishlistsNotifier extends _$WishlistsNotifier {
late final WishlistService _service;


Future<List<Wishlist>> build() async {
_service = ref.read(wishlistServiceProvider);
return _service.getWishlists();
}
}

Data Flow

Read Flow

Widget → ref.watch(provider) → Provider → Service → API Client → Backend

Write Flow

Widget → ref.read(provider.notifier).method() → Provider → Service → API Client → Backend

Provider.refresh()

Widget rebuilds

Testing Services

Services are easily testable due to dependency injection:

// Mock API client
class MockApiClient extends Mock implements ApiClient {}

void main() {
late WishlistService service;
late MockApiClient mockApi;

setUp(() {
mockApi = MockApiClient();
service = WishlistService(mockApi);
});

test('getWishlists returns list of wishlists', () async {
when(() => mockApi.get<Map<String, dynamic>>('/wishlists'))
.thenAnswer((_) async => Response(
data: {'wishlists': []},
statusCode: 200,
requestOptions: RequestOptions(path: '/wishlists'),
));

final result = await service.getWishlists();
expect(result, isEmpty);
verify(() => mockApi.get<Map<String, dynamic>>('/wishlists')).called(1);
});
}

Best Practices

  1. Single Responsibility - Each service handles one domain
  2. Dependency Injection - Pass ApiClient in constructor
  3. Consistent Naming - Use verbs (get, create, update, delete)
  4. Error Handling - Let errors bubble to provider layer
  5. Type Safety - Use generic types for API responses
  6. Documentation - Document complex business logic
  7. No Flutter Dependencies - Keep services pure Dart
  8. Stateless - Services should not maintain state
  9. Testable - Design for easy mocking
  10. Async/Await - Use async operations consistently

Common Patterns

List Operations

Future<List<Model>> getItems() async {
final response = await _api.get<Map<String, dynamic>>('/items');
return (response.data?['items'] as List<dynamic>?)
?.map((json) => Model.fromJson(json))
.toList() ?? [];
}

Single Item Operations

Future<Model> getItem(String id) async {
final response = await _api.get<Map<String, dynamic>>('/items/$id');
return Model.fromJson(response.data!['item']);
}

Create Operations

Future<Model> createItem(Map<String, dynamic> data) async {
final response = await _api.post<Map<String, dynamic>>('/items', data: data);
return Model.fromJson(response.data!['item']);
}

Update Operations

Future<Model> updateItem(String id, {String? name, int? value}) async {
final response = await _api.put<Map<String, dynamic>>('/items/$id', data: {
if (name != null) 'name': name,
if (value != null) 'value': value,
});
return Model.fromJson(response.data!['item']);
}

Delete Operations

Future<void> deleteItem(String id) async {
await _api.delete('/items/$id');
}

Query Parameters

Future<List<Model>> searchItems(String query, {int? limit}) async {
final response = await _api.get<Map<String, dynamic>>(
'/items/search',
queryParameters: {
'q': query,
if (limit != null) 'limit': limit,
},
);
return (response.data?['items'] as List)
.map((json) => Model.fromJson(json))
.toList();
}