feat: revamp detail screen, add loading shimmer

This commit is contained in:
Syaroful 2024-10-07 09:27:32 +07:00
parent cf8cd34697
commit 10880bddfd
21 changed files with 902 additions and 500 deletions

View File

@ -2,6 +2,7 @@ import 'package:agrilink_vocpro/features/auth/view/login_screen.dart';
import 'package:agrilink_vocpro/features/dashboard/view/dashboard_screen.dart';
import 'package:agrilink_vocpro/features/home/pages/humidity/view/humidity_screen.dart';
import 'package:agrilink_vocpro/features/home/pages/light/view/light_screen.dart';
import 'package:agrilink_vocpro/features/home/pages/ph/view/ph_screen.dart';
import 'package:agrilink_vocpro/features/splash/view/splash_screen.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
@ -17,6 +18,7 @@ class AppRoute {
static const String temperature = '/temperature';
static const String soil = '/soil';
static const String light = '/dashboard/light';
static const String ph = '/dashboard/ph';
static const String water = '/water';
static const String acidity = '/acidity';
@ -53,7 +55,15 @@ class AppRoute {
double.tryParse(state.pathParameters['value'] ?? '') ?? 0.0;
return LightScreen(lightIntensity: value);
},
)
),
GoRoute(
path: 'ph/:value',
builder: (context, state) {
final double value =
double.tryParse(state.pathParameters['value'] ?? '') ?? 0.0;
return PhScreen(phValue: value);
},
),
],
),
],

View File

@ -27,19 +27,22 @@ class MQTTService {
}
Future<ResultState> publishMessage(String topic, String message) async {
final bool isConnected = await isMqttConnected();
if (isConnected) {
final builder = MqttClientPayloadBuilder();
try {
builder.addString(message);
client!.publishMessage(topic, MqttQos.atLeastOnce, builder.payload!);
return ResultState.hasData;
} catch (e) {
print(e);
final bool isConnected = await isMqttConnected(); // Cek apakah terhubung
if (!isConnected) {
print('MQTT: Tidak terhubung ke broker. Tidak bisa publish message.');
return ResultState.error;
}
} else {
print('MQTT: Published message to $topic: $message');
builder.addString(message);
client!.publishMessage(topic, MqttQos.atMostOnce, builder.payload!);
print('MQTT: Message published');
return ResultState.hasData;
} catch (e) {
print('MQTT: Error: $e');
return ResultState.error;
}
}
@ -68,4 +71,55 @@ class MQTTService {
return false; //not connected
}
}
Future<bool> subscribeToTopic(String topic) async {
bool isActive = false;
if (client != null &&
client!.connectionStatus!.state == MqttConnectionState.connected) {
try {
print('MQTT: Subscribing to $topic');
client!.subscribe(topic, MqttQos.atMostOnce);
print('MQTT: Subscribed to $topic');
// Tambahkan log ini untuk memastikan bahwa listener dijalankan
if (client!.updates != null) {
print('MQTT: Listening for updates...');
} else {
print('MQTT: No updates stream available');
}
client!.updates!.listen(
(List<MqttReceivedMessage<MqttMessage?>>? messages) {
print('MQTT: Message received!');
if (messages != null && messages.isNotEmpty) {
final MqttPublishMessage recMessage =
messages[0].payload as MqttPublishMessage;
final String payload = MqttPublishPayload.bytesToStringAsString(
recMessage.payload.message);
print(
'MQTT: Message received on topic ${messages[0].topic}: $payload');
if (payload == 'ON') {
isActive = true;
// Update UI atau provider untuk menandakan relay ON
} else if (payload == 'OFF') {
isActive = false;
// Update UI atau provider untuk menandakan relay OFF
}
} else {
print('MQTT: No messages received');
}
},
);
return isActive;
} catch (e) {
print('MQTT: Error subscribing to $topic: $e');
return isActive;
}
} else {
print('MQTT: Not connected, cannot subscribe.');
return false;
}
}
}

View File

