Skip to main content

Flutter Seller App Bootstrap

Cross-platform seller app for Talbino marketplace (iOS + Android + Web).

Tech Stack

  • Framework: Flutter 3.16+
  • Language: Dart 3.2+
  • State Management: Riverpod
  • Networking: Dio + Retrofit
  • Real-time: web_socket_channel
  • Storage: Hive / SharedPreferences
  • Navigation: go_router
  • Push: Firebase Cloud Messaging
  • Analytics: Firebase Analytics
  • Image: cached_network_image

Project Structure

seller/
├── lib/
│ ├── main.dart
│ ├── app.dart
│ ├── features/
│ │ ├── auth/
│ │ │ ├── presentation/
│ │ │ │ ├── phone_input_screen.dart
│ │ │ │ └── otp_verification_screen.dart
│ │ │ ├── providers/
│ │ │ │ └── auth_provider.dart
│ │ │ └── models/
│ │ │ └── user.dart
│ │ ├── requests/
│ │ │ ├── presentation/
│ │ │ │ ├── requests_list_screen.dart
│ │ │ │ ├── request_detail_screen.dart
│ │ │ │ └── widgets/
│ │ │ │ └── request_card.dart
│ │ │ ├── providers/
│ │ │ │ └── requests_provider.dart
│ │ │ └── models/
│ │ │ └── buy_request.dart
│ │ ├── offers/
│ │ │ ├── presentation/
│ │ │ │ ├── my_offers_screen.dart
│ │ │ │ ├── create_offer_screen.dart
│ │ │ │ └── widgets/
│ │ │ │ └── offer_card.dart
│ │ │ ├── providers/
│ │ │ │ └── offers_provider.dart
│ │ │ └── models/
│ │ │ └── offer.dart
│ │ ├── chat/
│ │ │ ├── presentation/
│ │ │ │ ├── conversations_screen.dart
│ │ │ │ ├── chat_screen.dart
│ │ │ │ └── widgets/
│ │ │ │ └── message_bubble.dart
│ │ │ ├── providers/
│ │ │ │ └── chat_provider.dart
│ │ │ └── models/
│ │ │ └── message.dart
│ │ ├── deals/
│ │ │ ├── presentation/
│ │ │ │ └── deals_list_screen.dart
│ │ │ ├── providers/
│ │ │ │ └── deals_provider.dart
│ │ │ └── models/
│ │ │ └── deal.dart
│ │ └── profile/
│ │ ├── presentation/
│ │ │ ├── profile_screen.dart
│ │ │ ├── seller_application_screen.dart
│ │ │ └── my_ratings_screen.dart
│ │ └── providers/
│ │ └── profile_provider.dart
│ ├── shared/
│ │ ├── services/
│ │ │ ├── api_service.dart
│ │ │ ├── websocket_service.dart
│ │ │ └── notification_service.dart
│ │ ├── widgets/
│ │ │ ├── loading_indicator.dart
│ │ │ ├── empty_state.dart
│ │ │ └── primary_button.dart
│ │ ├── utils/
│ │ │ ├── constants.dart
│ │ │ ├── extensions.dart
│ │ │ └── validators.dart
│ │ └── models/
│ │ └── api_response.dart
│ └── l10n/
│ ├── app_ar.arb
│ └── app_en.arb
├── test/
├── pubspec.yaml
└── .env.example
final goRouter = GoRouter(
routes: [
GoRoute(
path: '/',
redirect: (context, state) => '/requests',
),
GoRoute(
path: '/requests',
builder: (context, state) => const RequestsListScreen(),
),
GoRoute(
path: '/requests/:id',
builder: (context, state) => RequestDetailScreen(
requestId: state.pathParameters['id']!,
),
),
GoRoute(
path: '/requests/:id/offer',
builder: (context, state) => CreateOfferScreen(
requestId: state.pathParameters['id']!,
),
),
GoRoute(
path: '/offers',
builder: (context, state) => const MyOffersScreen(),
),
GoRoute(
path: '/deals',
builder: (context, state) => const DealsListScreen(),
),
GoRoute(
path: '/chat',
builder: (context, state) => const ConversationsScreen(),
),
GoRoute(
path: '/chat/:id',
builder: (context, state) => ChatScreen(
conversationId: state.pathParameters['id']!,
),
),
GoRoute(
path: '/profile',
builder: (context, state) => const ProfileScreen(),
),
],
);

State Management (Riverpod)

// providers/requests_provider.dart
final requestsProvider = StateNotifierProvider<RequestsNotifier, AsyncValue<List<BuyRequest>>>((ref) {
return RequestsNotifier(ref.read(apiServiceProvider));
});

class RequestsNotifier extends StateNotifier<AsyncValue<List<BuyRequest>>> {
final ApiService _apiService;

RequestsNotifier(this._apiService) : super(const AsyncValue.loading()) {
fetchRequests();
}

Future<void> fetchRequests() async {
state = const AsyncValue.loading();
try {
final requests = await _apiService.getActiveRequests();
state = AsyncValue.data(requests);
} catch (e, stack) {
state = AsyncValue.error(e, stack);
}
}
}

API Service

class ApiService {
final Dio _dio;

ApiService(this._dio);

Future<List<BuyRequest>> getActiveRequests({
String? category,
String? location,
}) async {
final response = await _dio.get(
'/requests',
queryParameters: {
'status': 'active',
if (category != null) 'category': category,
if (location != null) 'location_city': location,
},
);

final data = ApiResponse<RequestsResponse>.fromJson(response.data);
return data.data.requests;
}

Future<Offer> createOffer(CreateOfferInput input) async {
final response = await _dio.post('/offers', data: input.toJson());
final data = ApiResponse<Offer>.fromJson(response.data);
return data.data;
}
}

Key Screens

RequestsListScreen

class RequestsListScreen extends ConsumerWidget {
const RequestsListScreen({super.key});


Widget build(BuildContext context, WidgetRef ref) {
final requestsAsync = ref.watch(requestsProvider);

return Scaffold(
appBar: AppBar(
title: Text(context.l10n.browseRequests),
actions: [
IconButton(
icon: const Icon(Icons.filter_list),
onPressed: () => _showFilters(context),
),
],
),
body: requestsAsync.when(
data: (requests) {
if (requests.isEmpty) {
return EmptyState(
message: context.l10n.noActiveRequests,
icon: Icons.inbox_outlined,
);
}

return RefreshIndicator(
onRefresh: () => ref.refresh(requestsProvider.future),
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: requests.length,
itemBuilder: (context, index) {
return RequestCard(
request: requests[index],
onTap: () => context.push('/requests/${requests[index].id}'),
);
},
),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => ErrorView(
message: error.toString(),
onRetry: () => ref.refresh(requestsProvider),
),
),
);
}
}

CreateOfferScreen

class CreateOfferScreen extends ConsumerStatefulWidget {
final String requestId;

const CreateOfferScreen({required this.requestId, super.key});


ConsumerState<CreateOfferScreen> createState() => _CreateOfferScreenState();
}

class _CreateOfferScreenState extends ConsumerState<CreateOfferScreen> {
final _formKey = GlobalKey<FormState>();
final _priceController = TextEditingController();
final _availabilityController = TextEditingController();
final _conditionNotesController = TextEditingController();
final _warrantyNotesController = TextEditingController();

bool _isSubmitting = false;


Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(context.l10n.makeOffer),
),
body: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
TextFormField(
controller: _priceController,
decoration: InputDecoration(
labelText: context.l10n.price,
suffixText: context.l10n.egp,
),
keyboardType: TextInputType.number,
validator: (value) {
if (value == null || value.isEmpty) {
return context.l10n.priceRequired;
}
return null;
},
),
const SizedBox(height: 16),
TextFormField(
controller: _availabilityController,
decoration: InputDecoration(
labelText: context.l10n.availability,
hintText: context.l10n.availabilityHint,
),
),
const SizedBox(height: 16),
TextFormField(
controller: _conditionNotesController,
decoration: InputDecoration(
labelText: context.l10n.conditionNotes,
),
maxLines: 3,
),
const SizedBox(height: 16),
TextFormField(
controller: _warrantyNotesController,
decoration: InputDecoration(
labelText: context.l10n.warrantyNotes,
),
maxLines: 2,
),
const SizedBox(height: 24),
PrimaryButton(
onPressed: _isSubmitting ? null : _submitOffer,
isLoading: _isSubmitting,
child: Text(context.l10n.submitOffer),
),
],
),
),
);
}

Future<void> _submitOffer() async {
if (!_formKey.currentState!.validate()) return;

setState(() => _isSubmitting = true);

try {
final input = CreateOfferInput(
requestId: widget.requestId,
price: double.parse(_priceController.text),
availability: _availabilityController.text,
conditionNotes: _conditionNotesController.text,
warrantyNotes: _warrantyNotesController.text,
);

await ref.read(apiServiceProvider).createOffer(input);

if (mounted) {
context.pop();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.offerSubmitted)),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.toString())),
);
}
} finally {
if (mounted) {
setState(() => _isSubmitting = false);
}
}
}
}

Widgets

RequestCard

class RequestCard extends StatelessWidget {
final BuyRequest request;
final VoidCallback onTap;

const RequestCard({
required this.request,
required this.onTap,
super.key,
});


Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
'${request.productBrand} ${request.productModel}',
style: Theme.of(context).textTheme.titleMedium,
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
'${request.offerCount} ${context.l10n.offers}',
style: TextStyle(
color: Colors.green.shade700,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
],
),
const SizedBox(height: 8),
Text(
'${context.l10n.budget}: ${request.budgetMin} - ${request.budgetMax} ${context.l10n.egp}',
style: Theme.of(context).textTheme.bodyMedium,
),
const SizedBox(height: 4),
Row(
children: [
Icon(Icons.location_on, size: 16, color: Colors.grey.shade600),
const SizedBox(width: 4),
Text(
request.locationDistrict,
style: Theme.of(context).textTheme.bodySmall,
),
const Spacer(),
Text(
request.createdAt.timeAgo(context),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey.shade600,
),
),
],
),
],
),
),
),
);
}
}

Localization

app_ar.arb

{
"@@locale": "ar",
"appName": "طلبينو - البائع",
"browseRequests": "تصفح الطلبات",
"myOffers": "عروضي",
"myDeals": "صفقاتي",
"profile": "الملف الشخصي",
"makeOffer": "تقديم عرض",
"price": "السعر",
"egp": "جنيه",
"availability": "التوفر",
"availabilityHint": "مثال: فوري، يومين",
"conditionNotes": "ملاحظات الحالة",
"warrantyNotes": "ملاحظات الضمان",
"submitOffer": "إرسال العرض",
"offerSubmitted": "تم إرسال العرض بنجاح",
"budget": "الميزانية",
"offers": "عروض",
"noActiveRequests": "لا توجد طلبات نشطة حالياً"
}

Push Notifications

class NotificationService {
final FirebaseMessaging _messaging = FirebaseMessaging.instance;

Future<void> initialize() async {
await _messaging.requestPermission();

final token = await _messaging.getToken();
if (token != null) {
await _registerToken(token);
}

FirebaseMessaging.onMessage.listen(_handleForegroundMessage);
FirebaseMessaging.onMessageOpenedApp.listen(_handleNotificationTap);
}

Future<void> _registerToken(String token) async {
await ApiService().registerDeviceToken(
platform: 'flutter_web',
pushToken: token,
);
}

void _handleForegroundMessage(RemoteMessage message) {
// Show in-app notification
}

void _handleNotificationTap(RemoteMessage message) {
final deepLink = message.data['deep_link'];
if (deepLink != null) {
// Navigate to deep link
}
}
}

pubspec.yaml

name: talbino_seller
description: Talbino Seller App
version: 1.0.0+1

environment:
sdk: '>=3.2.0 <4.0.0'

dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter

# State Management
flutter_riverpod: ^2.4.9

# Navigation
go_router: ^13.0.0

# Networking
dio: ^5.4.0
retrofit: ^4.0.3

# Real-time
web_socket_channel: ^2.4.0

# Storage
hive: ^2.2.3
hive_flutter: ^1.1.0
shared_preferences: ^2.2.2

# UI
cached_network_image: ^3.3.1
flutter_svg: ^2.0.9

# Firebase
firebase_core: ^2.24.2
firebase_messaging: ^14.7.9
firebase_analytics: ^10.8.0

# Utils
intl: ^0.18.1
timeago: ^3.6.0

dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^3.0.1
build_runner: ^2.4.7
retrofit_generator: ^8.0.4
hive_generator: ^2.0.1

MVP Screens Checklist

  • Phone input + OTP verification
  • Seller application (if not approved)
  • Browse active requests (with filters)
  • Request detail
  • Create offer
  • My offers list
  • Offer detail
  • Chat
  • My deals list
  • Deal detail
  • Profile with ratings
  • Settings