📋 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
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;
}
}
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
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();
}
}
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
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
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
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;
}
}
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 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/