From 50c63f1d33d9b38b1a87988692ab79ce60cd791b Mon Sep 17 00:00:00 2001 From: Anurag Roy Date: Wed, 22 Jul 2020 20:22:11 +0530 Subject: [PATCH] Build version 1.1.0 release - New charting implementation - Add license page - Add some documentation - Some visual fixes and tweaks --- android/build.gradle | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 4 +- lib/data/all_data.dart | 4 - .../analysis_current_value_provider.dart | 38 ++ .../analysis_data_provider.dart | 29 ++ lib/main.dart | 16 +- lib/pages/Analysis.dart | 231 ++++++------ lib/pages/Credits.dart | 61 ++-- lib/pages/Licenses.dart | 131 +++++++ lib/pages/Overview.dart | 2 +- lib/ui/analysis_graph.dart | 344 +++++++++++++----- lib/ui/options.dart | 17 +- lib/utils/custom_datetimespec.dart | 21 ++ lib/utils/custom_renderer.dart | 34 ++ pubspec.lock | 160 ++++++-- pubspec.yaml | 17 +- 16 files changed, 808 insertions(+), 303 deletions(-) create mode 100644 lib/data_providers/analysis_current_value_provider.dart create mode 100644 lib/data_providers/analysis_data_provider.dart create mode 100644 lib/pages/Licenses.dart create mode 100644 lib/utils/custom_datetimespec.dart create mode 100644 lib/utils/custom_renderer.dart diff --git a/android/build.gradle b/android/build.gradle index 83f114c..205da3d 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.6.1' + classpath 'com.android.tools.build:gradle:4.0.1' } } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index dbec2d1..1863ec4 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Apr 06 13:07:01 IST 2020 +#Tue Jul 14 15:44:44 IST 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip diff --git a/lib/data/all_data.dart b/lib/data/all_data.dart index 9026718..9792fe8 100644 --- a/lib/data/all_data.dart +++ b/lib/data/all_data.dart @@ -34,25 +34,21 @@ final List> devDetails = [ { 'Name': 'ANURAG ROY', 'Github': 'RoyARG02', - 'Bio': 'Programming language nomad, and a Flutter developer.', 'Twitter': '_royarg' }, { 'Name': 'AYUSH THAKUR', 'Github': 'ayulockin', - 'Bio': 'Deep Learning for Computer Vision | Computer Vision for Robotics', 'Twitter': 'ayushthakur0' }, { 'Name': 'SNEHANGSHU BHATTACHARYA', 'Github': 'forkbomb-666', - 'Bio': 'Linux | Bigdata(Hadoop) | DIY Electronics | Robotics', 'Twitter': 'snehangshu_' }, { 'Name': 'ARITRA ROY GOSTHIPATY', 'Github': 'ariG23498', - 'Bio': '| Flutter | Android | Algorithms | Digital Signal Processing |', 'Twitter': 'ariG23498' } ]; diff --git a/lib/data_providers/analysis_current_value_provider.dart b/lib/data_providers/analysis_current_value_provider.dart new file mode 100644 index 0000000..22858e5 --- /dev/null +++ b/lib/data_providers/analysis_current_value_provider.dart @@ -0,0 +1,38 @@ +import 'package:flutter/foundation.dart'; + +import 'package:soil_moisture_app/ui/analysis_graph.dart'; +import 'package:soil_moisture_app/utils/date_func.dart'; + +/// Provides the current value selected in the [AnalysisGraph]. +class AnalysisCurrentValueProvider with ChangeNotifier { + DataPoint current; + + AnalysisCurrentValueProvider(); + + /// Initailizes the provider, with a list of data values. + /// + /// Calculates the current value taking the last value of the + /// list. + AnalysisCurrentValueProvider.init(List data) + : current = DataPoint( + DateTime(date.year, date.month, date.day, data.length - 1), + data.last, + ); + + /// Updates the current value. + /// + /// Used in conjunction with [DataProvider] as [ProxyProvider]. + void update(List data) { + current = DataPoint( + DateTime(date.year, date.month, date.day, data.length - 1), + data.last, + ); + notifyListeners(); + } + + /// Changes the currently selected value. + changeValue(DataPoint current) { + this.current = current; + notifyListeners(); + } +} diff --git a/lib/data_providers/analysis_data_provider.dart b/lib/data_providers/analysis_data_provider.dart new file mode 100644 index 0000000..22558df --- /dev/null +++ b/lib/data_providers/analysis_data_provider.dart @@ -0,0 +1,29 @@ +import 'package:flutter/foundation.dart'; + +/// Contains the current values alongwith its unit to be displayed +/// in [AnalysisGraph]. +class AnalysisDataProvider with ChangeNotifier { + List data; + String unit; + + AnalysisDataProvider(); + + /// Initializes the provider given some data and its unit. + void init(List newData, String unit) { + data = newData; + this.unit = unit; + } + + /// Same as [AnalysisDataProvider.init] but notifies dependant widgets. + void changeData(List newData, String unit) { + data = newData; + this.unit = unit; + notifyListeners(); + } + + /// "Updates", which isn't saying much considering it only notifies dependant + /// widgets. Used in conjunction with [SelectedCardState] as a [ProxyProvider]. + void update() { + notifyListeners(); + } +} diff --git a/lib/main.dart b/lib/main.dart index b1479e8..9ed9b54 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,6 +3,9 @@ import 'package:flutter/material.dart'; // * External Packages import import 'package:provider/provider.dart'; +// * Data providers import +import 'package:soil_moisture_app/data_providers/analysis_data_provider.dart'; + // * Prefs import import 'package:soil_moisture_app/prefs/user_prefs.dart'; @@ -60,14 +63,19 @@ class Root extends StatelessWidget { class Home extends StatefulWidget { final List _tabPages = [ Overview(), - Analysis(), + ChangeNotifierProxyProvider( + create: (context) => AnalysisDataProvider(), + update: (context, selCard, dataProvider) => dataProvider..update(), + child: Analysis(), + ), ]; @override @override _HomeState createState() => _HomeState(); } -class _HomeState extends State with SingleTickerProviderStateMixin { +class _HomeState extends State + with SingleTickerProviderStateMixin, AutomaticKeepAliveClientMixin { TabController _controller; void initState() { super.initState(); @@ -97,6 +105,7 @@ class _HomeState extends State with SingleTickerProviderStateMixin { @override Widget build(BuildContext context) { + super.build(context); print(appWidth(context)); return WillPopScope( onWillPop: _popScopeInvoke, @@ -153,4 +162,7 @@ class _HomeState extends State with SingleTickerProviderStateMixin { ), ); } + + @override + bool get wantKeepAlive => true; } diff --git a/lib/pages/Analysis.dart b/lib/pages/Analysis.dart index ca4b155..5bb5bb1 100644 --- a/lib/pages/Analysis.dart +++ b/lib/pages/Analysis.dart @@ -8,15 +8,31 @@ * of required data at the REST API server. */ +/// (RoyARG02): This is legacy code made to be functionally relevant due to some issues. +/// See: https://github.com/RoyARG02/soil_moisture_app/issues/23 +/// +/// Never in my life I would: +/// - put the entire view in the single widget. +/// - skimp on documentation. +/// - be foolish to unnecessarily use setState() so much that I would have to +/// use [mounted]. +/// +/// I would NOT recommend to work on this code. Time will be better spent in any newer version +/// of this app. For instance, see https://github.com/RoyARG02/soil_moisture_app/tree/master. +/// However, if you would like to annoy me, be my guest. + import 'package:flutter/material.dart'; // * External packages import import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; +// * Data providers import +import 'package:soil_moisture_app/data_providers/analysis_current_value_provider.dart'; +import 'package:soil_moisture_app/data_providers/analysis_data_provider.dart'; + // * State import import 'package:soil_moisture_app/states/selected_card_state.dart'; -import 'package:soil_moisture_app/states/theme_state.dart'; // * ui import import 'package:soil_moisture_app/ui/analysis_graph.dart'; @@ -42,6 +58,7 @@ class _AnalysisState extends State { String _measure; dynamic _chartObj; + @override void initState() { _measure = 'Moisture'; super.initState(); @@ -82,11 +99,20 @@ class _AnalysisState extends State { initialDate: date, firstDate: oldestDate, lastDate: now, + helpText: 'GO TO DATE', builder: (BuildContext context, Widget child) { return Theme( data: Theme.of(context).copyWith( - buttonBarTheme: ButtonBarTheme.of(context) - .copyWith(buttonTextTheme: ButtonTextTheme.normal), + buttonTheme: Theme.of(context).buttonTheme.copyWith( + colorScheme: Theme.of(context) + .buttonTheme + .colorScheme + .copyWith( + primary: Theme.of(context).brightness == Brightness.dark + ? Colors.white + : Colors.black, + ), + ), ), child: child, ); @@ -105,6 +131,8 @@ class _AnalysisState extends State { setState(() { _initChart(_measure); }); + Provider.of(context) + .changeData(_chartObj.getAllValues, _chartObj.getUnit); } if (isNow()) { latData = fetchLatestData(); @@ -131,132 +159,64 @@ class _AnalysisState extends State { children: [ Container( margin: EdgeInsets.symmetric(vertical: appWidth(context) * 0.03), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - FutureBuilder( - future: totData, - builder: (context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - _initChart(this._measure); - return (isDataGot) - ? Container( - height: appWidth(context) * 0.215, - alignment: Alignment.center, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Text( - (_chartObj.getLastValue * - ((_measure == 'Moisture') - ? 100 - : 1)) - .toStringAsFixed(1), - style: Theme.of(context) - .textTheme - .headline3 - .copyWith( - color: (Provider.of( - context) - .isDarkTheme) - ? Theme.of(context) - .accentColor - : appSecondaryDarkColor, - fontSize: - appWidth(context) * 0.09, - ), - ), - Text( - '${_chartObj.getUnit}', - style: Theme.of(context) - .textTheme - .bodyText2 - .copyWith( - fontSize: - appWidth(context) * 0.06, - ), - ), - ], - ), - Text( - 'On $fetchDateEEEMMMd', - style: Theme.of(context) - .textTheme - .bodyText1 - .copyWith( - fontSize: appWidth(context) * 0.025, - ), - ), - ], - ), - ) - : SizedBox( - height: appWidth(context) * 0.215, - ); - } else { - return SizedBox( - height: appWidth(context) * 0.215, - ); - } - }, - ), - Container( - padding: EdgeInsets.only(right: appWidth(context) * 0.02), - height: appWidth(context) * 0.1, - child: Theme( - data: Theme.of(context).copyWith( - canvasColor: Theme.of(context).primaryColor, - ), - child: DropdownButtonHideUnderline( - child: DropdownButton( - icon: Icon(FontAwesomeIcons.chevronDown), - iconSize: appWidth(context) * 0.03, - value: _measure, - onChanged: (String measure) { - setState(() { - _initChart(measure); - }); - }, - items: [ - 'Moisture', - 'Light', - 'Humidity', - 'Temperature' - ].map>((String option) { - return DropdownMenuItem( - value: option, - child: Container( - alignment: Alignment.center, - width: appWidth(context) * 0.31, - child: Text( - option, - style: Theme.of(context) - .textTheme - .bodyText1 - .copyWith( - fontSize: appWidth(context) * 0.035), - textAlign: TextAlign.center, - ), + child: Center( + child: Container( + padding: EdgeInsets.only(right: appWidth(context) * 0.02), + height: appWidth(context) * 0.1, + child: Theme( + data: Theme.of(context).copyWith( + canvasColor: Theme.of(context).primaryColor, + ), + child: DropdownButtonHideUnderline( + child: DropdownButton( + icon: FaIcon(FontAwesomeIcons.chevronDown), + iconSize: appWidth(context) * 0.03, + value: _measure, + onChanged: (String measure) { + setState(() { + _initChart(measure); + }); + if (isDataGot) + Provider.of(context) + .changeData( + _chartObj.getAllValues, _chartObj.getUnit); + }, + items: [ + 'Moisture', + 'Light', + 'Humidity', + 'Temperature' + ].map>((String option) { + return DropdownMenuItem( + value: option, + child: Container( + alignment: Alignment.center, + width: appWidth(context) * 0.31, + child: Text( + option, + style: Theme.of(context) + .textTheme + .bodyText1 + .copyWith( + fontSize: appWidth(context) * 0.035), + textAlign: TextAlign.center, ), - ); - }).toList(), - ), + ), + ); + }).toList(), ), ), - decoration: BoxDecoration( - border: Border.all( - width: 2.0, - color: appPrimaryDarkColor, - ), - borderRadius: - BorderRadius.circular(appWidth(context) * 0.1), - shape: BoxShape.rectangle, + ), + decoration: BoxDecoration( + border: Border.all( + width: 2.0, + color: appPrimaryDarkColor, ), + borderRadius: + BorderRadius.circular(appWidth(context) * 0.1), + shape: BoxShape.rectangle, ), - ], + ), ), ), Container( @@ -271,9 +231,26 @@ class _AnalysisState extends State { return NoNowData(isScrollable: false); } else if (snapshot.connectionState == ConnectionState.done) { - return (isDataGot) - ? displayChart(_chartObj, _measure, context) - : NoData(); + if (isDataGot) { + this._initChart(_measure); + Provider.of(context) + .init(_chartObj.getAllValues, _chartObj.getUnit); + return ChangeNotifierProxyProvider< + AnalysisDataProvider, + AnalysisCurrentValueProvider>( + create: (_) => AnalysisCurrentValueProvider.init( + Provider.of(context) + .data), + update: (context, data, currentData) => + currentData..update(data.data), + child: AnalysisGraph( + animate: true, + graph: _measure, + ), + ); + } else { + return NoData(); + } } else { return Center( child: CircularProgressIndicator(), diff --git a/lib/pages/Credits.dart b/lib/pages/Credits.dart index 90964ed..4ea8080 100644 --- a/lib/pages/Credits.dart +++ b/lib/pages/Credits.dart @@ -3,7 +3,7 @@ * A page to acknowledge the developers who created the project. * Also provides links to browse the repository of the app and the -* API server. +* API server, along with the licenses. */ import 'package:flutter/material.dart'; @@ -27,6 +27,9 @@ import 'package:soil_moisture_app/utils/sizes.dart'; // * data import import 'package:soil_moisture_app/data/all_data.dart'; +// * pages import +import 'Licenses.dart'; + class Credits extends StatefulWidget { @override _CreditsState createState() => _CreditsState(); @@ -44,39 +47,27 @@ class _CreditsState extends State { IndexedWidgetBuilder devInfoBuilder = (context, index) { return ListTile( + dense: true, contentPadding: EdgeInsets.symmetric( - horizontal: appWidth(context) * 0.03, - vertical: appWidth(context) * 0.01), + horizontal: appWidth(context) * 0.03, + ), leading: CircleAvatar( backgroundColor: Theme.of(context).accentColor, backgroundImage: NetworkImageWithRetry( 'https://avatars.githubusercontent.com/${devDetails[index]['Github']}', ), ), - title: RichText( - text: TextSpan( - children: [ - TextSpan( - text: devDetails[index]['Name'], - style: Theme.of(context).textTheme.subtitle1), - TextSpan( - text: ' ${devDetails[index]['Github']}', - style: Theme.of(context).textTheme.bodyText1.copyWith( - color: (Provider.of(context).isDarkTheme) - ? subtleWhiteTextColor - : subtleBlackTextColor, - ), - ) - ], - ), + title: Text( + devDetails[index]['Name'], + style: Theme.of(context).textTheme.subtitle1, ), subtitle: Text( - devDetails[index]['Bio'], - style: Theme.of(context).textTheme.caption.copyWith( - fontSize: appWidth(context) * 0.03, + devDetails[index]['Github'], + style: Theme.of(context).textTheme.bodyText1.copyWith( + color: (Provider.of(context).isDarkTheme) + ? subtleWhiteTextColor + : subtleBlackTextColor, ), - maxLines: 2, - overflow: TextOverflow.ellipsis, ), trailing: Row( mainAxisSize: MainAxisSize.min, @@ -85,7 +76,7 @@ class _CreditsState extends State { tooltip: 'Open GitHub profile', onPressed: () => _launchUrl('http://github.com/${devDetails[index]['Github']}'), - icon: Icon( + icon: FaIcon( FontAwesomeIcons.github, color: (Provider.of(context).isDarkTheme) ? githubWhite @@ -96,7 +87,7 @@ class _CreditsState extends State { tooltip: 'Open Twitter profile', onPressed: () => _launchUrl( 'http://twitter.com/${devDetails[index]['Twitter']}'), - icon: Icon( + icon: FaIcon( FontAwesomeIcons.twitter, color: twitterBlue, ), @@ -176,7 +167,8 @@ class _CreditsState extends State { ), Card( child: ListTile( - leading: Icon(FontAwesomeIcons.codeBranch), + dense: true, + leading: FaIcon(FontAwesomeIcons.codeBranch), title: Text( 'Fork the project on GitHub', style: Theme.of(context).textTheme.subtitle1, @@ -192,7 +184,8 @@ class _CreditsState extends State { ), Card( child: ListTile( - leading: Icon(FontAwesomeIcons.server), + dense: true, + leading: FaIcon(FontAwesomeIcons.server), title: Text( 'View API implementation', style: Theme.of(context).textTheme.subtitle1, @@ -206,6 +199,18 @@ class _CreditsState extends State { onTap: () => _launchUrl(apiUrl), ), ), + Container( + margin: const EdgeInsets.only(top: 16.0), + alignment: Alignment.center, + child: FlatButton( + onPressed: () => Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => Licenses(), + ), + ), + child: Text('LICENSES'), + ), + ) ], ), ), diff --git a/lib/pages/Licenses.dart b/lib/pages/Licenses.dart new file mode 100644 index 0000000..234700f --- /dev/null +++ b/lib/pages/Licenses.dart @@ -0,0 +1,131 @@ +import 'dart:developer' show Timeline, Flow; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart' hide Flow; +import 'package:flutter/scheduler.dart'; + +import 'package:soil_moisture_app/utils/sizes.dart'; + +class Licenses extends StatefulWidget { + @override + _LicensesState createState() => _LicensesState(); +} + +class _LicensesState extends State { + @override + void initState() { + super.initState(); + _initLicenses(); + } + + final List _licenses = []; + bool _loaded = false; + + Future _initLicenses() async { + int debugFlowId = -1; + assert(() { + final Flow flow = Flow.begin(); + Timeline.timeSync('_initLicenses()', () {}, flow: flow); + debugFlowId = flow.id; + return true; + }()); + await for (final LicenseEntry license in LicenseRegistry.licenses) { + if (!mounted) { + return; + } + assert(() { + Timeline.timeSync('_initLicenses()', () {}, + flow: Flow.step(debugFlowId)); + return true; + }()); + final List paragraphs = + await SchedulerBinding.instance.scheduleTask>( + license.paragraphs.toList, + Priority.animation, + debugLabel: 'License', + ); + if (!mounted) { + return; + } + setState(() { + _licenses.add(const Padding( + padding: EdgeInsets.symmetric(vertical: 18.0), + child: Text( + '🍀‬', // That's U+1F340. Could also use U+2766 (❦) if U+1F340 doesn't work everywhere. + textAlign: TextAlign.center, + ), + )); + _licenses.add(Container( + decoration: const BoxDecoration( + border: Border(bottom: BorderSide(width: 0.0))), + child: Text( + license.packages.join(', '), + style: const TextStyle(fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + )); + for (final LicenseParagraph paragraph in paragraphs) { + if (paragraph.indent == LicenseParagraph.centeredIndent) { + _licenses.add(Padding( + padding: const EdgeInsets.only(top: 16.0), + child: Text( + paragraph.text, + style: const TextStyle(fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + )); + } else { + assert(paragraph.indent >= 0); + _licenses.add(Padding( + padding: EdgeInsetsDirectional.only( + top: 8.0, start: 16.0 * paragraph.indent), + child: Text(paragraph.text), + )); + } + } + }); + } + setState(() { + _loaded = true; + }); + assert(() { + Timeline.timeSync('Build scheduled', () {}, flow: Flow.end(debugFlowId)); + return true; + }()); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: BackButton(), + title: Text( + 'Licenses', + style: Theme.of(context).textTheme.headline6.copyWith( + fontSize: appWidth(context) * 0.055, + ), + ), + centerTitle: true, + ), + body: SafeArea( + child: Scrollbar( + child: ListView( + physics: AlwaysScrollableScrollPhysics(parent: BouncingScrollPhysics()), + padding: + const EdgeInsets.symmetric(horizontal: 8.0, vertical: 12.0), + children: [ + ..._licenses, + if (!_loaded) + const Padding( + padding: EdgeInsets.symmetric(vertical: 24.0), + child: Center( + child: CircularProgressIndicator(), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/Overview.dart b/lib/pages/Overview.dart index fe6aacc..d22aab2 100644 --- a/lib/pages/Overview.dart +++ b/lib/pages/Overview.dart @@ -252,7 +252,7 @@ class AvatarData extends StatelessWidget { padding: EdgeInsets.all(appWidth(context) * 0.01), child: CircleAvatar( backgroundColor: this._bkgrndColor, - child: Icon( + child: FaIcon( this._icon, size: appWidth(context) * 0.05, color: Theme.of(context).scaffoldBackgroundColor, diff --git a/lib/ui/analysis_graph.dart b/lib/ui/analysis_graph.dart index 784e37a..90fdd43 100644 --- a/lib/ui/analysis_graph.dart +++ b/lib/ui/analysis_graph.dart @@ -5,98 +5,280 @@ * This graph has zoom, pan and tooltip features. */ +import 'dart:math' as math; // * For max() and min() import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'dart:math'; // * For max() and min() // * External packages import -import 'package:syncfusion_flutter_charts/charts.dart'; +import 'package:charts_flutter/flutter.dart' as charts; +import 'package:charts_common/common.dart' as common; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +// * data providers import +import 'package:soil_moisture_app/data_providers/analysis_current_value_provider.dart'; +import 'package:soil_moisture_app/data_providers/analysis_data_provider.dart'; + +// * State import +import 'package:soil_moisture_app/states/theme_state.dart'; + +// * ui import +import 'package:soil_moisture_app/ui/colors.dart'; // * utils Import -import 'package:soil_moisture_app/utils/sizes.dart'; +import 'package:soil_moisture_app/utils/custom_datetimespec.dart'; +import 'package:soil_moisture_app/utils/custom_renderer.dart'; import 'package:soil_moisture_app/utils/date_func.dart'; +import 'package:soil_moisture_app/utils/sizes.dart'; -// * This function defines the behaviour and formatting of the chart -SfCartesianChart displayChart( - dynamic chartObj, String graph, BuildContext context) { - num dataMinValue = chartObj.getAllValues.reduce((num a, num b) => min(a, b)); - num dataMaxValue = chartObj.getAllValues.reduce((num a, num b) => max(a, b)); - return SfCartesianChart( - zoomPanBehavior: ZoomPanBehavior( - enablePinching: true, - enablePanning: true, - zoomMode: ZoomMode.x, - ), - plotAreaBorderWidth: 0, - primaryXAxis: DateTimeAxis( - minimum: DateTime(date.year, date.month, date.day, 0), - maximum: DateTime(date.year, date.month, date.day, 23), - axisLine: AxisLine(width: 1), - interval: 3, - dateFormat: DateFormat.jm(), - majorGridLines: MajorGridLines(width: 1), - labelStyle: ChartTextStyle( - fontFamily: 'Ocrb', - fontSize: appWidth(context) * 0.027, - ), - ), - primaryYAxis: NumericAxis( - minimum: (dataMinValue < 0) ? dataMinValue - 100.0 : 0, - maximum: (dataMaxValue > 100) ? dataMaxValue + 100.0 : 100, - interval: 20, - axisLine: AxisLine(width: 1), - labelFormat: '{value}${chartObj.getUnit}', - isVisible: true, - labelStyle: ChartTextStyle( - fontSize: appWidth(context) * 0.027, - fontFamily: 'Ocrb', - ), - ), - series: getLineSeries(chartObj.getAllValues, graph, context), - tooltipBehavior: TooltipBehavior( - enable: true, - animationDuration: 200, - canShowMarker: false, - header: '', - ), - ); +/// The behaviour and formatting of the chart shown in the Analysis page. +class AnalysisGraph extends StatelessWidget { + AnalysisGraph({ + this.animate, + this.graph, + }); + + final bool animate; + final String graph; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + CurrentPointerValue( + unit: Provider.of(context).unit, + graph: graph, + ), + Flexible( + child: Container( + margin: const EdgeInsets.only(left: 8.0, bottom: 8.0), + child: charts.TimeSeriesChart( + _createSeries(Provider.of(context).data, + graph, context), + behaviors: [ + charts.LinePointHighlighter( + showVerticalFollowLine: + charts.LinePointHighlighterFollowLineType.nearest, + showHorizontalFollowLine: + charts.LinePointHighlighterFollowLineType.nearest, + dashPattern: [4, 3], + drawFollowLinesAcrossChart: false, + symbolRenderer: CustomCircleSymbolRenderer( + fillColor: materialColorToCommonChartsColor(Colors.white), + strokeColor: materialColorToCommonChartsColor( + Theme.of(context).accentColor, + ), + strokeWidth: 3.0, + ), + ), + charts.PanAndZoomBehavior(), + ], + selectionModels: [ + charts.SelectionModelConfig( + changedListener: (charts.SelectionModel model) { + if (model.hasDatumSelection) + Provider.of(context, + listen: false) + .changeValue(model.selectedDatum[0].datum); + }, + ) + ], + animate: animate ?? false, + customSeriesRenderers: [ + charts.LineRendererConfig( + includePoints: true, + customRendererId: 'point', + ) + ], + primaryMeasureAxis: charts.NumericAxisSpec( + tickFormatterSpec: _analysisGraphMeasureTickFormatter, + tickProviderSpec: charts.StaticNumericTickProviderSpec( + _createStaticTicks( + Provider.of(context).data, graph), + ), + renderSpec: charts.GridlineRendererSpec( + labelStyle: charts.TextStyleSpec( + color: materialColorToCommonChartsColor( + Theme.of(context).brightness == Brightness.dark + ? Colors.white + : Colors.black, + ), + fontFamily: 'Ocrb', + ), + ), + ), + domainAxis: CustomDateTimeAxisSpec( + tickFormatterSpec: + common.BasicDateTimeTickFormatterSpec.fromDateFormat( + DateFormat('h:mma'), + ), + renderSpec: charts.SmallTickRendererSpec( + labelStyle: charts.TextStyleSpec( + color: charts.MaterialPalette.gray.shadeDefault, + fontFamily: 'Ocrb', + ), + lineStyle: charts.LineStyleSpec( + color: charts.MaterialPalette.gray.shadeDefault)), + ), + ), + ), + ), + ], + ); + } } -// * This function returns the series(List) to be displayed in the graph -// * The graph displays mapped x and y values from Class defined below -List> getLineSeries( - List chartData, String graph, BuildContext context) { - // Debug Print - print(chartData); - List<_ChartData> data = []; - for (var i = 0; i < chartData.length; ++i) { - data.add( - _ChartData(chartData[i], DateTime(date.year, date.month, date.day, i))); +/// The tick formatter used in the measure axis(y-axis) of the [AnalysisGraph]. +/// +/// Converts values greater than 1000 in the form of 'K' and values greater than +/// 1000000 in the form of 'M'. +/// +/// 237.5 => 237.5 +/// 1013.4 => 1.0K +/// 91142069.1 => 91.1M +final _analysisGraphMeasureTickFormatter = charts.BasicNumericTickFormatterSpec( + (num value) { + if (value >= 1000) { + return '${(value / 1000).toStringAsFixed(1)}K'; + } else if (value >= 1000000) { + return '${(value / 1000000).toStringAsFixed(1)}M'; + } else { + return value.toString(); + } + }, +); + +/// The widget showing the currently selected value in the [Analysis] graph. +/// Rapidly changes value with animation from zero to the selected value. +/// The text will change from this value to the next value selected. +class CurrentPointerValue extends StatefulWidget { + final String unit; + final String graph; + CurrentPointerValue({this.unit, this.graph}); + @override + _CurrentPointerValueState createState() => _CurrentPointerValueState(); +} + +class _CurrentPointerValueState extends State { + num _begin; + num _end; + + @override + void initState() { + super.initState(); + _begin = 0.0; } - return >[ - LineSeries<_ChartData, DateTime>( - enableTooltip: true, - animationDuration: 150, - dataSource: data, - xValueMapper: (point, _) => point.index, - yValueMapper: (point, _) { - return (graph == 'Moisture') ? point.value * 100 : point.value; - }, - pointColorMapper: (x, _) => Theme.of(context).accentColor, - width: 2, - markerSettings: MarkerSettings( - isVisible: true, - color: Theme.of(context).cardColor, - height: appWidth(context) * 0.015, - width: appWidth(context) * 0.015, - ), - ), + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _end = Provider.of(context).current.value; + } + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.baseline, + textBaseline: TextBaseline.alphabetic, + children: [ + TweenAnimationBuilder( + duration: Duration(milliseconds: 500), + tween: Tween(begin: _begin, end: _end), + builder: (context, value, _) => RichText( + text: TextSpan(children: [ + TextSpan( + text: + '${(value * ((widget.graph == "Moisture") ? 100 : 1)).toStringAsFixed(1)}', + style: Theme.of(context).textTheme.headline3.copyWith( + color: (Provider.of(context).isDarkTheme) + ? Theme.of(context).accentColor + : appSecondaryDarkColor, + fontSize: appWidth(context) * 0.09, + ), + ), + TextSpan( + text: widget.unit, + style: Theme.of(context).textTheme.bodyText2.copyWith( + fontSize: appWidth(context) * 0.06, + ), + ), + ]), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + 'at ${DateFormat('ha').format(Provider.of(context).current.time)}', + style: Theme.of(context).textTheme.bodyText1.copyWith( + fontSize: appWidth(context) * 0.025, + ), + ), + ) + ], + ); + } +} + +List> _createSeries( + List data, String graph, BuildContext context) { + final chartData = Iterable.generate( + data.length, + (i) => DataPoint(DateTime(date.year, date.month, date.day, i), data[i]), + ).toList(); + return >[ + charts.Series( + id: 'Data', + colorFn: (_, __) => + materialColorToCommonChartsColor(Theme.of(context).accentColor), + domainFn: (DataPoint point, _) => point.time, + measureFn: (DataPoint point, _) => + (graph == 'Moisture') ? point.value * 100 : point.value, + data: chartData, + )..setAttribute(charts.rendererIdKey, 'point'), ]; } -// * Class for mapping x and y values for the Graph -class _ChartData { - dynamic value; - DateTime index; - _ChartData(this.value, this.index); +/// Converts a [Color] to the type of [charts.Color]. +charts.Color materialColorToCommonChartsColor(Color materialColor) { + return charts.Color( + r: materialColor.red, + b: materialColor.blue, + g: materialColor.green, + a: materialColor.alpha, + ); +} + +/// Creates static ticks for the chart. +/// +/// The chart bt default changes its ticks whenever zoomed or panned to always +/// fit the visible data values throughout the vertical axis. Explicitly +/// providing the ticks prevents the chart to adjust the vertical zoom by itself. +/// +/// In this case, we disable the vertical zoom. +List> _createStaticTicks(List data, String graph) { + num dataMinValue = data.reduce((num a, num b) => math.min(a, b)); + num dataMaxValue = data.reduce((num a, num b) => math.max(a, b)); + double minYAxisValue = (dataMinValue < 0.0) ? dataMinValue - 50.0 : 0.0; + double maxYAxisValue = (dataMaxValue > 100.0) ? dataMaxValue + 50.0 : 100.0; + final double interval = + (((maxYAxisValue - minYAxisValue) / 100) * 20).round().toDouble(); + List> ticks = []; + print('$minYAxisValue $maxYAxisValue'); + while (minYAxisValue - maxYAxisValue < interval) { + ticks.add(charts.TickSpec(minYAxisValue.toInt())); + minYAxisValue += interval; + } + return ticks; +} + +/// Class representing each of the data point of the graph. +class DataPoint { + final DateTime time; + final dynamic value; + + DataPoint(this.time, this.value); + + @override + String toString() => 'DataPoint: ${this.time},${this.value}'; } diff --git a/lib/ui/options.dart b/lib/ui/options.dart index dcf312c..4c33e72 100644 --- a/lib/ui/options.dart +++ b/lib/ui/options.dart @@ -29,19 +29,16 @@ class Options extends StatelessWidget { builder: (context) => Column( mainAxisSize: MainAxisSize.min, children: [ - ListTile( - leading: (_themeState.isDarkTheme) - ? Icon(FontAwesomeIcons.solidLightbulb) - : Icon(FontAwesomeIcons.lightbulb), + SwitchListTile( + secondary: (_themeState.isDarkTheme) + ? FaIcon(FontAwesomeIcons.solidLightbulb) + : FaIcon(FontAwesomeIcons.lightbulb), title: Text('Dark Theme'), - trailing: Switch( - value: _themeState.isDarkTheme, - onChanged: (_) => _themeState.toggleTheme(), - ), - onTap: () => _themeState.toggleTheme(), + value: _themeState.isDarkTheme, + onChanged: (_) => _themeState.toggleTheme(), ), ListTile( - leading: Icon(FontAwesomeIcons.slidersH), + leading: FaIcon(FontAwesomeIcons.slidersH), title: Text('Pump Threshold Control'), onTap: () { Navigator.pop(context); diff --git a/lib/utils/custom_datetimespec.dart b/lib/utils/custom_datetimespec.dart new file mode 100644 index 0000000..9b3d6c9 --- /dev/null +++ b/lib/utils/custom_datetimespec.dart @@ -0,0 +1,21 @@ +import 'package:charts_flutter/flutter.dart' as charts; + +class CustomDateTimeAxisSpec extends charts.DateTimeAxisSpec { + const CustomDateTimeAxisSpec({ + charts.RenderSpec renderSpec, + charts.DateTimeTickProviderSpec tickProviderSpec, + charts.DateTimeTickFormatterSpec tickFormatterSpec, + bool showAxisLine, + }) : super( + renderSpec: renderSpec, + tickProviderSpec: tickProviderSpec, + tickFormatterSpec: tickFormatterSpec, + showAxisLine: showAxisLine); + + @override + configure(charts.Axis axis, charts.ChartContext context, + charts.GraphicsFactory graphicsFactory) { + super.configure(axis, context, graphicsFactory); + axis.autoViewport = false; + } +} diff --git a/lib/utils/custom_renderer.dart b/lib/utils/custom_renderer.dart new file mode 100644 index 0000000..7a91d1a --- /dev/null +++ b/lib/utils/custom_renderer.dart @@ -0,0 +1,34 @@ +import 'dart:math' show Rectangle; + +import 'package:charts_flutter/flutter.dart'; + +class CustomCircleSymbolRenderer extends CircleSymbolRenderer { + final Color fillColor; + final Color strokeColor; + double strokeWidth; + CustomCircleSymbolRenderer({ + this.fillColor, + this.strokeColor, + this.strokeWidth, + }); + @override + void paint( + ChartCanvas canvas, + Rectangle bounds, { + List dashPattern, + Color fillColor, + FillPatternType fillPattern, + Color strokeColor, + double strokeWidthPx, + }) { + super.paint( + canvas, + bounds, + dashPattern: dashPattern, + fillColor: this.fillColor, + fillPattern: fillPattern, + strokeColor: this.strokeColor, + strokeWidthPx: this.strokeWidth, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index b050ef8..4356809 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -7,42 +7,56 @@ packages: name: archive url: "https://pub.dartlang.org" source: hosted - version: "2.0.11" + version: "2.0.13" args: dependency: transitive description: name: args url: "https://pub.dartlang.org" source: hosted - version: "1.5.2" + version: "1.6.0" async: dependency: transitive description: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.4.0" + version: "2.4.1" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "2.0.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.1.2" + version: "1.1.3" + charts_common: + dependency: transitive + description: + name: charts_common + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.0" + charts_flutter: + dependency: "direct main" + description: + name: charts_flutter + url: "https://pub.dartlang.org" + source: hosted + version: "0.9.0" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.14.11" + version: "1.14.12" convert: dependency: transitive description: @@ -56,7 +70,7 @@ packages: name: crypto url: "https://pub.dartlang.org" source: hosted - version: "2.1.3" + version: "2.1.4" cupertino_icons: dependency: "direct main" description: @@ -64,6 +78,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.3" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "5.2.1" flutter: dependency: "direct main" description: flutter @@ -92,35 +113,42 @@ packages: name: font_awesome_flutter url: "https://pub.dartlang.org" source: hosted - version: "8.5.0" + version: "8.8.1" http: dependency: "direct main" description: name: http url: "https://pub.dartlang.org" source: hosted - version: "0.12.0+2" + version: "0.12.2" http_parser: dependency: transitive description: name: http_parser url: "https://pub.dartlang.org" source: hosted - version: "3.1.3" + version: "3.1.4" image: dependency: transitive description: name: image url: "https://pub.dartlang.org" source: hosted - version: "2.1.4" + version: "2.1.12" intl: - dependency: transitive + dependency: "direct main" description: name: intl url: "https://pub.dartlang.org" source: hosted - version: "0.15.8" + version: "0.16.1" + logging: + dependency: transitive + description: + name: logging + url: "https://pub.dartlang.org" + source: hosted + version: "0.11.4" matcher: dependency: transitive description: @@ -141,7 +169,7 @@ packages: name: package_info url: "https://pub.dartlang.org" source: hosted - version: "0.4.0+13" + version: "0.4.1" path: dependency: transitive description: @@ -149,20 +177,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.6.4" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1+2" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" pedantic: dependency: transitive description: name: pedantic url: "https://pub.dartlang.org" source: hosted - version: "1.8.0+1" + version: "1.9.0" percent_indicator: dependency: "direct main" description: name: percent_indicator url: "https://pub.dartlang.org" source: hosted - version: "2.1.1+1" + version: "2.1.5" petitparser: dependency: transitive description: @@ -170,13 +212,34 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.4.0" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1" + platform_detect: + dependency: transitive + description: + name: platform_detect + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.2" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.13" provider: dependency: "direct main" description: @@ -184,41 +247,55 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.2.0" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.4" quiver: dependency: transitive description: name: quiver url: "https://pub.dartlang.org" source: hosted - version: "2.0.5" + version: "2.1.3" shared_preferences: dependency: "direct main" description: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "0.5.6" + version: "0.5.8" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.2+1" shared_preferences_macos: dependency: transitive description: name: shared_preferences_macos url: "https://pub.dartlang.org" source: hosted - version: "0.0.1+3" + version: "0.0.1+10" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.0.4" shared_preferences_web: dependency: transitive description: name: shared_preferences_web url: "https://pub.dartlang.org" source: hosted - version: "0.1.2+2" + version: "0.1.2+7" sky_engine: dependency: transitive description: flutter @@ -230,7 +307,7 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.5.5" + version: "1.7.0" stack_trace: dependency: transitive description: @@ -252,13 +329,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.5" - syncfusion_flutter_charts: - dependency: "direct main" - description: - name: syncfusion_flutter_charts - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.0-beta.4" term_glyph: dependency: transitive description: @@ -272,7 +342,7 @@ packages: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.11" + version: "0.2.15" typed_data: dependency: transitive description: @@ -286,28 +356,35 @@ packages: name: url_launcher url: "https://pub.dartlang.org" source: hosted - version: "5.4.1" + version: "5.5.0" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.1+1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos url: "https://pub.dartlang.org" source: hosted - version: "0.0.1+2" + version: "0.0.1+7" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "1.0.7" url_launcher_web: dependency: transitive description: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "0.1.0+2" + version: "0.1.2" vector_math: dependency: transitive description: @@ -315,13 +392,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.8" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.0" xml: dependency: transitive description: name: xml url: "https://pub.dartlang.org" source: hosted - version: "3.5.0" + version: "3.6.1" sdks: - dart: ">=2.4.0 <3.0.0" - flutter: ">=1.12.13+hotfix.4 <2.0.0" + dart: ">=2.6.0 <3.0.0" + flutter: ">=1.12.13+hotfix.5 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index aa83dc4..61df497 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: soil_moisture_app description: A new Flutter project. -version: 1.0.3+10003 +version: 1.1.0+10100 environment: sdk: ">=2.2.2 <3.0.0" @@ -10,14 +10,14 @@ dependencies: flutter: sdk: flutter percent_indicator: ^2.1.1+1 - syncfusion_flutter_charts: ^1.0.0-beta.4 font_awesome_flutter: ^8.5.0 url_launcher: ^5.4.1 flutter_image: ^3.0.0 provider: ^3.1.0+1 shared_preferences: ^0.5.6 package_info: ^0.4.0+13 - + charts_flutter: ^0.9.0 + intl: ^0.16.1 cupertino_icons: ^0.1.3 http: @@ -26,16 +26,15 @@ dev_dependencies: sdk: flutter flutter: - uses-material-design: true assets: - - assets/images/Soif_sk.png - - assets/images/Soif_sk_dark.png - - assets/images/plant.png - - assets/images/plant_dark.png + - assets/images/Soif_sk.png + - assets/images/Soif_sk_dark.png + - assets/images/plant.png + - assets/images/plant_dark.png fonts: - family: Ocrb fonts: - - asset: assets/fonts/Ocrb.ttf \ No newline at end of file + - asset: assets/fonts/Ocrb.ttf