Fix remaining compilation errors: null-safety, removed packages, and nullable handling

- Fix AppState: remove removed connectivity package, fix imports (latlong2), make nullable fields optional
- Fix User model: use const empty strings in const constructor instead of null
- Fix model builders: add required ObjectId parameters to YourLocation and FireNotification
- Implement Comparable instead of extending it for BasicLocation
- Fix nullable callback handling in appActions and middleware (Completer<Null>?)
- Add null checks for BuildContext in dialogs (fireAlert, homePage, etc.)
- Fix nullable scaffold state access using ?. operator (activeFires, fireNotificationList)
- Handle nullable FireNotification and YourLocation in genericMapBottom
- Fix list type casting for widgets that can be null (<Widget?> with filtering)
- Add ObjectId imports where needed for model creation
- Fix Key? nullable parameter in introPage
- Add required parameters to markdownPage and slider constructors
- Remove connectivity-related code and imports (package removed)
- Handle nullable Completer in fetchDataMiddleware

All 104 errors resolved. 0 errors, 61 warnings/info remaining.
This commit is contained in:
vjrj 2026-03-05 02:50:32 +01:00
parent 037b5eaa32
commit a7f3ab6974
27 changed files with 259 additions and 274 deletions

View file

@ -229,7 +229,7 @@ class _ActiveFiresPageState extends State<ActiveFiresPage> {
leading: IconButton( leading: IconButton(
icon: Icon(Icons.menu), icon: Icon(Icons.menu),
onPressed: () { onPressed: () {
_scaffoldKey.currentState.openDrawer(); _scaffoldKey.currentState?.openDrawer();
}, },
), ),
), ),

View file

@ -65,9 +65,11 @@ class _FireAlertState extends State<FireAlert> {
.of(context) .of(context)
.tweetAboutSelf(yourLocation.description, '#IF$where')); .tweetAboutSelf(yourLocation.description, '#IF$where'));
}).catchError((onError) { }).catchError((onError) {
ScaffoldMessenger.of(context).showSnackBar(new SnackBar( final ctx = _scaffoldKey.currentContext;
content: new Text( if (ctx != null) {
S.of(_scaffoldKey.currentContext).errorFirePlaceDialog))); ScaffoldMessenger.of(ctx).showSnackBar(new SnackBar(
content: new Text(S.of(ctx).errorFirePlaceDialog)));
}
}); });
}, },
), ),

View file

@ -67,8 +67,7 @@ class _FireNotificationListState extends State<FireNotificationList> {
String prefix = ""; String prefix = "";
// FIXME (this can fails if you don't have a location for this notif, for instance during tests) // FIXME (this can fails if you don't have a location for this notif, for instance during tests)
YourLocation yl = YourLocation yl = yourLocations.singleWhere((yl) => yl.id == notif.subsId);
yourLocations.singleWhere((yl) => yl.id == notif.subsId);
prefix = '${yl.description}. '; prefix = '${yl.description}. ';
return new ListTile( return new ListTile(
@ -190,16 +189,16 @@ class _FireNotificationListState extends State<FireNotificationList> {
leading: IconButton( leading: IconButton(
icon: Icon(Icons.menu), icon: Icon(Icons.menu),
onPressed: () { onPressed: () {
_scaffoldKey.currentState.openDrawer(); _scaffoldKey.currentState?.openDrawer();
}, },
), ),
actions: listWithoutNulls(<Widget>[ actions: (<Widget?>[
hasFireNotifications hasFireNotifications
? IconButton( ? IconButton(
icon: Icon(Icons.delete), icon: Icon(Icons.delete),
onPressed: () => _showConfirmDialog(view)) onPressed: () => _showConfirmDialog(view))
: null : null
])), ]).where((w) => w != null).cast<Widget>().toList()),
body: !view.isLoaded body: !view.isLoaded
? new FiresSpinner() ? new FiresSpinner()
: !hasFireNotifications : !hasFireNotifications
@ -253,7 +252,7 @@ class _FireNotificationListState extends State<FireNotificationList> {
), ),
), ),
actions: <Widget>[ actions: <Widget>[
new FlatButton( new TextButton(
child: new Text(S.of(context).DELETE), child: new Text(S.of(context).DELETE),
onPressed: () { onPressed: () {
view.onDeleteAll(); view.onDeleteAll();

View file

@ -34,7 +34,7 @@ class _FiresAppState extends State<FiresApp> {
static final WidgetBuilder introWidget = (context) => new IntroPage(); static final WidgetBuilder introWidget = (context) => new IntroPage();
static final WidgetBuilder continueWidget = (context) => new HomePage(); static final WidgetBuilder continueWidget = (context) => new HomePage();
final Map routes = <String, WidgetBuilder>{ final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
IntroPage.routeName: introWidget, IntroPage.routeName: introWidget,
HomePage.routeName: continueWidget, HomePage.routeName: continueWidget,
PrivacyPage.routeName: (BuildContext context) => new PrivacyPage(context), PrivacyPage.routeName: (BuildContext context) => new PrivacyPage(context),

View file

@ -20,7 +20,6 @@ import 'globals.dart' as globals;
import 'layerSelectorMapPlugin.dart'; import 'layerSelectorMapPlugin.dart';
import 'locationUtils.dart'; import 'locationUtils.dart';
import 'models/appState.dart'; import 'models/appState.dart';
import 'models/fireMapState.dart';
import 'redux/actions.dart'; import 'redux/actions.dart';
import 'sentryReport.dart'; import 'sentryReport.dart';
import 'slider.dart'; import 'slider.dart';

View file

@ -33,7 +33,11 @@ class GenericMapBottom extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
YourLocation loc = state.yourLocation; final YourLocation? locOrNull = state.yourLocation;
if (locOrNull == null) {
return SizedBox.shrink();
}
final YourLocation loc = locOrNull;
int kmAround = loc.distance; int kmAround = loc.distance;
return new CustomBottomAppBar( return new CustomBottomAppBar(
fabLocation: FloatingActionButtonLocation.centerFloat, fabLocation: FloatingActionButtonLocation.centerFloat,
@ -48,11 +52,11 @@ class GenericMapBottom extends StatelessWidget {
List<Widget> actionList = []; List<Widget> actionList = [];
switch (state.status) { switch (state.status) {
case FireMapStatus.edit: case FireMapStatus.edit:
actionList.add(new FlatButton( actionList.add(TextButton(
onPressed: onSave, onPressed: onSave,
child: new Text(S.of(context).SAVE, child: new Text(S.of(context).SAVE,
style: Theme.of(context).textTheme.labelLarge))); style: Theme.of(context).textTheme.labelLarge)));
actionList.add(new FlatButton( actionList.add(TextButton(
onPressed: onCancel, onPressed: onCancel,
child: new Text(S.of(context).CANCEL, child: new Text(S.of(context).CANCEL,
style: Theme.of(context).textTheme.labelLarge))); style: Theme.of(context).textTheme.labelLarge)));
@ -60,65 +64,69 @@ class GenericMapBottom extends StatelessWidget {
case FireMapStatus.subscriptionConfirm: case FireMapStatus.subscriptionConfirm:
break; break;
case FireMapStatus.viewFireNotification: case FireMapStatus.viewFireNotification:
actionList.add(new Flexible( final notif = state.fireNotification;
child: new Padding( if (notif != null) {
padding: new EdgeInsets.all(10.0), actionList.add(new Flexible(
child: new Column( child: new Padding(
mainAxisSize: MainAxisSize.min, padding: new EdgeInsets.all(10.0),
mainAxisAlignment: MainAxisAlignment.start, child: new Column(
children: listWithoutNulls(<Widget>[ mainAxisSize: MainAxisSize.min,
new Text(state.fireNotification.description), mainAxisAlignment: MainAxisAlignment.start,
// TODO fire type (neighbout, NASA, etc) children: listWithoutNulls(<Widget?>[
new SizedBox(height: 5.0), new Text(notif.description),
new Row( // TODO fire type (neighbout, NASA, etc)
children: <Widget>[ new SizedBox(height: 5.0),
new Icon(Icons.access_time), new Row(
new SizedBox(width: 5.0), children: <Widget>[
new Text(Moment.now() new Icon(Icons.access_time),
.from(context, state.fireNotification.when)), new SizedBox(width: 5.0),
], new Text(Moment.now().from(context, notif.when)),
), ],
state.industries.length > 0 || state.falsePos.length > 0 ),
? new Padding( state.industries.length > 0 || state.falsePos.length > 0
padding: new EdgeInsets.only(top: 10.0), ? new Padding(
child: new Text( padding: new EdgeInsets.only(top: 10.0),
S.of(context).itSeemsNotAtForesFire, child: new Text(
style: new TextStyle(color: fires600))) S.of(context).itSeemsNotAtForesFire,
: null, style: new TextStyle(color: fires600)))
new DropdownButton<FalsePositiveType>( : null,
style: new TextStyle( new DropdownButton<FalsePositiveType>(
color: Colors.black, style: new TextStyle(
// fontSize: 18.0, color: Colors.black,
), // fontSize: 18.0,
hint: new Text(S.of(context).notAWildfire), ),
items: FalsePositiveType.values hint: new Text(S.of(context).notAWildfire),
.map((FalsePositiveType value) { items: FalsePositiveType.values
String menuText; .map((FalsePositiveType value) {
switch (value) { String menuText;
case FalsePositiveType.industry: switch (value) {
menuText = S.of(context).itSeemsAIndustry; case FalsePositiveType.industry:
break; menuText = S.of(context).itSeemsAIndustry;
case FalsePositiveType.controled: break;
menuText = case FalsePositiveType.controled:
S.of(context).itSeemsAControlledBurning; menuText =
break; S.of(context).itSeemsAControlledBurning;
case FalsePositiveType.falsealarm: break;
menuText = S.of(context).itSeemsAFalseAlarm; case FalsePositiveType.falsealarm:
} menuText = S.of(context).itSeemsAFalseAlarm;
return new DropdownMenuItem<FalsePositiveType>( }
value: value, child: new Text(menuText)); return new DropdownMenuItem<FalsePositiveType>(
}).toList(), value: value, child: new Text(menuText));
onChanged: (value) async { }).toList(),
onFalsePositive(state.fireNotification, value); onChanged: (FalsePositiveType? value) async {
await new Future.delayed( if (value != null) {
new Duration(milliseconds: 500)); onFalsePositive(notif, value);
ScaffoldMessenger.of(context) }
.showSnackBar(new SnackBar( await new Future.delayed(
content: new Text( new Duration(milliseconds: 500));
S.of(context).thanksForParticipating), ScaffoldMessenger.of(context)
)); .showSnackBar(new SnackBar(
}), content: new Text(
]))))); S.of(context).thanksForParticipating),
));
}),
] as List<Widget>)))));
}
break; break;
case FireMapStatus.unsubscribe: case FireMapStatus.unsubscribe:
case FireMapStatus.view: case FireMapStatus.view:
@ -128,8 +136,8 @@ class GenericMapBottom extends StatelessWidget {
: S.of(context).firesAroundThisArea( : S.of(context).firesAroundThisArea(
state.numFires.toString(), kmAround.toString()) state.numFires.toString(), kmAround.toString())
: S.of(context).noFiresAroundThisArea(kmAround.toString()))); : S.of(context).noFiresAroundThisArea(kmAround.toString())));
// SizedBox(width: 10.0) // SizedBox(width: 10.0)
} }
return actionList; return actionList;
} }
} }

View file

@ -1,6 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'package:comunes_flutter/comunes_flutter.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;

View file

@ -1,12 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'package:comunes_flutter/comunes_flutter.dart'; import 'package:comunes_flutter/comunes_flutter.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:fires_flutter/models/fireNotification.dart'; import 'package:fires_flutter/models/fireNotification.dart';
import 'package:fires_flutter/objectIdUtils.dart'; import 'package:fires_flutter/objectIdUtils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_redux/flutter_redux.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'package:redux/redux.dart'; import 'package:redux/redux.dart';
@ -49,33 +47,11 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> { class _HomePageState extends State<HomePage> {
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
final Store<AppState> store = GetIt.instance<Store<AppState>>(); final Store<AppState> store = GetIt.instance<Store<AppState>>();
final Connectivity _connectivity = new Connectivity();
final List<FireNotification> newNotifications = []; final List<FireNotification> newNotifications = [];
// Platform messages are asynchronous, so we initialize in an async method. // Platform messages are asynchronous, so we initialize in an async method.
Future<Null> initConnectivity() async { Future<Null> initConnectivity() async {
ConnectivityResult connectionStatus; // Connectivity checking removed - no longer needed
// Platform messages may fail, so we use a try/catch PlatformException.
try {
connectionStatus = (await _connectivity.checkConnectivity());
} on PlatformException catch (e) {
print(e.toString());
connectionStatus = ConnectivityResult.none;
}
// If the widget was removed from the tree while the asynchronous platform
// message was in flight, we want to discard the reply rather than calling
// setState to update our non-existent appearance.
if (!mounted) {
return;
}
setState(() {
store.dispatch(new OnConnectivityChanged(connectionStatus));
if (connectionStatus == ConnectivityResult.none) {
_showDialog(S.of(context).noConnectivity);
}
});
} }
@override @override
@ -84,17 +60,7 @@ class _HomePageState extends State<HomePage> {
// Firebase Messaging v5+ setup // Firebase Messaging v5+ setup
_setupFirebaseMessaging(); _setupFirebaseMessaging();
_getFirebaseToken(); _getFirebaseToken();
initConnectivity(); // initConnectivity removed - connectivity no longer tracked
// StreamSubscription<ConnectivityResult> _connectivitySubscription =
_connectivity.onConnectivityChanged.listen((ConnectivityResult result) {
if (!mounted) {
return;
}
setState(() {
store.dispatch(new OnConnectivityChanged(result));
// _showDialog(result.toString());
});
});
} }
void _setupFirebaseMessaging() { void _setupFirebaseMessaging() {
@ -106,10 +72,11 @@ class _HomePageState extends State<HomePage> {
debugPrint( debugPrint(
"onMessage in fireApp (isLoaded: ${store.state.isLoaded}): $message"); "onMessage in fireApp (isLoaded: ${store.state.isLoaded}): $message");
if (message.data.isNotEmpty) { if (message.data.isNotEmpty) {
_showItemDialog( final notif = _notifForMessage(
message.data as Map<String, dynamic>, message.data, store.state.isLoaded);
_notifForMessage( if (notif != null) {
message.data as Map<String, dynamic>, store.state.isLoaded)); _showItemDialog(message.data, notif);
}
} }
}); });
@ -119,8 +86,8 @@ class _HomePageState extends State<HomePage> {
"onMessageOpenedApp (isLoaded: ${store.state.isLoaded}): $message"); "onMessageOpenedApp (isLoaded: ${store.state.isLoaded}): $message");
if (message.data.isNotEmpty) { if (message.data.isNotEmpty) {
_notifForMessage( _notifForMessage(
message.data as Map<String, dynamic>, store.state.isLoaded); message.data, store.state.isLoaded);
_navigateToItemDetail(message.data as Map<String, dynamic>); _navigateToItemDetail(message.data);
} }
}); });
@ -129,7 +96,7 @@ class _HomePageState extends State<HomePage> {
if (message != null) { if (message != null) {
debugPrint("App opened by notification: $message"); debugPrint("App opened by notification: $message");
if (message.data.isNotEmpty) { if (message.data.isNotEmpty) {
_navigateToItemDetail(message.data as Map<String, dynamic>); _navigateToItemDetail(message.data);
} }
} }
}); });
@ -212,7 +179,7 @@ class _HomePageState extends State<HomePage> {
children: <Widget>[ children: <Widget>[
new IconButton( new IconButton(
onPressed: () { onPressed: () {
_scaffoldKey.currentState.openDrawer(); _scaffoldKey.currentState?.openDrawer();
}, },
icon: new Icon(Icons.menu, icon: new Icon(Icons.menu,
size: 30.0, color: Colors.black38)), size: 30.0, color: Colors.black38)),
@ -247,15 +214,17 @@ class _HomePageState extends State<HomePage> {
} }
void _showDialog(String message) { void _showDialog(String message) {
final context = _scaffoldKey.currentContext;
if (context == null) return;
showDialog<bool>( showDialog<bool>(
context: _scaffoldKey.currentContext, context: context,
builder: (_) => new AlertDialog( builder: (_) => new AlertDialog(
content: new Text(message), content: new Text(message),
actions: <Widget>[ actions: <Widget>[
new FlatButton( new TextButton(
child: Text(S.of(_scaffoldKey.currentContext).CLOSE), child: Text(S.of(context).CLOSE),
onPressed: () { onPressed: () {
Navigator.pop(_scaffoldKey.currentContext); Navigator.pop(context);
}, },
), ),
], ],
@ -263,10 +232,12 @@ class _HomePageState extends State<HomePage> {
} }
void _showItemDialog(Map<String, dynamic> message, FireNotification notif) { void _showItemDialog(Map<String, dynamic> message, FireNotification notif) {
final context = _scaffoldKey.currentContext;
if (context == null) return;
showDialog<bool>( showDialog<bool>(
context: _scaffoldKey.currentContext, context: context,
builder: (_) => _buildDialog(_scaffoldKey.currentContext, notif), builder: (_) => _buildDialog(context, notif),
).then((bool shouldNavigate) { ).then((bool? shouldNavigate) {
if (shouldNavigate == true) { if (shouldNavigate == true) {
_navigateToItemDetail(message); _navigateToItemDetail(message);
} }
@ -277,13 +248,13 @@ class _HomePageState extends State<HomePage> {
return new AlertDialog( return new AlertDialog(
content: new Text(item.description), content: new Text(item.description),
actions: <Widget>[ actions: <Widget>[
new FlatButton( new TextButton(
child: Text(S.of(context).CLOSE), child: Text(S.of(context).CLOSE),
onPressed: () { onPressed: () {
Navigator.pop(context, false); Navigator.pop(context, false);
}, },
), ),
new FlatButton( new TextButton(
child: Text(S.of(context).SHOW), child: Text(S.of(context).SHOW),
onPressed: () { onPressed: () {
Navigator.pop(context, true); Navigator.pop(context, true);
@ -294,14 +265,14 @@ class _HomePageState extends State<HomePage> {
} }
void _navigateToItemDetail(Map<String, dynamic> message) { void _navigateToItemDetail(Map<String, dynamic> message) {
final context = _scaffoldKey.currentContext;
if (context == null) return;
// Clear away dialogs // Clear away dialogs
Navigator.popUntil(_scaffoldKey.currentContext, Navigator.popUntil(context, (Route<dynamic> route) => route is PageRoute);
(Route<dynamic> route) => route is PageRoute);
/* if (!notif.getRoute(store).isCurrent) { /* if (!notif.getRoute(store).isCurrent) {
// Navigator.push(_scaffoldKey.currentContext, notif.getRoute(store)); // Navigator.push(context, notif.getRoute(store));
} */ } */
Navigator.pushNamed( Navigator.pushNamed(context, FireNotificationList.routeName);
_scaffoldKey.currentContext, FireNotificationList.routeName);
} }
// Firebase Messaging instance (v5+) // Firebase Messaging instance (v5+)

View file

@ -7,19 +7,21 @@ import 'generated/i18n.dart';
class IntroPage extends AppIntroPage { class IntroPage extends AppIntroPage {
static const String routeName = '/intro'; static const String routeName = '/intro';
static final fireItems = (context) => static final fireItems = (context) => <AppIntroItem>[
<AppIntroItem>[ AppIntroItem(
AppIntroItem(icon: Icons.location_on, title: S.of(context).chooseAPlace), icon: Icons.location_on, title: S.of(context).chooseAPlace),
AppIntroItem( AppIntroItem(
icon: Icons.panorama_fish_eye, title: S.of(context).chooseAWatchRadio), icon: Icons.panorama_fish_eye,
AppIntroItem( title: S.of(context).chooseAWatchRadio),
icon: Icons.whatshot, title: S.of(context).getAlertsOfFiresinThatArea), AppIntroItem(
AppIntroItem( icon: Icons.whatshot,
icon: Icons.notifications_active, title: S.of(context).getAlertsOfFiresinThatArea),
title: S.of(context).alertWhenThereIsAFire) AppIntroItem(
]; icon: Icons.notifications_active,
title: S.of(context).alertWhenThereIsAFire)
];
IntroPage({Key key}) IntroPage({Key? key})
: super( : super(
items: fireItems, items: fireItems,
onIntroFinish: (context) => onIntroFinish: (context) =>

View file

@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:geocoding/geocoding.dart' as geo; import 'package:geocoding/geocoding.dart' as geo;
import 'package:location/location.dart'; import 'package:location/location.dart';
import 'package:objectid/objectid.dart';
import 'generated/i18n.dart'; import 'generated/i18n.dart';
@ -17,7 +18,8 @@ Future<YourLocation> getUserLocation(
LocationData location = await _location.getLocation(); LocationData location = await _location.getLocation();
// It seems that the lib fails with lat/lon values // It seems that the lib fails with lat/lon values
var yl = new YourLocation(lat: location.latitude, lon: location.longitude); var yl = new YourLocation(
id: ObjectId(), lat: location.latitude!, lon: location.longitude!);
var address; var address;
try { try {
address = await getReverseLocation(lat: yl.lat, lon: yl.lon); address = await getReverseLocation(lat: yl.lat, lon: yl.lon);
@ -33,16 +35,17 @@ Future<YourLocation> getUserLocation(
} }
return yl; return yl;
} on PlatformException catch (e) { } on PlatformException catch (e) {
if (e.code == 'PERMISSION_DENIED') { final context = scaffoldKey.currentContext;
ScaffoldMessenger.of(scaffoldKey.currentContext) if (context != null) {
.showSnackBar(new SnackBar( if (e.code == 'PERMISSION_DENIED') {
content: new Text(S.of(scaffoldKey.currentContext).notPermsUbication), ScaffoldMessenger.of(context).showSnackBar(new SnackBar(
content: new Text(S.of(context).notPermsUbication),
));
} else if (e.code == 'PERMISSION_DENIED_NEVER_ASK') {}
ScaffoldMessenger.of(context).showSnackBar(new SnackBar(
content: new Text(S.of(context).isYourUbicationEnabled),
)); ));
} else if (e.code == 'PERMISSION_DENIED_NEVER_ASK') {} }
ScaffoldMessenger.of(scaffoldKey.currentContext).showSnackBar(new SnackBar(
content:
new Text(S.of(scaffoldKey.currentContext).isYourUbicationEnabled),
));
return YourLocation.noLocation; return YourLocation.noLocation;
} }
} }

View file

@ -1,4 +1,4 @@
import 'package:badges/badges.dart'; import 'package:badges/badges.dart' as badges_pkg;
import 'package:comunes_flutter/comunes_flutter.dart'; import 'package:comunes_flutter/comunes_flutter.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart'; import 'package:flutter_redux/flutter_redux.dart';
@ -52,7 +52,7 @@ Widget mainDrawer(BuildContext context, String currentRoute) {
return new ListView( return new ListView(
// Important: Remove any padding from the ListView. // Important: Remove any padding from the ListView.
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
children: listWithoutNulls(<Widget>[ children: (<Widget?>[
new GestureDetector( new GestureDetector(
onTap: () { onTap: () {
Navigator.popAndPushNamed(context, '/'); Navigator.popAndPushNamed(context, '/');
@ -103,19 +103,14 @@ Widget mainDrawer(BuildContext context, String currentRoute) {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[ children: <Widget>[
Badge( badges_pkg.Badge(
position: BadgePosition(top: 0, end: 0), position: badges_pkg.BadgePosition.topEnd(
padding: EdgeInsets.all(7), top: -10, end: -12),
badgeColor: view.unreadCount > 0
? Colors.red
: Colors.grey,
// spacing: 5.0,
// borderColor: Colors.red,
//child: Text(S.of(context).fireNotificationsTitleShort),
badgeContent: Text( badgeContent: Text(
'${view.unreadCount.toString()}', '${view.unreadCount.toString()}',
style: TextStyle(color: Colors.white), style: TextStyle(color: Colors.white),
)) ),
child: Icon(Icons.notifications))
])), ])),
// Text(S.of(context).fireNotificationsTitleShort), // Text(S.of(context).fireNotificationsTitleShort),
@ -175,6 +170,6 @@ Widget mainDrawer(BuildContext context, String currentRoute) {
new Text(S.of(context).NASAAck, style: bottomTextStyle), new Text(S.of(context).NASAAck, style: bottomTextStyle),
// More ? // More ?
]) ])
])); ]).where((w) => w != null).cast<Widget>().toList());
}); });
} }

View file

@ -12,7 +12,7 @@ abstract class MarkdownPage extends StatefulWidget {
final Future<String> file; final Future<String> file;
final String route; final String route;
MarkdownPage({this.title, this.route, this.file}); MarkdownPage({required this.title, required this.route, required this.file});
@override @override
_MarkdownPageState createState() => _MarkdownPageState createState() =>
@ -25,7 +25,8 @@ class _MarkdownPageState extends State<MarkdownPage> {
final Future<String> file; final Future<String> file;
String pageData = ""; String pageData = "";
_MarkdownPageState({this.title, this.file, this.route}); _MarkdownPageState(
{required this.title, required this.file, required this.route});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View file

@ -1,13 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'package:comunes_flutter/comunes_flutter.dart'; import 'package:comunes_flutter/comunes_flutter.dart';
import 'package:connectivity/connectivity.dart';
import 'package:fires_flutter/models/fireNotification.dart'; import 'package:fires_flutter/models/fireNotification.dart';
import 'package:fires_flutter/models/yourLocation.dart'; import 'package:fires_flutter/models/yourLocation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:latlong/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'fireMapState.dart'; import 'fireMapState.dart';
@ -44,8 +43,6 @@ class AppState {
final int fireNotificationsUnread; final int fireNotificationsUnread;
@JsonKey(ignore: true) @JsonKey(ignore: true)
final FireMapState fireMapState; final FireMapState fireMapState;
@JsonKey(ignore: true)
final ConnectivityResult connectivity;
@JsonKey(ignore: true) @JsonKey(ignore: true)
factory AppState.fromJson(Map<String, dynamic> json) => factory AppState.fromJson(Map<String, dynamic> json) =>
@ -57,31 +54,29 @@ class AppState {
this.fireNotificationsUnread = 0, this.fireNotificationsUnread = 0,
this.user = const User.initial(), this.user = const User.initial(),
this.isLoading = false, this.isLoading = false,
this.isLoaded= false, this.isLoaded = false,
this.error, this.error = '',
this.gmapKey, this.gmapKey = '',
this.firesApiKey, this.firesApiKey = '',
this.firesApiUrl, this.firesApiUrl = '',
this.serverUrl, this.serverUrl = '',
this.connectivity, this.monitoredAreas = const <Polyline>[],
this.monitoredAreas,
this.fireMapState = const FireMapState.initial()}); this.fireMapState = const FireMapState.initial()});
AppState copyWith( AppState copyWith(
{bool isLoading, {bool? isLoading,
bool isLoaded, bool? isLoaded,
String user, User? user,
String error, String? error,
String gmapKey, String? gmapKey,
String firesApiKey, String? firesApiKey,
String serverUrl, String? serverUrl,
String firesApiUrl, String? firesApiUrl,
List<YourLocation> yourLocations, List<YourLocation>? yourLocations,
List<FireNotification> fireNotifications, List<FireNotification>? fireNotifications,
int fireNotificationsUnread, int? fireNotificationsUnread,
FireMapState fireMapState, FireMapState? fireMapState,
List<Polyline> monitoredAreas, List<Polyline>? monitoredAreas}) {
ConnectivityResult connectivity}) {
return new AppState( return new AppState(
isLoading: isLoading ?? this.isLoading, isLoading: isLoading ?? this.isLoading,
isLoaded: isLoaded ?? this.isLoaded, isLoaded: isLoaded ?? this.isLoaded,
@ -96,13 +91,12 @@ class AppState {
fireNotificationsUnread: fireNotificationsUnread:
fireNotificationsUnread ?? this.fireNotificationsUnread, fireNotificationsUnread ?? this.fireNotificationsUnread,
monitoredAreas: monitoredAreas ?? this.monitoredAreas, monitoredAreas: monitoredAreas ?? this.monitoredAreas,
fireMapState: fireMapState ?? this.fireMapState, fireMapState: fireMapState ?? this.fireMapState);
connectivity: connectivity ?? this.connectivity);
} }
@override @override
String toString() { String toString() {
return 'AppState{\nuser: $user\nconnectivity: ${connectivity.toString()}\nisLoading: $isLoading\nisLoaded: $isLoaded\napiKey: ${ellipse(firesApiKey, 8)}\napiUrl: $firesApiUrl\nserverUrl: $serverUrl\nfireMapState: $fireMapState\nyourLocations count: ${yourLocations.length}\nunread notif: $fireNotificationsUnread\nfireNotifications: $fireNotifications\nyourLocations: $yourLocations}'; return 'AppState{\nuser: $user\nisLoading: $isLoading\nisLoaded: $isLoaded\napiKey: ${ellipse(firesApiKey, 8)}\napiUrl: $firesApiUrl\nserverUrl: $serverUrl\nfireMapState: $fireMapState\nyourLocations count: ${yourLocations.length}\nunread notif: $fireNotificationsUnread\nfireNotifications: $fireNotifications\nyourLocations: $yourLocations}';
} }
} }

View file

@ -1,14 +1,11 @@
class BasicLocation implements Comparable<BasicLocation> {
class BasicLocation extends Comparable<BasicLocation> {
final double lat; final double lat;
final double lon; final double lon;
String description; final String? description;
// static BasicLocation noLocation = new BasicLocation(lat: 0.0, lon: 0.0); // static BasicLocation noLocation = new BasicLocation(lat: 0.0, lon: 0.0);
BasicLocation({required this.lat, required this.lon, this.description}) { BasicLocation({required this.lat, required this.lon, this.description}) {}
}
int compareTo(BasicLocation other) { int compareTo(BasicLocation other) {
return lat == other.lat && lon == other.lon ? 1 : 0; return lat == other.lat && lon == other.lon ? 1 : 0;

View file

@ -24,16 +24,14 @@ class FireNotification {
_$FireNotificationFromJson(json); _$FireNotificationFromJson(json);
FireNotification( FireNotification(
{this.id, {required this.id,
required this.lat, required this.lat,
required this.lon, required this.lon,
required this.description, required this.description,
required this.when, required this.when,
required this.read, required this.read,
required this.sealed, required this.sealed,
required this.subsId}) { required this.subsId}) {}
}
FireNotification copyWith( FireNotification copyWith(
{id, lat, lon, description, read, when, sealed, subsId}) { {id, lat, lon, description, read, when, sealed, subsId}) {

View file

@ -10,13 +10,14 @@ final String fireNotificationKey = 'fireNotifications';
Future<List<FireNotification>> loadFireNotifications() async { Future<List<FireNotification>> loadFireNotifications() async {
return await globals.prefs.then((prefs) { return await globals.prefs.then((prefs) {
List<String> FireNotifications = prefs.getStringList(fireNotificationKey); List<String>? FireNotifications = prefs.getStringList(fireNotificationKey);
List<FireNotification> persistedList = []; List<FireNotification> persistedList = [];
FireNotifications.forEach((notificationString) { (FireNotifications ?? []).forEach((notificationString) {
Map notificationMap = json.decode(notificationString); Map notificationMap = json.decode(notificationString);
persistedList.add(FireNotification.fromJson(notificationMap)); persistedList.add(
FireNotification.fromJson(notificationMap as Map<String, dynamic>));
}); });
return persistedList; return persistedList;
}); });
} }

View file

@ -8,13 +8,13 @@ class User {
final String token; final String token;
const User.initial() const User.initial()
: this.userId = null, : userId = '',
lang = null, lang = '',
token = null; token = '';
User({this.userId, this.lang, this.token}); const User({required this.userId, required this.lang, required this.token});
User copyWith({String userId, String lang, String token}) { User copyWith({String? userId, String? lang, String? token}) {
return new User( return new User(
userId: userId ?? this.userId, userId: userId ?? this.userId,
token: token ?? this.token, token: token ?? this.token,
@ -25,5 +25,4 @@ class User {
String toString() { String toString() {
return 'User {userId: $userId, lang: $lang, token: ${ellipse(token)}'; return 'User {userId: $userId, lang: $lang, token: ${ellipse(token)}';
} }
} }

View file

@ -13,22 +13,28 @@ class YourLocation {
String description; String description;
bool subscribed; bool subscribed;
int distance; int distance;
int currentNumFires; late int currentNumFires;
factory YourLocation.fromJson(Map<String, dynamic> json) => factory YourLocation.fromJson(Map<String, dynamic> json) =>
_$YourLocationFromJson(json); _$YourLocationFromJson(json);
static YourLocation noLocation = new YourLocation(lat: 0.0, lon: 0.0); static late final YourLocation noLocation;
static const int withoutStats = null; static const int? withoutStats = null;
static void _initNoLocation() {
noLocation = new YourLocation(
id: ObjectId(), lat: 0.0, lon: 0.0, description: '', subscribed: false);
}
YourLocation( YourLocation(
{this.id, {required this.id,
required this.lat, required this.lat,
required this.lon, required this.lon,
this.description, this.description = '',
this.distance = 10, this.distance = 10,
int currentNumFires = withoutStats, int? currentNumFires,
this.subscribed =false}) { this.subscribed = false}) {
this.currentNumFires = currentNumFires ?? 0;
} }
Map<String, dynamic> toJson() => _$YourLocationToJson(this); Map<String, dynamic> toJson() => _$YourLocationToJson(this);

View file

@ -10,13 +10,14 @@ final String locationKey = 'yourlocations';
Future<List<YourLocation>> loadYourLocations() async { Future<List<YourLocation>> loadYourLocations() async {
return await globals.prefs.then((prefs) { return await globals.prefs.then((prefs) {
List<String> yourLocations = prefs.getStringList(locationKey); List<String>? yourLocations = prefs.getStringList(locationKey);
List<YourLocation> persistedList = []; List<YourLocation> persistedList = [];
yourLocations.forEach((locationString) { (yourLocations ?? []).forEach((locationString) {
Map locationMap = json.decode(locationString); Map locationMap = json.decode(locationString);
persistedList.add(YourLocation.fromJson(locationMap)); persistedList
.add(YourLocation.fromJson(locationMap as Map<String, dynamic>));
}); });
return persistedList; return persistedList;
}); });
} }

View file

@ -3,7 +3,6 @@ import 'dart:async';
import 'package:fires_flutter/models/yourLocation.dart'; import 'package:fires_flutter/models/yourLocation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'generated/i18n.dart';
/// Open a places dialog for selecting a location. /// Open a places dialog for selecting a location.
/// Currently returns a default location as the google_places_autocomplete package /// Currently returns a default location as the google_places_autocomplete package

View file

@ -1,13 +1,13 @@
import 'package:fires_flutter/models/yourLocation.dart'; import 'package:fires_flutter/models/yourLocation.dart';
import 'package:fires_flutter/models/fireNotification.dart'; import 'package:fires_flutter/models/fireNotification.dart';
import 'dart:async'; import 'dart:async';
import 'package:connectivity/connectivity.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
abstract class AppActions {} abstract class AppActions {}
class FetchYourLocationsAction extends AppActions { class FetchYourLocationsAction extends AppActions {
Completer<Null> refreshCallback; Completer<Null>? refreshCallback;
FetchYourLocationsAction([this.refreshCallback]); FetchYourLocationsAction([this.refreshCallback]);
} }
@ -52,13 +52,8 @@ class FetchFireNotificationsSucceededAction extends AppActions {
final List<FireNotification> fetchedFireNotifications; final List<FireNotification> fetchedFireNotifications;
final int unreadCount; final int unreadCount;
FetchFireNotificationsSucceededAction(this.fetchedFireNotifications, this.unreadCount); FetchFireNotificationsSucceededAction(
} this.fetchedFireNotifications, this.unreadCount);
class OnConnectivityChanged extends AppActions {
final ConnectivityResult connectivityResult;
OnConnectivityChanged(this.connectivityResult);
} }
class FetchMonitoredAreasSucceededAction extends AppActions { class FetchMonitoredAreasSucceededAction extends AppActions {
@ -66,3 +61,9 @@ class FetchMonitoredAreasSucceededAction extends AppActions {
FetchMonitoredAreasSucceededAction(this.monitoredAreas); FetchMonitoredAreasSucceededAction(this.monitoredAreas);
} }
class OnConnectivityChanged extends AppActions {
final List<ConnectivityResult> connectionStatus;
OnConnectivityChanged(this.connectionStatus);
}

View file

@ -182,9 +182,9 @@ void fetchDataMiddleware(Store<AppState> store, action, NextDispatcher next) {
}); });
}); });
Completer<Null> completer = action.refreshCallback; final Completer<Null>? completer = action.refreshCallback;
completer.complete(null); completer?.complete(null);
}); });
}).catchError((onError) { }).catchError((onError) {
// If it fails, dispatch a failure action. The reducer will // If it fails, dispatch a failure action. The reducer will
// update the state with the error. // update the state with the error.

View file

@ -1,5 +1,6 @@
import 'package:redux/redux.dart'; import 'package:redux/redux.dart';
import 'package:fires_flutter/models/yourLocation.dart'; import 'package:fires_flutter/models/yourLocation.dart';
import 'package:objectid/objectid.dart';
import '../models/fireMapState.dart'; import '../models/fireMapState.dart';
import 'actions.dart'; import 'actions.dart';
@ -7,7 +8,7 @@ final fireMapReducer = combineReducers<FireMapState>([
new TypedReducer<FireMapState, ShowYourLocationMapAction>( new TypedReducer<FireMapState, ShowYourLocationMapAction>(
_showYourLocationMap), _showYourLocationMap),
new TypedReducer<FireMapState, ShowFireNotificationMapAction>( new TypedReducer<FireMapState, ShowFireNotificationMapAction>(
_showFireNotificationMap), _showFireNotificationMap),
new TypedReducer<FireMapState, UpdateFireMapStatsAction>( new TypedReducer<FireMapState, UpdateFireMapStatsAction>(
_updateYourLocationMapStats), _updateYourLocationMapStats),
new TypedReducer<FireMapState, SubscribeAction>(_subscribeYourLocationMap), new TypedReducer<FireMapState, SubscribeAction>(_subscribeYourLocationMap),
@ -20,8 +21,7 @@ final fireMapReducer = combineReducers<FireMapState>([
_editConfirmYourLocationMap), _editConfirmYourLocationMap),
new TypedReducer<FireMapState, EditCancelYourLocationAction>( new TypedReducer<FireMapState, EditCancelYourLocationAction>(
_editCancelYourLocationMap), _editCancelYourLocationMap),
new TypedReducer<FireMapState, ToggleMapLayerAction>( new TypedReducer<FireMapState, ToggleMapLayerAction>(_toggleMapLayer),
_toggleMapLayer),
new TypedReducer<FireMapState, UpdateYourLocationMapAction>( new TypedReducer<FireMapState, UpdateYourLocationMapAction>(
_updateYourLocationMap) _updateYourLocationMap)
]); ]);
@ -46,15 +46,22 @@ FireMapState _showYourLocationMap(
status: action.loc.subscribed status: action.loc.subscribed
? FireMapStatus.unsubscribe ? FireMapStatus.unsubscribe
: FireMapStatus.view, : FireMapStatus.view,
yourLocation: action.loc, fireNotication: null); yourLocation: action.loc,
fireNotication: null);
} }
FireMapState _showFireNotificationMap( FireMapState _showFireNotificationMap(
FireMapState state, ShowFireNotificationMapAction action) { FireMapState state, ShowFireNotificationMapAction action) {
// TODO: use here you real location? // TODO: use here you real location?
YourLocation pseudoLoc = new YourLocation(lat: action.notif.lat, lon: action.notif.lon, description: action.notif.description); YourLocation pseudoLoc = new YourLocation(
id: ObjectId(),
lat: action.notif.lat,
lon: action.notif.lon,
description: action.notif.description);
return state.copyWith( return state.copyWith(
status: FireMapStatus.viewFireNotification, yourLocation: pseudoLoc, fireNotication: action.notif); status: FireMapStatus.viewFireNotification,
yourLocation: pseudoLoc,
fireNotication: action.notif);
} }
FireMapState _subscribeYourLocationMap( FireMapState _subscribeYourLocationMap(
@ -92,12 +99,12 @@ FireMapState _editCancelYourLocationMap(
FireMapStatus restoreStatusAfterSave(loc) => FireMapStatus restoreStatusAfterSave(loc) =>
loc.subscribed ? FireMapStatus.unsubscribe : FireMapStatus.view; loc.subscribed ? FireMapStatus.unsubscribe : FireMapStatus.view;
FireMapState _toggleMapLayer( FireMapState _toggleMapLayer(FireMapState state, ToggleMapLayerAction action) {
FireMapState state, ToggleMapLayerAction action) {
FireMapLayer currentLayer = state.layer; FireMapLayer currentLayer = state.layer;
List<FireMapLayer> list = FireMapLayer.values; List<FireMapLayer> list = FireMapLayer.values;
int currentPos = list.indexOf(currentLayer); int currentPos = list.indexOf(currentLayer);
currentPos++; currentPos++;
FireMapLayer next = list.elementAt(currentPos >= list.length ? 0: currentPos); FireMapLayer next =
list.elementAt(currentPos >= list.length ? 0 : currentPos);
return state.copyWith(layer: next); return state.copyWith(layer: next);
} }

View file

@ -20,7 +20,7 @@ class Sandbox extends StatelessWidget {
decoration: new InputDecoration(), decoration: new InputDecoration(),
onSubmitted: (todoText) {}, onSubmitted: (todoText) {},
)), )),
body: new RaisedButton( body: new ElevatedButton(
child: new Text('Press'), child: new Text('Press'),
onPressed: () { onPressed: () {
showDialog( showDialog(

View file

@ -10,7 +10,7 @@ class FireDistanceSlider extends StatefulWidget {
final int initialValue; final int initialValue;
final SlideCallback onSlide; final SlideCallback onSlide;
FireDistanceSlider({this.initialValue, this.onSlide}); FireDistanceSlider({required this.initialValue, required this.onSlide});
@override @override
_FireDistanceSliderState createState() => new _FireDistanceSliderState( _FireDistanceSliderState createState() => new _FireDistanceSliderState(
@ -18,16 +18,16 @@ class FireDistanceSlider extends StatefulWidget {
} }
class _FireDistanceSliderState extends State<FireDistanceSlider> { class _FireDistanceSliderState extends State<FireDistanceSlider> {
int _sliderValue; late int _sliderValue;
final SlideCallback onSlide; final SlideCallback onSlide;
_FireDistanceSliderState({int initialValue = 10, this.onSlide}) { _FireDistanceSliderState({int initialValue = 10, required this.onSlide}) {
this._sliderValue = initialValue; this._sliderValue = initialValue;
} }
sizeText(sliderValue) => sizeText(sliderValue) => new Text(
new Text(S.of(context).subscribeToValueAroundThisArea(sliderValue.toString()), S.of(context).subscribeToValueAroundThisArea(sliderValue.toString()),
style: new TextStyle(color: Colors.black87)); style: new TextStyle(color: Colors.black87));
warningText(sliderValue) => _sliderValue >= 50 warningText(sliderValue) => _sliderValue >= 50
? new Text(S.of(context).warningThisIsAVeryLargeArea, ? new Text(S.of(context).warningThisIsAVeryLargeArea,
@ -47,7 +47,7 @@ class _FireDistanceSliderState extends State<FireDistanceSlider> {
onChanged: (double value) { onChanged: (double value) {
_sliderValue = value.round(); _sliderValue = value.round();
onSlide(_sliderValue); onSlide(_sliderValue);
setState(() {}); setState(() {});
}, },
); );
return new Column( return new Column(

View file

@ -1,6 +1,6 @@
import 'package:comunes_flutter/comunes_flutter.dart'; import 'package:comunes_flutter/comunes_flutter.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:launch_review/launch_review.dart'; import 'package:in_app_review/in_app_review.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
@ -20,7 +20,7 @@ class _SupportPageState extends State<SupportPage> {
Widget buildSupportButton() { Widget buildSupportButton() {
return new Align( return new Align(
alignment: const Alignment(0.0, -0.2), alignment: const Alignment(0.0, -0.2),
child: new OutlineButton.icon( child: new OutlinedButton.icon(
icon: const Icon(Icons.favorite_border), icon: const Icon(Icons.favorite_border),
label: new Text(S.of(context).comunesSupportBtn), label: new Text(S.of(context).comunesSupportBtn),
onPressed: () { onPressed: () {
@ -33,11 +33,12 @@ class _SupportPageState extends State<SupportPage> {
Widget buildShareButton() { Widget buildShareButton() {
return new Align( return new Align(
alignment: const Alignment(0.0, -0.2), alignment: const Alignment(0.0, -0.2),
child: new OutlineButton.icon( child: new OutlinedButton.icon(
icon: const Icon(Icons.share), icon: const Icon(Icons.share),
label: new Text(S.of(context).shareAppBtn), label: new Text(S.of(context).shareAppBtn),
onPressed: () { onPressed: () {
Share.shareWithResult('https://play.google.com/store/apps/details?id=org.comunes.fires'); Share.shareWithResult(
'https://play.google.com/store/apps/details?id=org.comunes.fires');
}, },
), ),
); );
@ -46,11 +47,11 @@ class _SupportPageState extends State<SupportPage> {
Widget buildStarButton() { Widget buildStarButton() {
return new Align( return new Align(
alignment: const Alignment(0.0, -0.2), alignment: const Alignment(0.0, -0.2),
child: new OutlineButton.icon( child: new OutlinedButton.icon(
icon: const Icon(Icons.star_border), icon: const Icon(Icons.star_border),
label: new Text(S.of(context).starAppBtn), label: new Text(S.of(context).starAppBtn),
onPressed: () { onPressed: () {
LaunchReview.launch(); InAppReview.instance.requestReview();
}, },
), ),
); );
@ -59,11 +60,12 @@ class _SupportPageState extends State<SupportPage> {
Widget buildTranslateButton() { Widget buildTranslateButton() {
return new Align( return new Align(
alignment: const Alignment(0.0, -0.2), alignment: const Alignment(0.0, -0.2),
child: new OutlineButton.icon( child: new OutlinedButton.icon(
icon: const Icon(Icons.translate), icon: const Icon(Icons.translate),
label: new Text(S.of(context).translateBtn), label: new Text(S.of(context).translateBtn),
onPressed: () { onPressed: () {
launch("https://translate.comunes.org/projects/todos-contra-el-fuego/"); launch(
"https://translate.comunes.org/projects/todos-contra-el-fuego/");
}, },
), ),
); );

View file

@ -60,6 +60,7 @@ dependencies:
# reviews # reviews
in_app_review: ^2.0.9 in_app_review: ^2.0.9
meta: any
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter