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/dashboard/view/dashboard_screen.dart';
import 'package:agrilink_vocpro/features/home/pages/humidity/view/humidity_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/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:agrilink_vocpro/features/splash/view/splash_screen.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@ -17,6 +18,7 @@ class AppRoute {
static const String temperature = '/temperature'; static const String temperature = '/temperature';
static const String soil = '/soil'; static const String soil = '/soil';
static const String light = '/dashboard/light'; static const String light = '/dashboard/light';
static const String ph = '/dashboard/ph';
static const String water = '/water'; static const String water = '/water';
static const String acidity = '/acidity'; static const String acidity = '/acidity';
@ -53,7 +55,15 @@ class AppRoute {
double.tryParse(state.pathParameters['value'] ?? '') ?? 0.0; double.tryParse(state.pathParameters['value'] ?? '') ?? 0.0;
return LightScreen(lightIntensity: value); 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 { Future<ResultState> publishMessage(String topic, String message) async {
final bool isConnected = await isMqttConnected();
if (isConnected) {
final builder = MqttClientPayloadBuilder(); final builder = MqttClientPayloadBuilder();
try { try {
builder.addString(message); final bool isConnected = await isMqttConnected(); // Cek apakah terhubung
client!.publishMessage(topic, MqttQos.atLeastOnce, builder.payload!); if (!isConnected) {
return ResultState.hasData; print('MQTT: Tidak terhubung ke broker. Tidak bisa publish message.');
} catch (e) {
print(e);
return ResultState.error; 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; return ResultState.error;
} }
} }
@ -68,4 +71,55 @@ class MQTTService {
return false; //not connected 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'; import 'package:flutter/material.dart';
class ControlProvider extends ChangeNotifier { class ControlProvider extends ChangeNotifier {
bool _control_1 = false; final MQTTService _mqttService = MQTTService();
bool _control_2 = false;
bool _control_3 = false; bool _control_1 = false;
bool _control_4 = false;
bool _control_5 = false;
bool _control_6 = false;
// Getters
bool get control_1 => _control_1; 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() { ControlProvider() {
connectMqtt(); connectMqtt();
} }
ResultState mqttState = ResultState.initial; ResultState mqttState = ResultState.initial;
ResultState subscribeState = ResultState.initial;
// Koneksi MQTT
Future<void> connectMqtt() async { Future<void> connectMqtt() async {
mqttState = ResultState.loading; mqttState = ResultState.loading;
subscribeState = ResultState.loading;
notifyListeners();
try { try {
final result = await MQTTService().setupMqtt(); final result = await _mqttService.setupMqtt();
if (result == ResultState.hasData) { if (result == ResultState.hasData) {
mqttState = result; 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 { } else {
mqttState = ResultState.error; mqttState = ResultState.error;
print('Failed to connect to MQTT');
} }
} catch (e) { } catch (e) {
mqttState = ResultState.error;
print(e); print(e);
} }
notifyListeners(); notifyListeners();
} }
Future<void> disconnectMqtt() async { Future<void> disconnectMqtt() async {
try { try {
final result = await MQTTService().disconnectMqtt(); await _mqttService.disconnectMqtt();
if (result == ResultState.hasData) print('Disconnected from MQTT');
} catch (e) { } catch (e) {
print(e);
rethrow; rethrow;
} }
notifyListeners(); notifyListeners();
} }
Future<bool> isMqttConnected() async {
return true;
}
@override @override
void dispose() { void dispose() {
disconnectMqtt(); disconnectMqtt();
@ -67,28 +65,22 @@ class ControlProvider extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
void switchControl2() { Future<ResultState> publishMessage(String topic, String message) async {
_control_2 = !_control_2; try {
notifyListeners(); final result = await _mqttService.publishMessage(topic, message);
return result;
} catch (e) {
print(e);
rethrow;
}
} }
void switchControl3() { Future<void> subscribeToTopic(String topic) async {
_control_3 = !_control_3; try {
notifyListeners(); await _mqttService.subscribeToTopic(topic);
} catch (e) {
print(e);
rethrow;
} }
void switchControl4() {
_control_4 = !_control_4;
notifyListeners();
}
void switchControl5() {
_control_5 = !_control_5;
notifyListeners();
}
void switchControl6() {
_control_6 = !_control_6;
notifyListeners();
} }
} }

View File

@ -1,6 +1,7 @@
import 'package:agrilink_vocpro/core/constant/app_theme.dart'; import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/core/state/result_state.dart'; import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/features/control/provider/control_provider.dart'; import 'package:agrilink_vocpro/features/control/provider/control_provider.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -21,15 +22,12 @@ class ControlScreen extends StatelessWidget {
return SafeArea( return SafeArea(
child: ListView( child: ListView(
children: [ children: [
SizedBox( Center(
height: 16.h,
child: Center(
child: provider.mqttState == ResultState.loading child: provider.mqttState == ResultState.loading
? const CircularProgressIndicator() ? const CupertinoActivityIndicator()
: provider.mqttState == ResultState.error : provider.mqttState == ResultState.hasData
? const Text('Failed to connect to MQTT') ? const Text('Terhubung ke Broker')
: const Text('Connected to MQTT'), : const Text('Gagal terhubung ke Broker'),
),
), ),
SizedBox(height: 16.h), SizedBox(height: 16.h),
ListTile( ListTile(
@ -38,7 +36,30 @@ class ControlScreen extends StatelessWidget {
trailing: Switch( trailing: Switch(
value: provider.control_1, value: provider.control_1,
onChanged: (value) { onChanged: (value) {
provider.control_1 == false
? provider.publishMessage('relay1', 'ON')
: provider.publishMessage('relay1', 'OFF');
provider.switchControl1(); 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/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/provider/home_provider.dart';
import 'package:agrilink_vocpro/features/home/widgets/graphic_widget.dart'; import 'package:agrilink_vocpro/features/home/widgets/graphic_widget.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart'; import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:gauge_indicator/gauge_indicator.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class HumidityScreen extends StatelessWidget { class HumidityScreen extends StatelessWidget {
@ -15,7 +15,7 @@ class HumidityScreen extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('Humidity', style: AppTheme.labelLarge), title: Text('Humidity', style: AppTheme.labelMedium),
centerTitle: true, centerTitle: true,
scrolledUnderElevation: 0, scrolledUnderElevation: 0,
leading: IconButton( leading: IconButton(
@ -40,12 +40,46 @@ class HumidityScreen extends StatelessWidget {
SizedBox( SizedBox(
height: MediaQuery.of(context).size.height * 0.05, height: MediaQuery.of(context).size.height * 0.05,
), ),
const SizedBox( SizedBox(
height: 320, height: 280.h,
width: double.infinity, child: Stack(
child: CircleChart( fit: StackFit.expand,
percentage: 60.5, children: [
icon: BootstrapIcons.droplet_half, 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), const SizedBox(height: 16),
@ -86,37 +120,37 @@ class HumidityScreen extends StatelessWidget {
padding: EdgeInsets.only(left: 16.w), padding: EdgeInsets.only(left: 16.w),
child: const Text('Deskripsi'), child: const Text('Deskripsi'),
), ),
ListView.builder( // ListView.builder(
shrinkWrap: true, // shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), // physics: const NeverScrollableScrollPhysics(),
itemCount: provider.humidtyRules.length, // itemCount: provider.humidtyRules.length,
itemBuilder: (context, index) { // itemBuilder: (context, index) {
final item = provider.humidtyRules[index]; // final item = provider.humidtyRules[index];
return Theme( // return Theme(
data: Theme.of(context) // data: Theme.of(context)
.copyWith(dividerColor: Colors.transparent), // .copyWith(dividerColor: Colors.transparent),
child: ExpansionTile( // child: ExpansionTile(
trailing: Text( // trailing: Text(
item.censorText, // item.censorText,
style: TextStyle(color: item.color), // style: TextStyle(color: item.color),
), // ),
expandedCrossAxisAlignment: CrossAxisAlignment.start, // expandedCrossAxisAlignment: CrossAxisAlignment.start,
childrenPadding: EdgeInsets.all(16.r), // childrenPadding: EdgeInsets.all(16.r),
title: Text( // title: Text(
'Kelembaban ${item.minPercentage}% - ${item.maxPercentage}%'), // 'Kelembaban ${item.minPercentage}% - ${item.maxPercentage}%'),
children: [ // children: [
Text( // Text(
item.description, // item.description,
style: AppTheme.labelMedium, // style: AppTheme.labelMedium,
), // ),
SizedBox(height: 8.h), // SizedBox(height: 8.h),
Text('Tindakan', style: AppTheme.labelSmall), // Text('Tindakan', style: AppTheme.labelSmall),
SizedBox(height: 8.h), // SizedBox(height: 8.h),
Text(item.action), // 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) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('Light', style: AppTheme.titleLarge), title: Text('Light', style: AppTheme.labelMedium),
centerTitle: true, centerTitle: true,
backgroundColor: Colors.white, backgroundColor: Colors.white,
scrolledUnderElevation: 0, 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/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/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:gauge_indicator/gauge_indicator.dart';
class SoilMoistureScreen extends StatelessWidget { class SoilMoistureScreen extends StatelessWidget {
const SoilMoistureScreen({super.key}); const SoilMoistureScreen({super.key});
@ -33,14 +35,46 @@ class SoilMoistureScreen extends StatelessWidget {
SizedBox( SizedBox(
height: MediaQuery.of(context).size.height * 0.05, height: MediaQuery.of(context).size.height * 0.05,
), ),
const SizedBox( SizedBox(
height: 320, height: 280.h,
width: double.infinity, child: Stack(
child: CircleChart( fit: StackFit.expand,
percentage: 60.5, children: [
icon: Icons.water_outlined, Center(
colorStart: Colors.lime, child: Column(
colorEnd: Colors.brown, 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), const SizedBox(height: 16),

View File

@ -14,7 +14,7 @@ class TemperatureScreen extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('Temperature', style: AppTheme.titleLarge), title: Text('Temperature', style: AppTheme.labelMedium),
centerTitle: true, centerTitle: true,
backgroundColor: Colors.white, backgroundColor: Colors.white,
scrolledUnderElevation: 0, 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'; import 'package:flutter/material.dart';
class HomeProvider extends ChangeNotifier { class HomeProvider extends ChangeNotifier {
final DateTime currentDate = DateTime.now(); final DateTime currentDate = DateTime.now();
List<CensorDataRule> humidtyRules = [ HomeProvider() {
CensorDataRule( getData();
minPercentage: 0, }
maxPercentage: 30,
censorText: 'Very Low', ResultState dataState = ResultState.initial;
description:
'Udara sangat kering. Tanaman bisa mengalami stress akibat kekurangan air.', Future<void> getData() async {
action: dataState = ResultState.loading;
'Aktifkan sistem penyiraman atau humidifier untuk menaikkan kelembaban. Periksa juga apakah ada kebocoran pada sistem irigasi yang mengakibatkan kelembaban terlalu rendah.', notifyListeners();
color: Colors.red, try {
), print('Fetching data...');
CensorDataRule( await Future.delayed(const Duration(seconds: 3));
minPercentage: 31, print('Data fetched');
maxPercentage: 50, dataState = ResultState.hasData;
censorText: 'Low', notifyListeners();
description: } catch (e) {
'Kelembaban masih cukup rendah. Beberapa jenis tanaman mungkin sudah mulai terpengaruh.', dataState = ResultState.error;
action: notifyListeners();
'Pertimbangkan untuk menambah irigasi atau memperpanjang durasi penyiraman. Pantau tanaman secara berkala.', }
color: Colors.orange, }
),
CensorDataRule( @override
minPercentage: 51, void dispose() {
maxPercentage: 70, dataState = ResultState.initial;
censorText: 'Normal', super.dispose();
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,
),
];
} }
// 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_color.dart';
import 'package:agrilink_vocpro/core/constant/app_theme.dart'; import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/core/extension/extention.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_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_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:animated_segmented_tab_control/animated_segmented_tab_control.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:provider/provider.dart';
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
const HomeScreen({super.key}); const HomeScreen({super.key});
@ -23,7 +25,7 @@ class _HomeScreenState extends State<HomeScreen> {
appBar: AppBar( appBar: AppBar(
toolbarHeight: 200.h, toolbarHeight: 200.h,
elevation: 0, elevation: 0,
backgroundColor: AppColor.backgroundColor, backgroundColor: Colors.white,
scrolledUnderElevation: 0, scrolledUnderElevation: 0,
flexibleSpace: Padding( flexibleSpace: Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w), 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( SizedBox(
@ -130,7 +142,7 @@ class _HomeScreenState extends State<HomeScreen> {
), ),
textStyle: AppTheme.labelSmall, textStyle: AppTheme.labelSmall,
tabs: const [ tabs: const [
SegmentTab(label: 'Main Censor'), SegmentTab(label: 'DHT'),
SegmentTab(label: 'NPK 1'), SegmentTab(label: 'NPK 1'),
SegmentTab(label: 'NPK 2'), SegmentTab(label: 'NPK 2'),
]), ]),
@ -138,7 +150,7 @@ class _HomeScreenState extends State<HomeScreen> {
Padding( Padding(
padding: EdgeInsets.only(top: 64.h), padding: EdgeInsets.only(top: 64.h),
child: const TabBarView(children: [ child: const TabBarView(children: [
ListDataFromMainCensor(), ListDataFromCensorDht(),
ListDataFromCensorNpk1(), ListDataFromCensorNpk1(),
ListDataFromCensorNpk2(), 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/constant/app_color.dart';
import 'package:agrilink_vocpro/core/route/app_route.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/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/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:agrilink_vocpro/features/home/widgets/data_display_widget.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart'; import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
class ListDataFromCensorNpk1 extends StatelessWidget { class ListDataFromCensorNpk1 extends StatelessWidget {
const ListDataFromCensorNpk1({ const ListDataFromCensorNpk1({
@ -17,6 +21,23 @@ class ListDataFromCensorNpk1 extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const String censorIdentifier = 'NPK 1'; 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( return GridView(
padding: EdgeInsets.all(16.r), padding: EdgeInsets.all(16.r),
shrinkWrap: true, shrinkWrap: true,
@ -69,7 +90,9 @@ class ListDataFromCensorNpk1 extends StatelessWidget {
icon: BootstrapIcons.pie_chart, icon: BootstrapIcons.pie_chart,
color: Colors.white, color: Colors.white,
censorIdentifier: censorIdentifier, censorIdentifier: censorIdentifier,
onTap: () {}, onTap: () {
context.push('${AppRoute.ph}/6.5');
},
), ),
DataDisplayerWidget( DataDisplayerWidget(
title: 'Conductivity', 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_color.dart';
import 'package:agrilink_vocpro/core/constant/app_theme.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:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:go_router/go_router.dart';
class SettingScreen extends StatelessWidget { class SettingScreen extends StatelessWidget {
const SettingScreen({super.key}); const SettingScreen({super.key});
@ -96,7 +98,6 @@ class SettingScreen extends StatelessWidget {
size: 16.r, size: 16.r,
), ),
onTap: () { onTap: () {
if (Platform.isAndroid) {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
@ -112,35 +113,12 @@ class SettingScreen extends StatelessWidget {
TextButton( TextButton(
child: Text('Ya'), child: Text('Ya'),
onPressed: () { 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/core/route/app_route.dart';
import 'package:agrilink_vocpro/features/auth/provider/auth_provider.dart'; import 'package:agrilink_vocpro/features/auth/provider/auth_provider.dart';
import 'package:agrilink_vocpro/features/control/provider/control_provider.dart'; import 'package:agrilink_vocpro/features/control/provider/control_provider.dart';

View File

@ -456,6 +456,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.1" 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: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter

View File

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