@ -3,59 +3,57 @@ import 'package:agrilink_vocpro/domain/service/mqtt_service.dart';
import 'package:flutter/material.dart';
class ControlProvider extends ChangeNotifier {
bool _control_1 = false;
bool _control_2 = false;
bool _control_3 = false;
bool _control_4 = false;
bool _control_5 = false;
bool _control_6 = false;
final MQTTService _mqttService = MQTTService();
bool _control_1 = false;
// Getters
bool get control_1 => _control_1;
bool get control_2 => _control_2;
bool get control_3 => _control_3;
bool get control_4 => _control_4;
bool get control_5 => _control_5;
bool get control_6 => _control_6;
ControlProvider() {
connectMqtt();
}
ResultState mqttState = ResultState.initial;
ResultState subscribeState = ResultState.initial;
// Koneksi MQTT
Future<void> connectMqtt() async {
mqttState = ResultState.loading;
subscribeState = ResultState.loading;
notifyListeners();
try {
final result = await MQTTService().setupMqtt();
final result = await _mqttService.setupMqtt();
if (result == ResultState.hasData) {
mqttState = result;
print('Connected to MQTT');
final result2 = await _mqttService.subscribeToTopic('relay1');
if (result2 == true) {
subscribeState = ResultState.hasData;
_control_1 = true;
} else {
subscribeState = ResultState.hasData;
_control_1 = false;
}
} else {
mqttState = ResultState.error;
print('Failed to connect to MQTT');
}
} catch (e) {
mqttState = ResultState.error;
print(e);
}
notifyListeners();
}
Future<void> disconnectMqtt() async {
try {
final result = await MQTTService().disconnectMqtt();
if (result == ResultState.hasData) print('Disconnected from MQTT');
await _mqttService.disconnectMqtt();
} catch (e) {
print(e);
rethrow;
}
notifyListeners();
}
Future<bool> isMqttConnected() async {
return true;
}
@override
void dispose() {
disconnectMqtt();
@ -67,28 +65,22 @@ class ControlProvider extends ChangeNotifier {
notifyListeners();
}
void switchControl2() {
_control_2 = !_control_2;
notifyListeners();
Future<ResultState> publishMessage(String topic, String message) async {
try {
final result = await _mqttService.publishMessage(topic, message);
return result;
} catch (e) {
print(e);
rethrow;
}
}
void switchControl3() {
_control_3 = !_control_3;
notifyListeners();
}
void switchControl4() {
_control_4 = !_control_4;
notifyListeners();
}
void switchControl5() {
_control_5 = !_control_5;
notifyListeners();
}
void switchControl6() {
_control_6 = !_control_6;
notifyListeners();
Future<void> subscribeToTopic(String topic) async {
try {
await _mqttService.subscribeToTopic(topic);
} catch (e) {
print(e);
rethrow;
}
}
}

View File

@ -1,6 +1,7 @@
import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/features/control/provider/control_provider.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:provider/provider.dart';
@ -21,15 +22,12 @@ class ControlScreen extends StatelessWidget {
return SafeArea(
child: ListView(
children: [
SizedBox(
height: 16.h,
child: Center(
Center(
child: provider.mqttState == ResultState.loading
? const CircularProgressIndicator()
: provider.mqttState == ResultState.error
? const Text('Failed to connect to MQTT')
: const Text('Connected to MQTT'),
),
? const CupertinoActivityIndicator()
: provider.mqttState == ResultState.hasData
? const Text('Terhubung ke Broker')
: const Text('Gagal terhubung ke Broker'),
),
SizedBox(height: 16.h),
ListTile(
@ -38,7 +36,30 @@ class ControlScreen extends StatelessWidget {
trailing: Switch(
value: provider.control_1,
onChanged: (value) {
provider.control_1 == false
? provider.publishMessage('relay1', 'ON')
: provider.publishMessage('relay1', 'OFF');
provider.switchControl1();
// showDialog(
// context: context,
// builder: (context) => AlertDialog(
// title: const Text('Konfirmasi'),
// content: const Text('Atur Relay 1?'),
// actions: [
// TextButton(
// onPressed: () {
// provider.control_1 == false
// ? provider.publishMessage('relay1', 'ON')
// : provider.publishMessage('relay1', 'OFF');
// provider.switchControl1();
// Navigator.pop(context);
// },
// child:
// Text(provider.control_1 == false ? 'ON' : 'OFF'),
// )
// ],
// ),
// );
},
),
),

View File

@ -1,11 +1,11 @@
import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/features/home/pages/humidity/widgets/circle_chart.dart';
import 'package:agrilink_vocpro/features/home/provider/home_provider.dart';
import 'package:agrilink_vocpro/features/home/widgets/graphic_widget.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:gauge_indicator/gauge_indicator.dart';
import 'package:provider/provider.dart';
class HumidityScreen extends StatelessWidget {
@ -15,7 +15,7 @@ class HumidityScreen extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Humidity', style: AppTheme.labelLarge),
title: Text('Humidity', style: AppTheme.labelMedium),
centerTitle: true,
scrolledUnderElevation: 0,
leading: IconButton(
@ -40,12 +40,46 @@ class HumidityScreen extends StatelessWidget {
SizedBox(
height: MediaQuery.of(context).size.height * 0.05,
),
const SizedBox(
height: 320,
width: double.infinity,
child: CircleChart(
percentage: 60.5,
icon: BootstrapIcons.droplet_half,
SizedBox(
height: 280.h,
child: Stack(
fit: StackFit.expand,
children: [
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(BootstrapIcons.droplet_half,
size: 32, color: Colors.blue),
Text('60 %', style: AppTheme.headline1),
],
),
),
RotatedBox(
quarterTurns: 2,
child: AnimatedRadialGauge(
duration: const Duration(seconds: 3),
curve: Curves.easeOut,
value: 60,
axis: GaugeAxis(
degrees: 360,
min: 0,
max: 100,
pointer: null,
style: GaugeAxisStyle(
background: Colors.grey.shade100,
thickness: 50,
),
progressBar: GaugeBasicProgressBar(
gradient: GaugeAxisGradient(colors: [
Colors.blue.shade200,
Colors.blue,
]),
),
),
),
),
],
),
),
const SizedBox(height: 16),
@ -86,37 +120,37 @@ class HumidityScreen extends StatelessWidget {
padding: EdgeInsets.only(left: 16.w),
child: const Text('Deskripsi'),
),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: provider.humidtyRules.length,
itemBuilder: (context, index) {
final item = provider.humidtyRules[index];
return Theme(
data: Theme.of(context)
.copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
trailing: Text(
item.censorText,
style: TextStyle(color: item.color),
),
expandedCrossAxisAlignment: CrossAxisAlignment.start,
childrenPadding: EdgeInsets.all(16.r),
title: Text(
'Kelembaban ${item.minPercentage}% - ${item.maxPercentage}%'),
children: [
Text(
item.description,
style: AppTheme.labelMedium,
),
SizedBox(height: 8.h),
Text('Tindakan', style: AppTheme.labelSmall),
SizedBox(height: 8.h),
Text(item.action),
],
),
);
})
// ListView.builder(
// shrinkWrap: true,
// physics: const NeverScrollableScrollPhysics(),
// itemCount: provider.humidtyRules.length,
// itemBuilder: (context, index) {
// final item = provider.humidtyRules[index];
// return Theme(
// data: Theme.of(context)
// .copyWith(dividerColor: Colors.transparent),
// child: ExpansionTile(
// trailing: Text(
// item.censorText,
// style: TextStyle(color: item.color),
// ),
// expandedCrossAxisAlignment: CrossAxisAlignment.start,
// childrenPadding: EdgeInsets.all(16.r),
// title: Text(
// 'Kelembaban ${item.minPercentage}% - ${item.maxPercentage}%'),
// children: [
// Text(
// item.description,
// style: AppTheme.labelMedium,
// ),
// SizedBox(height: 8.h),
// Text('Tindakan', style: AppTheme.labelSmall),
// SizedBox(height: 8.h),
// Text(item.action),
// ],
// ),
// );
// })
],
);
}),

View File

@ -1,120 +0,0 @@
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';
class CircleChart extends StatefulWidget {
const CircleChart({
super.key,
required this.percentage,
required this.icon,
this.colorStart,
this.colorEnd,
});
final double percentage;
final IconData icon;
final Color? colorStart;
final Color? colorEnd;
@override
State<CircleChart> createState() => _CircleChartState();
}
class _CircleChartState extends State<CircleChart> {
double currentPercentage = 0;
@override
void initState() {
super.initState();
Future.delayed(Duration.zero, () async {
for (double i = 0; i <= widget.percentage; i++) {
await Future.delayed(const Duration(milliseconds: 25), () {
setState(() {
currentPercentage = i;
});
});
}
});
}
@override
Widget build(BuildContext context) {
return Center(
child: Stack(
alignment: Alignment.center,
children: [
ShaderMask(
shaderCallback: (Rect bounds) {
return RadialGradient(
center: Alignment.center,
radius: 0.6,
colors: <Color>[
Colors.white,
_getAnimatedColor(currentPercentage)
],
tileMode: TileMode.mirror,
).createShader(bounds);
},
child: PieChart(
PieChartData(
sections: _createSections(currentPercentage),
centerSpaceRadius: MediaQuery.of(context).size.width * 0.25,
sectionsSpace: 0,
startDegreeOffset: 270,
borderData: FlBorderData(show: false),
),
),
),
TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0, end: currentPercentage),
duration: const Duration(milliseconds: 300),
builder: (context, value, child) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(widget.icon, size: 32, color: _getAnimatedColor(value)),
Text(
'${value.toStringAsFixed(0)}%', // Animated percentage text
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
],
);
},
),
],
),
);
}
List<PieChartSectionData> _createSections(double percentage) {
return [
PieChartSectionData(
color: Colors.white,
value: percentage,
title: '',
radius: 50, // Size of the pie slice
titleStyle: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
PieChartSectionData(
color: Colors.white24,
value: 100 - percentage,
title: '',
radius: 50,
),
];
}
Color _getAnimatedColor(double percentage) {
return Color.lerp(
widget.colorStart ?? Colors.green,
widget.colorEnd ?? Colors.blue,
percentage / widget.percentage,
)!;
}
}

View File

@ -16,7 +16,7 @@ class LightScreen extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Light', style: AppTheme.titleLarge),
title: Text('Light', style: AppTheme.labelMedium),
centerTitle: true,
backgroundColor: Colors.white,
scrolledUnderElevation: 0,

View File

@ -0,0 +1,178 @@
import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/features/home/pages/ph/widget/ph_bar_pointer.dart';
import 'package:agrilink_vocpro/features/home/widgets/graphic_widget.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class PhScreen extends StatelessWidget {
const PhScreen({super.key, required this.phValue});
final double phValue;
double get value => phValue;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('pH Tanah', style: AppTheme.labelMedium),
centerTitle: true,
backgroundColor: Colors.white,
scrolledUnderElevation: 0,
actions: const [
Padding(
padding: EdgeInsets.only(right: 16),
child: Icon(
BootstrapIcons.thermometer_half,
color: Colors.red,
),
)
],
),
body: SafeArea(
child: ListView(
padding: EdgeInsets.all(16.w),
children: [
SizedBox(
height: MediaQuery.of(context).size.height * 0.05,
),
Center(
child: PhIndicator(phValue: value), // Set nilai pH di sini
),
const SizedBox(height: 16),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'pH',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
),
IconButton(
iconSize: 20.r,
color: Colors.blue,
onPressed: () {},
icon: const Icon(BootstrapIcons.info_circle))
],
),
SizedBox(height: 16.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Container(
height: 100.h,
width: 100.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: Colors.blue.withOpacity(0.1),
border: Border.all(
color: Colors.blue,
width: 2,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Low',
style: AppTheme.labelMedium
.copyWith(color: Colors.blue)),
// SizedBox(height: 8.h),
// const Icon(
// BootstrapIcons.thermometer_low,
// color: Colors.blue,
// ),
SizedBox(height: 8.h),
Text(
'<20°C',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
),
],
),
),
Container(
height: 100.h,
width: 100.w,
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.green,
width: 2,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Ideal',
style: AppTheme.labelMedium
.copyWith(color: Colors.green)),
// SizedBox(height: 8.h),
// const Icon(
// BootstrapIcons.thermometer_half,
// color: Colors.green,
// ),
SizedBox(height: 8.h),
Text(
'20-30°C',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
),
],
),
),
Container(
height: 100.h,
width: 100.w,
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.orange.shade800,
width: 2,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('high',
style: AppTheme.labelMedium
.copyWith(color: Colors.orange)),
// SizedBox(height: 8.h),
// const Icon(
// BootstrapIcons.thermometer_high,
// color: Colors.orange,
// ),
SizedBox(height: 8.h),
Text(
'>30°C',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
),
],
),
),
],
),
SizedBox(height: 16.h),
const Text('Grafik'),
SizedBox(height: 16.h),
AspectRatio(
aspectRatio: 1.6.h,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.w),
border: Border.all(color: Colors.grey.shade300, width: 1.w),
),
child: const GarphicWidget(),
),
)
],
),
),
);
}
}

View File

@ -0,0 +1,88 @@
import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class PhIndicator extends StatelessWidget {
final double phValue; // Nilai pH yang ditampilkan (misal 3.5 atau 6.7)
const PhIndicator({super.key, required this.phValue});
@override
Widget build(BuildContext context) {
return Column(
children: [
Center(
child: Text(
'pH : $phValue',
style: AppTheme.headline1,
),
),
SizedBox(height: 16.h),
SizedBox(
height: 80.h,
child: Stack(
alignment: Alignment.center,
children: [
CustomPaint(
size: Size(300.w, 30.h), // Ukuran bar
painter: PhBarPainter(),
),
Positioned(
top: 48.h,
left: calculatePointerPosition(
phValue, 300), // Hitung posisi pointer
child: const Icon(
Icons.arrow_drop_up,
size: 40,
color: Colors.black, // Warna pointer
),
),
const Positioned(
top: 0,
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('0'),
Text('7'),
Text('14'),
],
),
),
],
),
),
],
);
}
// Fungsi untuk menghitung posisi pointer berdasarkan nilai pH
double calculatePointerPosition(double phValue, double barWidth) {
// Nilai pH umumnya antara 0 dan 14, sesuaikan dengan lebar bar
double normalizedPh = phValue / 14; // Normalisasi nilai pH (-7 hingga 7)
return normalizedPh * barWidth;
}
}
class PhBarPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// Buat gradasi dari biru ke merah
const gradient = LinearGradient(
colors: [Colors.blue, Colors.orange, Colors.red],
stops: [0.0, 0.5, 1.0], // Set stop untuk 0, 7, 14 (pH)
);
// Buat rect untuk menggambar gradasi
final rect = Rect.fromLTWH(0, 0, size.width, size.height);
final paint = Paint()..shader = gradient.createShader(rect);
// Gambar rect dengan gradasi
canvas.drawRect(rect, paint);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => false;
}

View File

@ -1,7 +1,9 @@
import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/features/home/pages/humidity/widgets/circle_chart.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:gauge_indicator/gauge_indicator.dart';
class SoilMoistureScreen extends StatelessWidget {
const SoilMoistureScreen({super.key});
@ -33,14 +35,46 @@ class SoilMoistureScreen extends StatelessWidget {
SizedBox(
height: MediaQuery.of(context).size.height * 0.05,
),
const SizedBox(
height: 320,
width: double.infinity,
child: CircleChart(
percentage: 60.5,
icon: Icons.water_outlined,
colorStart: Colors.lime,
colorEnd: Colors.brown,
SizedBox(
height: 280.h,
child: Stack(
fit: StackFit.expand,
children: [
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(BootstrapIcons.water,
size: 32, color: Colors.blue),
Text('60 %', style: AppTheme.headline1),
],
),
),
RotatedBox(
quarterTurns: 2,
child: AnimatedRadialGauge(
duration: const Duration(seconds: 3),
curve: Curves.easeOut,
value: 60,
axis: GaugeAxis(
degrees: 360,
min: 0,
max: 100,
pointer: null,
style: GaugeAxisStyle(
background: Colors.grey.shade100,
thickness: 50,
),
progressBar: GaugeBasicProgressBar(
gradient: GaugeAxisGradient(colors: [
Colors.blue.shade200,
Colors.blue,
]),
),
),
),
),
],
),
),
const SizedBox(height: 16),

View File

@ -14,7 +14,7 @@ class TemperatureScreen extends StatelessWidget {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Temperature', style: AppTheme.titleLarge),
title: Text('Temperature', style: AppTheme.labelMedium),
centerTitle: true,
backgroundColor: Colors.white,
scrolledUnderElevation: 0,

View File

@ -1,59 +1,87 @@
import 'package:agrilink_vocpro/features/dashboard/model/censor_data_rule.dart';
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:flutter/material.dart';
class HomeProvider extends ChangeNotifier {
final DateTime currentDate = DateTime.now();
List<CensorDataRule> humidtyRules = [
CensorDataRule(
minPercentage: 0,
maxPercentage: 30,
censorText: 'Very Low',
description:
'Udara sangat kering. Tanaman bisa mengalami stress akibat kekurangan air.',
action:
'Aktifkan sistem penyiraman atau humidifier untuk menaikkan kelembaban. Periksa juga apakah ada kebocoran pada sistem irigasi yang mengakibatkan kelembaban terlalu rendah.',
color: Colors.red,
),
CensorDataRule(
minPercentage: 31,
maxPercentage: 50,
censorText: 'Low',
description:
'Kelembaban masih cukup rendah. Beberapa jenis tanaman mungkin sudah mulai terpengaruh.',
action:
'Pertimbangkan untuk menambah irigasi atau memperpanjang durasi penyiraman. Pantau tanaman secara berkala.',
color: Colors.orange,
),
CensorDataRule(
minPercentage: 51,
maxPercentage: 70,
censorText: 'Normal',
description:
'Ini adalah kelembaban yang ideal untuk sebagian besar tanaman dalam greenhouse.',
action:
'Pertahankan kondisi ini. Tidak ada tindakan yang diperlukan kecuali jika ada perubahan mendadak.',
color: Colors.green,
),
CensorDataRule(
minPercentage: 71,
maxPercentage: 85,
censorText: 'High',
description:
'Udara mulai terlalu lembap. Kelembaban tinggi dapat meningkatkan risiko penyakit jamur atau bakteri.',
action:
'Aktifkan ventilasi atau kipas untuk mengurangi kelembaban. Pastikan aliran udara di greenhouse cukup baik.',
color: Colors.lime,
),
CensorDataRule(
minPercentage: 86,
maxPercentage: 100,
censorText: 'Very High',
description:
'Udara sangat lembap, yang bisa berisiko menyebabkan jamur, lumut, dan penyakit tanaman.',
action:
'Segera aktifkan sistem ventilasi maksimal, mungkin juga gunakan dehumidifier jika diperlukan. Kurangi frekuensi penyiraman atau periksa sistem irigasi agar tidak berlebihan.',
color: Colors.brown,
),
];
HomeProvider() {
getData();
}
ResultState dataState = ResultState.initial;
Future<void> getData() async {
dataState = ResultState.loading;
notifyListeners();
try {
print('Fetching data...');
await Future.delayed(const Duration(seconds: 3));
print('Data fetched');
dataState = ResultState.hasData;
notifyListeners();
} catch (e) {
dataState = ResultState.error;
notifyListeners();
}
}
@override
void dispose() {
dataState = ResultState.initial;
super.dispose();
}
}
// List<CensorDataRule> humidtyRules = [
// CensorDataRule(
// minPercentage: 0,
// maxPercentage: 30,
// censorText: 'Very Low',
// description:
// 'Udara sangat kering. Tanaman bisa mengalami stress akibat kekurangan air.',
// action:
// 'Aktifkan sistem penyiraman atau humidifier untuk menaikkan kelembaban. Periksa juga apakah ada kebocoran pada sistem irigasi yang mengakibatkan kelembaban terlalu rendah.',
// color: Colors.red,
// ),
// CensorDataRule(
// minPercentage: 31,
// maxPercentage: 50,
// censorText: 'Low',
// description:
// 'Kelembaban masih cukup rendah. Beberapa jenis tanaman mungkin sudah mulai terpengaruh.',
// action:
// 'Pertimbangkan untuk menambah irigasi atau memperpanjang durasi penyiraman. Pantau tanaman secara berkala.',
// color: Colors.orange,
// ),
// CensorDataRule(
// minPercentage: 51,
// maxPercentage: 70,
// censorText: 'Normal',
// description:
// 'Ini adalah kelembaban yang ideal untuk sebagian besar tanaman dalam greenhouse.',
// action:
// 'Pertahankan kondisi ini. Tidak ada tindakan yang diperlukan kecuali jika ada perubahan mendadak.',
// color: Colors.green,
// ),
// CensorDataRule(
// minPercentage: 71,
// maxPercentage: 85,
// censorText: 'High',
// description:
// 'Udara mulai terlalu lembap. Kelembaban tinggi dapat meningkatkan risiko penyakit jamur atau bakteri.',
// action:
// 'Aktifkan ventilasi atau kipas untuk mengurangi kelembaban. Pastikan aliran udara di greenhouse cukup baik.',
// color: Colors.lime,
// ),
// CensorDataRule(
// minPercentage: 86,
// maxPercentage: 100,
// censorText: 'Very High',
// description:
// 'Udara sangat lembap, yang bisa berisiko menyebabkan jamur, lumut, dan penyakit tanaman.',
// action:
// 'Segera aktifkan sistem ventilasi maksimal, mungkin juga gunakan dehumidifier jika diperlukan. Kurangi frekuensi penyiraman atau periksa sistem irigasi agar tidak berlebihan.',
// color: Colors.brown,
// ),
// ];

View File

@ -1,12 +1,14 @@
import 'package:agrilink_vocpro/core/constant/app_color.dart';
import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/core/extension/extention.dart';
import 'package:agrilink_vocpro/features/home/provider/home_provider.dart';
import 'package:agrilink_vocpro/features/home/widgets/list_data_from_censor_npk1.dart';
import 'package:agrilink_vocpro/features/home/widgets/list_data_from_censor_npk2.dart';
import 'package:agrilink_vocpro/features/home/widgets/list_data_from_main_censor.dart';
import 'package:agrilink_vocpro/features/home/widgets/list_data_from_censor_dht.dart';
import 'package:animated_segmented_tab_control/animated_segmented_tab_control.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:provider/provider.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@ -23,7 +25,7 @@ class _HomeScreenState extends State<HomeScreen> {
appBar: AppBar(
toolbarHeight: 200.h,
elevation: 0,
backgroundColor: AppColor.backgroundColor,
backgroundColor: Colors.white,
scrolledUnderElevation: 0,
flexibleSpace: Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
@ -55,6 +57,16 @@ class _HomeScreenState extends State<HomeScreen> {
),
],
),
const Spacer(),
IconButton(
onPressed: () {
context.read<HomeProvider>().getData();
},
icon: const Icon(
Icons.refresh_rounded,
color: AppColor.primary,
),
)
],
),
SizedBox(
@ -130,7 +142,7 @@ class _HomeScreenState extends State<HomeScreen> {
),
textStyle: AppTheme.labelSmall,
tabs: const [
SegmentTab(label: 'Main Censor'),
SegmentTab(label: 'DHT'),
SegmentTab(label: 'NPK 1'),
SegmentTab(label: 'NPK 2'),
]),
@ -138,7 +150,7 @@ class _HomeScreenState extends State<HomeScreen> {
Padding(
padding: EdgeInsets.only(top: 64.h),
child: const TabBarView(children: [
ListDataFromMainCensor(),
ListDataFromCensorDht(),
ListDataFromCensorNpk1(),
ListDataFromCensorNpk2(),
]),

View File

@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:shimmer/shimmer.dart';
class CensorItemLoadingWidgets extends StatelessWidget {
const CensorItemLoadingWidgets({super.key});
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: Colors.grey.shade300,
highlightColor: Colors.grey.shade100,
child: Container(
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(16.r),
),
),
);
}
}

View File

@ -0,0 +1,103 @@
import 'package:agrilink_vocpro/core/constant/app_color.dart';
import 'package:agrilink_vocpro/core/route/app_route.dart';
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/features/home/pages/temperature/view/temperature_screen.dart';
import 'package:agrilink_vocpro/features/home/provider/home_provider.dart';
import 'package:agrilink_vocpro/features/home/widgets/censor_item_loading_widgets.dart';
import 'package:agrilink_vocpro/features/home/widgets/data_display_widget.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
class ListDataFromCensorDht extends StatelessWidget {
const ListDataFromCensorDht({
super.key,
});
@override
Widget build(BuildContext context) {
return Consumer<HomeProvider>(builder: (context, provider, child) {
switch (provider.dataState) {
case ResultState.loading:
return GridView(
padding: EdgeInsets.all(16.r),
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16.r,
mainAxisSpacing: 16.r,
childAspectRatio: 0.9,
),
children: [
for (int i = 0; i < 4; i++) const CensorItemLoadingWidgets(),
],
);
case ResultState.hasData:
return GridView(
padding: EdgeInsets.all(16.r),
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16.r,
mainAxisSpacing: 16.r,
childAspectRatio: 0.9,
),
children: [
DataDisplayerWidget(
title: 'Humidity',
subtitle: 'kelembaban udara',
value: '60',
unit: '%',
icon: BootstrapIcons.droplet_half,
textColor: Colors.white,
color: AppColor.secondary,
iconColor: Colors.white,
censorIdentifier: 'NPK 1',
onTap: () async {
context.push(AppRoute.humidity, extra: '60');
},
),
DataDisplayerWidget(
title: 'Temperature',
subtitle: 'suhu greenhouse',
value: '28',
unit: '°C',
icon: BootstrapIcons.thermometer_half,
color: Colors.white,
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const TemperatureScreen()));
},
),
DataDisplayerWidget(
title: 'Light',
subtitle: 'intensitas cahaya',
value: '1000',
unit: 'lux',
icon: BootstrapIcons.sun,
color: Colors.white,
onTap: () async {
context.push('${AppRoute.light}/300');
},
),
],
);
case ResultState.noData:
return const Center(child: Text('No Data'));
case ResultState.error:
return const Center(child: Text('Error'));
case ResultState.initial:
return const SizedBox.shrink();
default:
return const SizedBox.shrink();
}
});
}
}

View File

@ -1,13 +1,17 @@
import 'package:agrilink_vocpro/core/constant/app_color.dart';
import 'package:agrilink_vocpro/core/route/app_route.dart';
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/features/home/pages/soil_moisture/view/soil_moisture_screen.dart';
import 'package:agrilink_vocpro/features/home/pages/temperature/view/temperature_screen.dart';
import 'package:agrilink_vocpro/features/home/provider/home_provider.dart';
import 'package:agrilink_vocpro/features/home/widgets/censor_item_loading_widgets.dart';
import 'package:agrilink_vocpro/features/home/widgets/data_display_widget.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
class ListDataFromCensorNpk1 extends StatelessWidget {
const ListDataFromCensorNpk1({
@ -17,6 +21,23 @@ class ListDataFromCensorNpk1 extends StatelessWidget {
@override
Widget build(BuildContext context) {
const String censorIdentifier = 'NPK 1';
return Consumer<HomeProvider>(builder: (context, provider, child) {
switch (provider.dataState) {
case ResultState.loading:
return GridView(
padding: EdgeInsets.all(16.r),
shrinkWrap: true,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16.r,
mainAxisSpacing: 16.r,
childAspectRatio: 0.9,
),
children: [
for (int i = 0; i < 4; i++) const CensorItemLoadingWidgets(),
],
);
case ResultState.hasData:
return GridView(
padding: EdgeInsets.all(16.r),
shrinkWrap: true,
@ -69,7 +90,9 @@ class ListDataFromCensorNpk1 extends StatelessWidget {
icon: BootstrapIcons.pie_chart,
color: Colors.white,
censorIdentifier: censorIdentifier,
onTap: () {},
onTap: () {
context.push('${AppRoute.ph}/6.5');
},
),
DataDisplayerWidget(
title: 'Conductivity',
@ -112,5 +135,13 @@ class ListDataFromCensorNpk1 extends StatelessWidget {
),
],
);
case ResultState.noData:
return const Center(child: Text('No Data'));
case ResultState.error:
return const Center(child: Text('Error'));
default:
return const SizedBox.shrink();
}
});
}
}

View File

@ -1,70 +0,0 @@
import 'package:agrilink_vocpro/core/constant/app_color.dart';
import 'package:agrilink_vocpro/core/route/app_route.dart';
import 'package:agrilink_vocpro/features/home/pages/temperature/view/temperature_screen.dart';
import 'package:agrilink_vocpro/features/home/widgets/data_display_widget.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:go_router/go_router.dart';
class ListDataFromMainCensor extends StatelessWidget {
const ListDataFromMainCensor({
super.key,
});
@override
Widget build(BuildContext context) {
return GridView(
padding: EdgeInsets.all(16.r),
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 16.r,
mainAxisSpacing: 16.r,
childAspectRatio: 0.9,
),
children: [
DataDisplayerWidget(
title: 'Humidity',
subtitle: 'kelembaban udara',
value: '60',
unit: '%',
icon: BootstrapIcons.droplet_half,
textColor: Colors.white,
color: AppColor.secondary,
iconColor: Colors.white,
censorIdentifier: 'NPK 1',
onTap: () async {
context.push(AppRoute.humidity, extra: '60');
},
),
DataDisplayerWidget(
title: 'Temperature',
subtitle: 'suhu greenhouse',
value: '28',
unit: '°C',
icon: BootstrapIcons.thermometer_half,
color: Colors.white,
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const TemperatureScreen()));
},
),
DataDisplayerWidget(
title: 'Light',
subtitle: 'intensitas cahaya',
value: '1000',
unit: 'lux',
icon: BootstrapIcons.sun,
color: Colors.white,
onTap: () async {
context.push('${AppRoute.light}/300');
},
),
],
);
}
}

View File

@ -2,10 +2,12 @@ import 'dart:io';
import 'package:agrilink_vocpro/core/constant/app_color.dart';
import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/core/route/app_route.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:go_router/go_router.dart';
class SettingScreen extends StatelessWidget {
const SettingScreen({super.key});
@ -96,7 +98,6 @@ class SettingScreen extends StatelessWidget {
size: 16.r,
),
onTap: () {
if (Platform.isAndroid) {
showDialog(
context: context,
builder: (context) => AlertDialog(
@ -112,35 +113,12 @@ class SettingScreen extends StatelessWidget {
TextButton(
child: Text('Ya'),
onPressed: () {
Navigator.pop(context);
context.go(AppRoute.root);
},
),
],
),
);
} else {
showDialog(
context: context,
builder: (context) => CupertinoAlertDialog(
title: Text('Logout'),
content: Text('Apakah anda yakin ingin logout?'),
actions: [
TextButton(
child: Text('Batal'),
onPressed: () {
Navigator.pop(context);
},
),
TextButton(
child: Text('Ya'),
onPressed: () {
Navigator.pop(context);
},
),
],
),
);
}
}),
],
),

View File

@ -1,4 +1,3 @@
import 'package:agrilink_vocpro/core/constant/app_color.dart';
import 'package:agrilink_vocpro/core/route/app_route.dart';
import 'package:agrilink_vocpro/features/auth/provider/auth_provider.dart';
import 'package:agrilink_vocpro/features/control/provider/control_provider.dart';

View File

@ -456,6 +456,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shimmer:
dependency: "direct main"
description:
name: shimmer
sha256: "5f88c883a22e9f9f299e5ba0e4f7e6054857224976a5d9f839d4ebdc94a14ac9"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
sky_engine:
dependency: transitive
description: flutter

View File

@ -46,6 +46,7 @@ dependencies:
flutter_screenutil: ^5.9.3
gauge_indicator: ^0.4.3
mqtt_client: ^10.5.1
shimmer: ^3.0.0
dev_dependencies:
flutter_test: