Priority Level: Web β PWA β iOS β Android
βββββββββββββββββββββββββββββββββββββββββββββββ
Development Speed HighβMed βLow βLow
User Reach HighβHigh βMed βHigh
Feature Completeness LowβHigh βHigh βHigh
Native Performance Med βHigh βHigh βHigh
Offline Capability Med βHigh βHigh βHigh
App Store Approval N/A βN/A βLow βLow
Single Codebase: Flutter Web
Primary Target: Web browsers
Features: Core functionality, authentication, basic tracking
Success Criteria: Functional MVP for alpha testing
Enhancement: Add PWA capabilities
Features: Offline support, push notifications, installable
Success Criteria: Mobile-optimized experience
Deployment: iOS App Store, Google Play Store
Features: Full native integration, background tracking
Success Criteria: Production-ready mobile apps
// Project structure optimized for web deployment
lib/
βββ main.dart // Web-optimized entry point
βββ core/
β βββ config/
β β βββ app_config.dart // Environment configuration
β β βββ web_config.dart // Web-specific settings
β βββ services/
β β βββ web_service.dart // Web-specific APIs
β β βββ offline_service.dart // Offline data management
β βββ theme/
β βββ web_theme.dart // Web-optimized styling
βββ data/
β βββ models/ // Data models with web serialization
β βββ repositories/ // Repository pattern for data access
β βββ providers/ // Riverpod providers
βββ presentation/
β βββ screens/ // Web-optimized screens
β β βββ home/
β β βββ elevator_search/
β β βββ tracking/
β βββ widgets/ // Responsive web widgets
β β βββ responsive_layout.dart
β β βββ web_navigation.dart
β β βββ touch_optimized.dart
β βββ providers/ // State management
βββ utils/
βββ web_utils.dart // Web-specific utilities
βββ geo_utils.dart // Geographic calculations
// Responsive design patterns
class ResponsiveLayout extends StatelessWidget {
final Widget mobile;
final Widget? tablet;
final Widget desktop;
const ResponsiveLayout({
Key? key,
required this.mobile,
this.tablet,
required this.desktop,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth >= 1200) {
return desktop;
} else if (constraints.maxWidth >= 768) {
return tablet ?? mobile;
} else {
return mobile;
}
},
);
}
}
// Touch-optimized interactions
class TouchOptimizedButton extends StatelessWidget {
final Widget child;
final VoidCallback onTap;
final double minTouchSize = 44.0; // Accessibility standard
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Container(
constraints: BoxConstraints(
minHeight: minTouchSize,
minWidth: minTouchSize,
),
padding: EdgeInsets.all(16),
child: Center(child: child),
),
);
}
}
// Main app providers
@riverpod
class AppNotifier extends _$AppNotifier {
@override
AppState build() {
_initializeApp();
return AppState();
}
void _initializeApp() {
// Initialize services, check connectivity, load user data
}
}
// Feature-specific providers
@riverpod
class ElevatorNotifier extends _$ElevatorNotifier {
@override
ElevatorState build() {
loadNearbyElevators();
return ElevatorState();
}
}
// Computed providers for derived state
@riverpod
bool isNearElevator(IsNearElevatorRef ref) {
final location = ref.watch(currentLocationProvider);
final elevators = ref.watch(elevatorsProvider);
if (location == null || elevators.isEmpty) return false;
return elevators.any((elevator) =>
calculateDistance(location, elevator.location) < 0.5 // 500m
);
}
-- User management
CREATE TABLE profiles (
id UUID REFERENCES auth.users PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
full_name TEXT,
truck_number TEXT,
company TEXT,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Elevator data
CREATE TABLE elevators (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL,
company TEXT NOT NULL,
location GEOGRAPHY(POINT, 4326) NOT NULL,
address TEXT,
accepted_grains TEXT[],
capacity_bushels INTEGER,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
-- Real-time status
CREATE TABLE elevator_status (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
elevator_id UUID REFERENCES elevators(id),
current_wait_time INTEGER, -- minutes
trucks_in_line INTEGER DEFAULT 0,
accepting_grain TEXT,
status TEXT CHECK (status IN ('open', 'full', 'closed', 'maintenance')),
last_updated TIMESTAMP DEFAULT NOW()
);
-- Tracking sessions
CREATE TABLE hauling_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES profiles(id),
elevator_id UUID REFERENCES elevators(id),
start_time TIMESTAMP NOT NULL,
end_time TIMESTAMP,
grain_type TEXT,
status TEXT CHECK (status IN ('active', 'completed', 'cancelled')),
notes TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
-- Location tracking
CREATE TABLE location_tracks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
session_id UUID REFERENCES hauling_sessions(id),
user_id UUID REFERENCES profiles(id),
location GEOGRAPHY(POINT, 4326) NOT NULL,
accuracy FLOAT,
speed FLOAT,
recorded_at TIMESTAMP DEFAULT NOW()
);
// Real-time elevator status updates
class ElevatorStatusService {
late RealtimeChannel _statusChannel;
void initialize() {
_statusChannel = Supabase.instance.client
.channel('elevator_status_changes')
.onPostgresChanges(
event: PostgresChangeEvent.update,
schema: 'public',
table: 'elevator_status',
callback: _handleStatusUpdate,
)
.subscribe();
}
void _handleStatusUpdate(PostgresChangePayload payload) {
final status = ElevatorStatus.fromJson(payload.newRecord);
ref.read(elevatorStatusProvider(status.elevatorId).notifier).update(status);
}
}
class LocationTrackingService {
StreamSubscription<Position>? _positionSubscription;
final GeolocatorPlatform _geolocator = GeolocatorPlatform.instance;
Future<void> startTracking() async {
// Check permissions
final permission = await _geolocator.checkPermission();
if (permission == LocationPermission.denied) {
await _geolocator.requestPermission();
}
// Start position stream
const locationSettings = LocationSettings(
accuracy: LocationAccuracy.high,
distanceFilter: 10, // Update every 10 meters
);
_positionSubscription = _geolocator
.getPositionStream(locationSettings: locationSettings)
.listen(_handlePositionUpdate);
}
void _handlePositionUpdate(Position position) {
final location = Location(
latitude: position.latitude,
longitude: position.longitude,
altitude: position.altitude,
accuracy: position.accuracy,
timestamp: DateTime.now(),
);
// Update location provider
ref.read(currentLocationProvider.notifier).updateLocation(location);
// Auto-detect nearby elevators
_checkProximityToElevators(location);
}
}
# 1. Clone and setup
git clone <repository-url>
cd haulpass_new
flutter pub get
# 2. Generate code
dart run build_runner build --delete-conflicting-outputs
# 3. Configure environment
cp .env.example .env
# Edit .env with Supabase credentials
# 4. Run web version
flutter run -d chrome
# 5. Test PWA features
flutter run -d chrome --web-renderer html
# Check: Offline functionality, service worker, installability
# 6. Mobile testing
flutter run -d android # or ios
// Example test structure
testWidgets('User can sign in with valid credentials', (tester) async {
// Arrange
await tester.pumpWidget(
ProviderScope(
child: MaterialApp(home: SignInScreen()),
),
);
// Act
await tester.enterText(find.byKey(Key('email')), 'test@example.com');
await tester.enterText(find.byKey(Key('password')), 'password123');
await tester.tap(find.byKey(Key('sign_in_button')));
// Assert
await tester.pumpAndSettle();
expect(find.byType(HomeScreen), findsOneWidget);
});
// Test complete hauling workflow
testWidgets('Complete hauling workflow', (tester) async {
// Setup app with test data
await loadTestData();
// 1. Sign in
await signIn(tester, 'testuser@example.com', 'password');
// 2. Search for elevators
await searchElevators(tester, 'wheat');
expect(find.byType(ElevatorList), findsOneWidget);
// 3. Start tracking
await startTracking(tester, firstElevator);
// 4. Navigate to elevator
await navigateToElevator(tester);
// 5. Complete session
await completeSession(tester);
// 6. Verify session saved
expect(find.byType(SessionSummary), findsOneWidget);
});
// Web performance benchmarks
testWidgets('App loads within 3 seconds', (tester) async {
final stopwatch = Stopwatch()..start();
await tester.pumpWidget(MyApp());
await tester.pumpAndSettle();
stopwatch.stop();
expect(stopwatch.elapsedMilliseconds, lessThan(3000));
});
// Memory usage monitoring
testWidgets('No memory leaks during GPS tracking', () async {
final initialMemory = await getMemoryUsage();
// Start GPS tracking
await startLocationTracking();
await waitFor(Duration(seconds: 60));
final finalMemory = await getMemoryUsage();
expect(finalMemory - initialMemory, lessThan(50 * 1024 * 1024)); // 50MB limit
});
# GitHub Actions workflow
name: Deploy to Web
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
with:
flutter-version: '3.19.0'
- run: flutter pub get
- run: dart run build_runner build --delete-conflicting-outputs
- run: flutter build web --release --web-renderer html
- uses: peaceiris/actions-gh-pages@v3
with:
github_token: $
publish_dir: ./build/web
// web/manifest.json
{
"name": "HaulPass - Grain Hauling Solution",
"short_name": "HaulPass",
"description": "Professional grain hauling logistics and tracking",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#2196F3",
"icons": [
{
"src": "icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
# iOS Deployment
name: Deploy iOS App
on:
push:
tags: ['v*']
jobs:
deploy-ios:
runs-on: macos-latest
steps:
- uses: actions/checkout@v3
- uses: subosito/flutter-action@v2
- run: flutter build ios --release
- uses: apple-actions/download-certificates@v2
with:
issuer-id: $
api-key-id: $
api-private-key: $
- uses: maierj/fastlane-action@v3.0.0
with:
lane: release
options: '{"app_identifier":"com.haulpass.app"}'
// Error tracking and performance monitoring
class MonitoringService {
static void initialize() {
// Initialize crash reporting
FlutterError.onError = (details) {
FirebaseCrashlytics.instance.recordError(
details.exception,
details.stack,
fatal: true,
);
};
// Performance monitoring
PerformanceObserver((list) {
for (final entry in list) {
FirebasePerformance.instance
.newTrace(entry.name)
.start();
}
});
}
}
// Privacy-compliant analytics
class AnalyticsService {
static void trackScreenView(String screenName) {
// Only track if user opted into analytics
if (_userConsent.analytics) {
Amplitude.instance.logEvent('screen_view', eventProperties: {
'screen_name': screenName,
'timestamp': DateTime.now().toIso8601String(),
});
}
}
static void trackFeatureUsage(String feature) {
if (_userConsent.analytics) {
Amplitude.instance.logEvent('feature_used', eventProperties: {
'feature': feature,
'user_type': _userProfile.userType,
});
}
}
}
// Current Issue: AuthState conflicts with Supabase's AuthState
// Solution: Use explicit imports
import 'package:supabase_flutter/supabase_flutter.dart' as supabase;
// Update all references
void _handleAuthStateChange(supabase.AuthStateChange<supabase.Session> data) {
// Implementation
}
// Current Issue: Location class conflicts with geocoding Location
// Solution: Use typedefs and explicit imports
import 'package:geolocator/geolocator.dart';
import 'package:geocoding/geocoding.dart' as geo;
import 'location_models.dart'; // Our Location class
typedef GeoPlacemark = geo.Placemark;
// Current Issue: Deprecated ref types
// Solution: Update to new syntax
@riverpod
class AuthNotifier extends _$AuthNotifier {
@override
AuthState build() {
// Remove deprecated ref type usage
return const AuthState();
}
}
// Update providers to use simple Ref
@riverpod
UserProfile? currentUser(Ref ref) {
final authState = ref.watch(authNotifierProvider);
return authState.user;
}
class OptimizedLocationTracking {
static const _normalInterval = Duration(seconds: 30);
static const _highAccuracyInterval = Duration(seconds: 5);
Timer? _updateTimer;
void startAdaptiveTracking() {
_updateTimer = Timer.periodic(_normalInterval, (timer) {
final speed = _currentSpeed;
if (speed > 60) { // Moving fast
_increaseAccuracy();
} else {
_decreaseAccuracy();
}
});
}
}
class MemoryEfficientImageLoading {
static Widget cachedImage(String url) {
return CachedNetworkImage(
imageUrl: url,
memCacheWidth: 800, // Limit memory usage
memCacheHeight: 600,
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
);
}
}
This technical guide should be updated regularly to reflect the evolving architecture and requirements as HaulPass moves from alpha to production.