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
- Single Responsibility - Each service handles one domain
- Dependency Injection - Pass ApiClient in constructor
- Consistent Naming - Use verbs (get, create, update, delete)
- Error Handling - Let errors bubble to provider layer
- Type Safety - Use generic types for API responses
- Documentation - Document complex business logic
- No Flutter Dependencies - Keep services pure Dart
- Stateless - Services should not maintain state
- Testable - Design for easy mocking
- 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();
}