Flutter Integration
The Artbase Creator Hub is a Flutter mobile app using Riverpod for state management and Supabase for the backend.
Project Structure
apps/creator_hub/
├── lib/
│ ├── core/
│ │ ├── models/ # Data models
│ │ ├── providers/ # Riverpod providers
│ │ ├── services/ # API services
│ │ └── utils/ # Utilities
│ ├── features/
│ │ ├── auth/ # Authentication screens
│ │ ├── dashboard/ # Main dashboard
│ │ ├── products/ # Product management
│ │ ├── orders/ # Order management
│ │ ├── analytics/ # Analytics views
│ │ ├── channels/ # Channel connections
│ │ └── settings/ # App settings
│ └── main.dart
├── test/
├── pubspec.yaml
└── analysis_options.yaml
State Management with Riverpod
Provider Types
// Simple provider
final greetingProvider = Provider<String>((ref) => 'Hello, Creator!');
// Future provider for async data
final productsProvider = FutureProvider<List<Product>>((ref) async {
final supabase = ref.watch(supabaseProvider);
final orgId = ref.watch(currentOrgProvider);
final response = await supabase
.from('products')
.select()
.eq('org_id', orgId)
.order('created_at', ascending: false);
return (response as List).map((json) => Product.fromJson(json)).toList();
});
// StateNotifier for complex state
final cartProvider = StateNotifierProvider<CartNotifier, CartState>((ref) {
return CartNotifier();
});
Core Providers
// lib/core/providers/supabase_provider.dart
final supabaseProvider = Provider<SupabaseClient>((ref) {
return Supabase.instance.client;
});
// lib/core/providers/auth_provider.dart
final authStateProvider = StreamProvider<AuthState>((ref) {
final supabase = ref.watch(supabaseProvider);
return supabase.auth.onAuthStateChange;
});
final currentUserProvider = Provider<User?>((ref) {
final authState = ref.watch(authStateProvider);
return authState.valueOrNull?.session?.user;
});
// lib/core/providers/organization_provider.dart
final currentOrgProvider = StateProvider<String?>((ref) => null);
final organizationProvider = FutureProvider<Organization?>((ref) async {
final orgId = ref.watch(currentOrgProvider);
if (orgId == null) return null;
final supabase = ref.watch(supabaseProvider);
final response = await supabase
.from('organizations')
.select('*, plans(*)')
.eq('id', orgId)
.single();
return Organization.fromJson(response);
});
Data Models
Product Model
// lib/core/models/product.dart
import 'package:freezed_annotation/freezed_annotation.dart';
part 'product.freezed.dart';
part 'product.g.dart';
class Product with _$Product {
const factory Product({
required String id,
required String orgId,
required String title,
String? description,
required double basePrice,
required String status,
List<ProductVariant>? variants,
List<String>? imageUrls,
DateTime? createdAt,
}) = _Product;
factory Product.fromJson(Map<String, dynamic> json) =>
_$ProductFromJson(json);
}
Order Model
// lib/core/models/order.dart
class Order with _$Order {
const factory Order({
required String id,
required String orgId,
required String orderNumber,
required String status,
required double totalAmount,
double? platformFee,
String? customerId,
String? customerEmail,
String? shippingAddress,
String? trackingNumber,
DateTime? createdAt,
DateTime? shippedAt,
List<OrderItem>? items,
}) = _Order;
factory Order.fromJson(Map<String, dynamic> json) =>
_$OrderFromJson(json);
}
API Services
Product Service
// lib/core/services/product_service.dart
class ProductService {
final SupabaseClient _supabase;
ProductService(this._supabase);
Future<List<Product>> getProducts(String orgId) async {
final response = await _supabase
.from('products')
.select('*, product_variants(*)')
.eq('org_id', orgId)
.order('created_at', ascending: false);
return (response as List)
.map((json) => Product.fromJson(json))
.toList();
}
Future<Product> createProduct(Product product) async {
final response = await _supabase
.from('products')
.insert(product.toJson())
.select()
.single();
return Product.fromJson(response);
}
Future<Product> updateProduct(String id, Map<String, dynamic> updates) async {
final response = await _supabase
.from('products')
.update(updates)
.eq('id', id)
.select()
.single();
return Product.fromJson(response);
}
Future<void> deleteProduct(String id) async {
await _supabase.from('products').delete().eq('id', id);
}
}
UI Components
Product List Screen
// lib/features/products/screens/product_list_screen.dart
class ProductListScreen extends ConsumerWidget {
const ProductListScreen({super.key});
Widget build(BuildContext context, WidgetRef ref) {
final products = ref.watch(productsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Products'),
actions: [
IconButton(
icon: const Icon(Icons.add),
onPressed: () => context.push('/products/new'),
),
],
),
body: products.when(
data: (items) => ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) => ProductCard(product: items[index]),
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
),
);
}
}
Product Card Widget
// lib/features/products/widgets/product_card.dart
class ProductCard extends StatelessWidget {
final Product product;
const ProductCard({super.key, required this.product});
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: ListTile(
leading: product.imageUrls?.isNotEmpty == true
? ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
product.imageUrls!.first,
width: 56,
height: 56,
fit: BoxFit.cover,
),
)
: const Icon(Icons.image, size: 56),
title: Text(product.title),
subtitle: Text('\$${product.basePrice.toStringAsFixed(2)}'),
trailing: _StatusChip(status: product.status),
onTap: () => context.push('/products/${product.id}'),
),
);
}
}
Error Handling
Graceful Fallbacks
// Example: Handle missing database tables gracefully
final inventoryProvider = FutureProvider<List<InventoryItem>>((ref) async {
final supabase = ref.watch(supabaseProvider);
final orgId = ref.watch(currentOrgProvider);
try {
final response = await supabase
.from('inventory_items')
.select()
.eq('org_id', orgId);
return (response as List)
.map((json) => InventoryItem.fromJson(json))
.toList();
} catch (e) {
// Table may not exist yet, return empty list
debugPrint('Inventory query failed: $e');
return [];
}
});
Error Widget
class ErrorView extends StatelessWidget {
final String message;
final VoidCallback? onRetry;
const ErrorView({
super.key,
required this.message,
this.onRetry,
});
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 48, color: Colors.red),
const SizedBox(height: 16),
Text(message, textAlign: TextAlign.center),
if (onRetry != null) ...[
const SizedBox(height: 16),
ElevatedButton(
onPressed: onRetry,
child: const Text('Retry'),
),
],
],
),
);
}
}
Testing
Provider Tests
// test/providers/product_provider_test.dart
void main() {
group('ProductProvider', () {
test('fetches products for organization', () async {
final container = ProviderContainer(
overrides: [
supabaseProvider.overrideWithValue(mockSupabase),
currentOrgProvider.overrideWith((ref) => 'test-org-id'),
],
);
final products = await container.read(productsProvider.future);
expect(products, isNotEmpty);
expect(products.first.orgId, equals('test-org-id'));
});
});
}
Widget Tests
// test/widgets/product_card_test.dart
void main() {
testWidgets('ProductCard displays product info', (tester) async {
final product = Product(
id: 'test-id',
orgId: 'org-id',
title: 'Test Product',
basePrice: 29.99,
status: 'active',
);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: ProductCard(product: product),
),
),
);
expect(find.text('Test Product'), findsOneWidget);
expect(find.text('\$29.99'), findsOneWidget);
});
}
Build & Deploy
Development
# Run on iOS Simulator
flutter run -d ios
# Run on Android Emulator
flutter run -d android
# Run with specific flavor
flutter run --flavor development
Production Build
# iOS
flutter build ios --release
# Android
flutter build appbundle --release
Code Generation
# Generate freezed/json_serializable code
flutter pub run build_runner build --delete-conflicting-outputs
# Watch mode
flutter pub run build_runner watch