📱 Phase 7: Mobile App Development

Native Mobile Application with Offline Capabilities

Duration: 4-6 weeks | Complexity: Advanced

📋 Phase Overview

This phase develops a native mobile application for field data collection with offline capabilities. You'll build Laravel API endpoints, create a mobile app using Flutter or React Native, implement QR code scanning, enable offline data storage, and create synchronization features for when connectivity is restored.

End Goal: Fully functional mobile app that works offline in the field and syncs data when online.

⚠️ Prerequisites

  • Phase 6 Completed: All web functionality operational
  • Mobile Development Environment: Android Studio or Xcode setup
  • API Testing Tools: Postman or similar for API testing
  • Device Testing: Physical mobile device or emulator
1

Laravel API Development

Create comprehensive API endpoints for all mobile functionality.

Install API Dependencies:

# Install Laravel Sanctum for API authentication composer require laravel/sanctum # Publish Sanctum configuration php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider" # Run migrations php artisan migrate # Create API resource controllers php artisan make:controller Api/PlantController --api php artisan make:controller Api/HarvestController --api php artisan make:controller Api/MeasurementController --api php artisan make:controller Api/WeatherController --api

API Authentication Setup:

// In app/Http/Kernel.php, add to api middleware group: 'api' => [ \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, 'throttle:api', \Illuminate\Routing\Middleware\SubstituteBindings::class, ], // API Routes (routes/api.php): Route::middleware('auth:sanctum')->group(function () { // Authentication Route::post('/logout', [AuthController::class, 'logout']); Route::get('/user', function (Request $request) { return $request->user(); }); // Plants Route::apiResource('plants', PlantController::class); Route::get('/plants/{plant}/qr', [PlantController::class, 'generateQr']); Route::get('/plants/scan/{plantId}', [PlantController::class, 'scanInfo']); // Measurements Route::post('/plants/{plant}/measurements', [MeasurementController::class, 'store']); Route::get('/plants/{plant}/measurements', [MeasurementController::class, 'index']); // Harvests Route::apiResource('harvests', HarvestController::class); Route::post('/harvests/bulk', [HarvestController::class, 'bulkStore']); // Weather Route::get('/weather/current', [WeatherController::class, 'current']); Route::get('/weather/forecast', [WeatherController::class, 'forecast']); // Sync endpoints Route::post('/sync/upload', [SyncController::class, 'upload']); Route::get('/sync/download', [SyncController::class, 'download']); }); // Public routes Route::post('/login', [AuthController::class, 'login']); Route::post('/register', [AuthController::class, 'register']);

API Plant Controller Example:

function($query) { $query->latest()->limit(1); }]) ->when($request->status, fn($q, $status) => $q->where('status', $status)) ->when($request->search, fn($q, $search) => $q->where('plant_id', 'like', "%{$search}%") ->orWhere('location_row', 'like', "%{$search}%") ) ->paginate(50); return PlantResource::collection($plants); } public function show(Plant $plant) { $plant->load([ 'variety', 'measurements' => fn($query) => $query->latest()->limit(5), 'harvests' => fn($query) => $query->latest()->limit(5), 'qrCodes' ]); return new PlantResource($plant); } public function store(Request $request) { $validated = $request->validate([ 'plant_id' => 'required|string|unique:plants', 'variety_id' => 'required|exists:plant_varieties,variety_id', 'planted_date' => 'required|date', 'location_row' => 'nullable|string', 'location_position' => 'nullable|string', 'coordinates_lat' => 'nullable|numeric', 'coordinates_lng' => 'nullable|numeric', 'notes' => 'nullable|string' ]); $validated['farm_id'] = 1; $validated['status'] = 'active'; $plant = Plant::create($validated); $plant->load('variety'); return new PlantResource($plant); } public function scanInfo($plantId) { $plant = Plant::with(['variety', 'measurements' => function($query) { $query->latest()->limit(3); }])->findOrFail($plantId); // Log scan for analytics $plant->increment('scan_count'); return response()->json([ 'plant' => new PlantResource($plant), 'quick_actions' => [ 'add_measurement' => true, 'record_harvest' => $this->isHarvestSeason(), 'water_log' => true, 'note_update' => true ], 'weather' => $this->getCurrentWeather() ]); } private function isHarvestSeason() { $month = now()->month; return in_array($month, [6, 7, 8, 9, 10]); // Harvest months } private function getCurrentWeather() { $weather = app(\App\Services\WeatherService::class)->fetchCurrentWeather(); if ($weather) { return [ 'temperature' => $weather['main']['temp'] . '°F', 'conditions' => $weather['weather'][0]['description'], 'humidity' => $weather['main']['humidity'] . '%' ]; } return null; } }
2

Mobile Framework Selection & Setup

Choose and set up the mobile development framework (Flutter recommended for cross-platform).

Flutter Setup (Recommended):

# Install Flutter SDK # Follow official Flutter installation guide for your OS # Create new Flutter project flutter create blackberry_farm_app cd blackberry_farm_app # Add required dependencies to pubspec.yaml: dependencies: flutter: sdk: flutter http: ^0.13.5 # HTTP requests qr_code_scanner: ^1.0.1 # QR code scanning sqflite: ^2.2.8 # Local SQLite database shared_preferences: ^2.1.1 # Local storage connectivity_plus: ^4.0.1 # Network connectivity geolocator: ^9.0.2 # GPS location camera: ^0.10.5 # Camera access provider: ^6.0.5 # State management intl: ^0.18.1 # Date formatting dev_dependencies: flutter_test: sdk: flutter flutter_lints: ^2.0.0 # Install dependencies flutter pub get # Generate app icons and splash screen flutter pub add flutter_launcher_icons flutter pub add flutter_native_splash

React Native Alternative:

# If choosing React Native instead: npx react-native init BlackberryFarmApp # Install required packages: npm install @react-navigation/native npm install @react-navigation/stack npm install react-native-qrcode-scanner npm install @react-native-async-storage/async-storage npm install @react-native-community/netinfo npm install react-native-geolocation-service npm install axios npm install react-native-sqlite-storage
3

QR Code Scanning Implementation

Implement native QR code scanning with camera integration.

Flutter QR Scanner Implementation:

// lib/screens/qr_scanner_screen.dart import 'package:flutter/material.dart'; import 'package:qr_code_scanner/qr_code_scanner.dart'; class QrScannerScreen extends StatefulWidget { @override _QrScannerScreenState createState() => _QrScannerScreenState(); } class _QrScannerScreenState extends State { final GlobalKey qrKey = GlobalKey(debugLabel: 'QR'); QRViewController? controller; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('Scan Plant QR Code'), backgroundColor: Colors.green, ), body: Column( children: [ Expanded( flex: 4, child: QRView( key: qrKey, onQRViewCreated: _onQRViewCreated, overlay: QrScannerOverlayShape( borderColor: Colors.green, borderRadius: 10, borderLength: 30, borderWidth: 10, cutOutSize: 300, ), ), ), Expanded( flex: 1, child: Container( padding: EdgeInsets.all(20), child: Column( children: [ Text( 'Position QR code within the frame', style: TextStyle(fontSize: 16), textAlign: TextAlign.center, ), SizedBox(height: 10), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ IconButton( onPressed: () async { await controller?.toggleFlash(); }, icon: Icon(Icons.flash_on), ), IconButton( onPressed: () async { await controller?.flipCamera(); }, icon: Icon(Icons.flip_camera_ios), ), ], ), ], ), ), ), ], ), ); } void _onQRViewCreated(QRViewController controller) { this.controller = controller; controller.scannedDataStream.listen((scanData) { controller.pauseCamera(); _handleScanResult(scanData.code); }); } void _handleScanResult(String? code) { if (code != null) { // Extract plant ID from QR code URL Uri uri = Uri.parse(code); String? plantId = uri.pathSegments.last; // Navigate to plant details Navigator.of(context).pushReplacementNamed( '/plant-details', arguments: {'plantId': plantId}, ); } } @override void dispose() { controller?.dispose(); super.dispose(); } }
4

Offline Data Storage

Implement local SQLite database for offline data collection and storage.

Local Database Schema:

// lib/database/database_helper.dart import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart'; class DatabaseHelper { static final DatabaseHelper _instance = DatabaseHelper._internal(); static Database? _database; factory DatabaseHelper() => _instance; DatabaseHelper._internal(); Future get database async { if (_database != null) return _database!; _database = await _initDatabase(); return _database!; } Future _initDatabase() async { String path = join(await getDatabasesPath(), 'blackberry_farm.db'); return await openDatabase( path, version: 1, onCreate: _onCreate, ); } Future _onCreate(Database db, int version) async { // Plants table await db.execute(''' CREATE TABLE plants_local ( plant_id TEXT PRIMARY KEY, variety_name TEXT, location_row TEXT, location_position TEXT, status TEXT, planted_date TEXT, notes TEXT, last_sync TEXT, is_synced INTEGER DEFAULT 0 ) '''); // Measurements table await db.execute(''' CREATE TABLE measurements_local ( id INTEGER PRIMARY KEY AUTOINCREMENT, plant_id TEXT, measurement_date TEXT, height_cm REAL, health_score INTEGER, cane_count INTEGER, notes TEXT, measured_by TEXT, created_at TEXT, is_synced INTEGER DEFAULT 0, FOREIGN KEY (plant_id) REFERENCES plants_local (plant_id) ) '''); // Harvests table await db.execute(''' CREATE TABLE harvests_local ( id INTEGER PRIMARY KEY AUTOINCREMENT, plant_id TEXT, harvest_date TEXT, season TEXT, yield_lbs REAL, berry_quality_grade TEXT, harvest_notes TEXT, weather_conditions TEXT, harvested_by TEXT, created_at TEXT, is_synced INTEGER DEFAULT 0, FOREIGN KEY (plant_id) REFERENCES plants_local (plant_id) ) '''); // Sync queue table await db.execute(''' CREATE TABLE sync_queue ( id INTEGER PRIMARY KEY AUTOINCREMENT, table_name TEXT, record_id TEXT, action TEXT, data TEXT, created_at TEXT, is_processed INTEGER DEFAULT 0 ) '''); } // Plant operations Future insertPlant(Map plant) async { final db = await database; await db.insert('plants_local', plant, conflictAlgorithm: ConflictAlgorithm.replace); } Future>> getPlants() async { final db = await database; return await db.query('plants_local', orderBy: 'plant_id'); } Future?> getPlant(String plantId) async { final db = await database; final results = await db.query( 'plants_local', where: 'plant_id = ?', whereArgs: [plantId], ); return results.isNotEmpty ? results.first : null; } // Measurement operations Future insertMeasurement(Map measurement) async { final db = await database; measurement['created_at'] = DateTime.now().toIso8601String(); await db.insert('measurements_local', measurement); // Add to sync queue await _addToSyncQueue('measurements', measurement); } Future>> getMeasurements(String plantId) async { final db = await database; return await db.query( 'measurements_local', where: 'plant_id = ?', whereArgs: [plantId], orderBy: 'measurement_date DESC', ); } // Harvest operations Future insertHarvest(Map harvest) async { final db = await database; harvest['created_at'] = DateTime.now().toIso8601String(); await db.insert('harvests_local', harvest); // Add to sync queue await _addToSyncQueue('harvests', harvest); } // Sync queue operations Future _addToSyncQueue(String tableName, Map data) async { final db = await database; await db.insert('sync_queue', { 'table_name': tableName, 'action': 'insert', 'data': jsonEncode(data), 'created_at': DateTime.now().toIso8601String(), }); } Future>> getPendingSyncItems() async { final db = await database; return await db.query( 'sync_queue', where: 'is_processed = ?', whereArgs: [0], orderBy: 'created_at', ); } Future markSyncItemProcessed(int id) async { final db = await database; await db.update( 'sync_queue', {'is_processed': 1}, where: 'id = ?', whereArgs: [id], ); } }
5

Data Synchronization Service

Create robust data synchronization between local storage and server.

Sync Service Implementation:

// lib/services/sync_service.dart import 'dart:convert'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:http/http.dart' as http; class SyncService { static final SyncService _instance = SyncService._internal(); factory SyncService() => _instance; SyncService._internal(); final DatabaseHelper _db = DatabaseHelper(); final String _baseUrl = 'https://blackberries.homesteadingoutlaws.com/api'; Future isOnline() async { var connectivityResult = await (Connectivity().checkConnectivity()); return connectivityResult != ConnectivityResult.none; } Future syncAll() async { if (!await isOnline()) { return SyncResult(success: false, message: 'No internet connection'); } try { // Get all pending sync items List> pendingItems = await _db.getPendingSyncItems(); int uploaded = 0; int failed = 0; for (var item in pendingItems) { try { await _syncItem(item); await _db.markSyncItemProcessed(item['id']); uploaded++; } catch (e) { print('Failed to sync item ${item['id']}: $e'); failed++; } } // Download updates from server await _downloadUpdates(); return SyncResult( success: true, message: 'Synced $uploaded items, $failed failed', uploaded: uploaded, failed: failed, ); } catch (e) { return SyncResult(success: false, message: 'Sync failed: $e'); } } Future _syncItem(Map item) async { String tableName = item['table_name']; String action = item['action']; Map data = jsonDecode(item['data']); String endpoint; switch (tableName) { case 'measurements': endpoint = '/plants/${data['plant_id']}/measurements'; break; case 'harvests': endpoint = '/harvests'; break; default: throw Exception('Unknown table: $tableName'); } final response = await http.post( Uri.parse('$_baseUrl$endpoint'), headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ${await _getAuthToken()}', }, body: jsonEncode(data), ); if (response.statusCode != 200 && response.statusCode != 201) { throw Exception('HTTP ${response.statusCode}: ${response.body}'); } } Future _downloadUpdates() async { // Download plant updates final plantsResponse = await http.get( Uri.parse('$_baseUrl/plants'), headers: { 'Authorization': 'Bearer ${await _getAuthToken()}', }, ); if (plantsResponse.statusCode == 200) { final plantsData = jsonDecode(plantsResponse.body); for (var plantData in plantsData['data']) { // Update local plant data await _db.insertPlant({ 'plant_id': plantData['plant_id'], 'variety_name': plantData['variety']['variety_name'], 'location_row': plantData['location_row'], 'location_position': plantData['location_position'], 'status': plantData['status'], 'planted_date': plantData['planted_date'], 'notes': plantData['notes'] ?? '', 'last_sync': DateTime.now().toIso8601String(), 'is_synced': 1, }); } } } Future _getAuthToken() async { // Get stored auth token from shared preferences SharedPreferences prefs = await SharedPreferences.getInstance(); return prefs.getString('auth_token') ?? ''; } Future scheduleAutoSync() async { // Schedule periodic sync when online Timer.periodic(Duration(minutes: 15), (timer) async { if (await isOnline()) { await syncAll(); } }); } } class SyncResult { final bool success; final String message; final int uploaded; final int failed; SyncResult({ required this.success, required this.message, this.uploaded = 0, this.failed = 0, }); }
6

Mobile UI Implementation

Create mobile-optimized user interface for all core functions.

Main App Structure:

// lib/main.dart import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; void main() { runApp(BlackberryFarmApp()); } class BlackberryFarmApp extends StatelessWidget { @override Widget build(BuildContext context) { return MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => PlantProvider()), ChangeNotifierProvider(create: (_) => SyncProvider()), ChangeNotifierProvider(create: (_) => AuthProvider()), ], child: MaterialApp( title: 'Blackberry Farm Manager', theme: ThemeData( primarySwatch: Colors.green, visualDensity: VisualDensity.adaptivePlatformDensity, ), initialRoute: '/splash', routes: { '/splash': (context) => SplashScreen(), '/login': (context) => LoginScreen(), '/home': (context) => HomeScreen(), '/plants': (context) => PlantsListScreen(), '/plant-details': (context) => PlantDetailsScreen(), '/add-measurement': (context) => AddMeasurementScreen(), '/add-harvest': (context) => AddHarvestScreen(), '/qr-scanner': (context) => QrScannerScreen(), '/sync': (context) => SyncScreen(), }, ), ); } } // lib/screens/home_screen.dart class HomeScreen extends StatefulWidget { @override _HomeScreenState createState() => _HomeScreenState(); } class _HomeScreenState extends State { int _currentIndex = 0; final List _screens = [ DashboardTab(), PlantsTab(), QrScannerTab(), SyncTab(), ]; @override Widget build(BuildContext context) { return Scaffold( body: _screens[_currentIndex], bottomNavigationBar: BottomNavigationBar( type: BottomNavigationBarType.fixed, currentIndex: _currentIndex, onTap: (index) { setState(() { _currentIndex = index; }); }, items: [ BottomNavigationBarItem( icon: Icon(Icons.dashboard), label: 'Dashboard', ), BottomNavigationBarItem( icon: Icon(Icons.eco), label: 'Plants', ), BottomNavigationBarItem( icon: Icon(Icons.qr_code_scanner), label: 'Scan', ), BottomNavigationBarItem( icon: Icon(Icons.sync), label: 'Sync', ), ], ), ); } } // lib/screens/plant_details_screen.dart class PlantDetailsScreen extends StatefulWidget { @override _PlantDetailsScreenState createState() => _PlantDetailsScreenState(); } class _PlantDetailsScreenState extends State { Map? plant; List> measurements = []; List> harvests = []; @override void initState() { super.initState(); _loadPlantData(); } void _loadPlantData() async { final args = ModalRoute.of(context)!.settings.arguments as Map; String plantId = args['plantId']; final db = DatabaseHelper(); setState(() { // Load from local database }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text(plant?['plant_id'] ?? 'Loading...'), actions: [ IconButton( icon: Icon(Icons.edit), onPressed: () { // Navigate to edit screen }, ), ], ), body: plant == null ? Center(child: CircularProgressIndicator()) : SingleChildScrollView( padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildPlantInfoCard(), SizedBox(height: 16), _buildQuickActionsCard(), SizedBox(height: 16), _buildRecentMeasurements(), SizedBox(height: 16), _buildRecentHarvests(), ], ), ), floatingActionButton: FloatingActionButton( onPressed: () { _showQuickActionMenu(); }, child: Icon(Icons.add), ), ); } Widget _buildPlantInfoCard() { return Card( child: Padding( padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Plant Information', style: Theme.of(context).textTheme.headline6), SizedBox(height: 8), _buildInfoRow('Variety', plant!['variety_name']), _buildInfoRow('Location', 'Row ${plant!['location_row']}, Pos ${plant!['location_position']}'), _buildInfoRow('Status', plant!['status']), _buildInfoRow('Planted', plant!['planted_date']), ], ), ), ); } Widget _buildInfoRow(String label, String value) { return Padding( padding: EdgeInsets.symmetric(vertical: 4), child: Row( children: [ SizedBox( width: 80, child: Text( '$label:', style: TextStyle(fontWeight: FontWeight.bold), ), ), Expanded(child: Text(value)), ], ), ); } void _showQuickActionMenu() { showModalBottomSheet( context: context, builder: (context) => Container( height: 300, child: Column( children: [ ListTile( leading: Icon(Icons.straighten), title: Text('Add Measurement'), onTap: () { Navigator.pop(context); Navigator.pushNamed(context, '/add-measurement', arguments: {'plantId': plant!['plant_id']}); }, ), ListTile( leading: Icon(Icons.agriculture), title: Text('Record Harvest'), onTap: () { Navigator.pop(context); Navigator.pushNamed(context, '/add-harvest', arguments: {'plantId': plant!['plant_id']}); }, ), ListTile( leading: Icon(Icons.water_drop), title: Text('Log Watering'), onTap: () { Navigator.pop(context); // Navigate to watering log screen }, ), ListTile( leading: Icon(Icons.note_add), title: Text('Add Note'), onTap: () { Navigator.pop(context); _showAddNoteDialog(); }, ), ], ), ), ); } }
7

GPS Location Integration

Add GPS functionality for automatic location recording of plants and activities.

Location Service Implementation:

// lib/services/location_service.dart import 'package:geolocator/geolocator.dart'; class LocationService { static final LocationService _instance = LocationService._internal(); factory LocationService() => _instance; LocationService._internal(); Future getCurrentLocation() async { bool serviceEnabled; LocationPermission permission; // Check if location services are enabled serviceEnabled = await Geolocator.isLocationServiceEnabled(); if (!serviceEnabled) { return null; } permission = await Geolocator.checkPermission(); if (permission == LocationPermission.denied) { permission = await Geolocator.requestPermission(); if (permission == LocationPermission.denied) { return null; } } if (permission == LocationPermission.deniedForever) { return null; } try { return await Geolocator.getCurrentPosition( desiredAccuracy: LocationAccuracy.high, ); } catch (e) { print('Error getting location: $e'); return null; } } Future calculateDistance( double lat1, double lon1, double lat2, double lon2, ) async { try { return Geolocator.distanceBetween(lat1, lon1, lat2, lon2); } catch (e) { return null; } } Future?> getCurrentLocationData() async { Position? position = await getCurrentLocation(); if (position != null) { return { 'latitude': position.latitude, 'longitude': position.longitude, 'accuracy': position.accuracy, 'timestamp': position.timestamp.toIso8601String(), }; } return null; } }
8

App Testing & Deployment

Test the application thoroughly and prepare for deployment to app stores.

Testing Checklist:

# Device Testing: 1. Test on multiple screen sizes (phone, tablet) 2. Test on different Android versions 3. Test iOS compatibility (if building for iOS) 4. Test offline functionality thoroughly 5. Test sync when coming back online 6. Test QR code scanning in various lighting 7. Test GPS accuracy and battery usage 8. Test app performance with large datasets # Functional Testing: - Plant creation and editing - QR code generation and scanning - Measurement recording - Harvest logging - Data synchronization - Authentication flow - Offline data storage - Location services - Camera permissions - Network connectivity handling # Build and Release: flutter build apk --release # Android APK flutter build appbundle --release # Android App Bundle flutter build ios --release # iOS (requires Mac + Xcode) # App Signing: # Follow platform-specific signing procedures # Set up app store listings # Prepare screenshots and descriptions

Performance Optimization:

// Optimization techniques: 1. Image Optimization: - Compress images before storing - Use appropriate image formats - Implement image caching 2. Database Optimization: - Index frequently queried fields - Implement pagination for large datasets - Clean up old sync records periodically 3. Memory Management: - Dispose of controllers properly - Use lazy loading for large lists - Implement proper state management 4. Battery Optimization: - Minimize GPS usage - Use efficient sync intervals - Implement smart sync (only when needed) - Optimize camera usage 5. Network Optimization: - Compress data before sync - Use delta sync for large datasets - Implement retry mechanisms - Cache frequently accessed data

✅ Phase 7 Completion Checklist

  • Laravel API endpoints with authentication
  • Mobile app framework setup (Flutter/React Native)
  • QR code scanning functionality
  • Offline SQLite database implementation
  • Data synchronization service
  • Mobile-optimized user interface
  • GPS location integration
  • Offline data collection capabilities
  • Camera permissions and integration
  • App testing on multiple devices
  • Performance optimization
  • App store deployment preparation

🎯 Next Steps: Phase 8 Preparation

With Phase 7 complete, you're ready for Phase 8: Launch & Scale. The final phase will involve:

  • Production deployment to hosting
  • Database optimization for scale
  • Performance monitoring setup
  • User training and documentation
  • Preparation for 1,000+ plant management
💾

Create Phase 7 Backup

# Create database backup mysqldump -u wwwhom8_main -p wwwhom8_blackberries > phase7_backup.sql # Create git commit git add . git commit -m "Phase 7: Mobile App Development completed - Flutter app with offline sync, QR scanning, GPS" # Create git tag git tag -a v7.0-phase7 -m "Phase 7: Mobile App Development completed" # Archive mobile app code tar -czf blackberry_mobile_app.tar.gz blackberry_farm_app/