todos-contra-el-fuego-mobile/lib/fireNotificationList.dart
vjrj 7812ed951e fix: Resolve Dart type inference errors and update Android build configuration for Gradle 8.14
Type Annotation Fixes:
- fireNotificationList.dart: Added DeleteFireNotificationFunction and TapFireNotificationFunction types to _buildRow and _buildSavedFireNotifications parameters
- homePage.dart: Added Object type to catchError handler parameters
- redux/appReducer.dart: Added dynamic type annotation to action parameter
- redux/fetchDataMiddleware.dart: Added explicit type annotations and improved type casting
- redux/reducers.dart: Added dynamic type annotation to action parameter
- redux/userReducer.dart: Added dynamic type annotation to action parameter

Android Build Configuration Updates:
- Updated compileSdkVersion and targetSdkVersion from 34 to 36 to match plugin requirements
- Moved plugins {} block to first position (Gradle 8.14 requirement)
- Added dev.flutter.flutter-gradle-plugin to plugins block for modern Flutter integration
- Removed deprecated useProguard setting, replaced with shrinkResources
- Disabled Jetifier (android.enableJetifier=false) to improve build performance
- Increased JVM heap allocation to 4096M for large project compilation
- Added Kotlin language version 1.8 override for plugin compatibility
- Added ProGuard rules to exclude optional Google Play Core classes

Build Results:
-  APK builds successfully (146MB debug APK)
-  Zero type inference errors detected by dart analyze
-  Kotlin compilation passes
-  R8 minification configured correctly
-  301 remaining issues are all INFO-level style suggestions (non-blocking)
2026-03-06 21:44:54 +01:00

273 lines
10 KiB
Dart

import 'dart:async';
import 'package:comunes_flutter/comunes_flutter.dart';
import 'package:flutter/material.dart';
import 'package:flutter_redux/flutter_redux.dart';
import 'package:redux/redux.dart';
import 'customMoment.dart';
import 'firesSpinner.dart';
import 'generated/i18n.dart';
import 'genericMap.dart';
import 'mainDrawer.dart';
import 'models/appState.dart';
import 'models/fireNotification.dart';
import 'models/yourLocation.dart';
import 'redux/actions.dart';
@immutable
class _ViewModel {
const _ViewModel(
{required this.isLoaded,
required this.onTap,
required this.onDelete,
required this.onDeleteAll,
required this.fireNotifications,
required this.yourLocations,
required this.fireNotificationsUnread});
final bool isLoaded;
final List<FireNotification> fireNotifications;
final int fireNotificationsUnread;
final List<YourLocation> yourLocations;
final TapFireNotificationFunction onTap;
final DeleteFireNotificationFunction onDelete;
final DeleteAllFireNotificationFunction onDeleteAll;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is _ViewModel &&
runtimeType == other.runtimeType &&
isLoaded == other.isLoaded &&
fireNotifications == other.fireNotifications &&
fireNotificationsUnread == other.fireNotificationsUnread &&
yourLocations == other.yourLocations;
@override
int get hashCode =>
isLoaded.hashCode ^
fireNotifications.hashCode ^
fireNotificationsUnread.hashCode ^
yourLocations.hashCode;
}
class FireNotificationList extends StatefulWidget {
const FireNotificationList({super.key});
static const String routeName = '/fireNotifications';
@override
_FireNotificationListState createState() => _FireNotificationListState();
}
class _FireNotificationListState extends State<FireNotificationList> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
Widget _buildRow(
BuildContext context,
List<YourLocation> yourLocations,
FireNotification notif,
DeleteFireNotificationFunction onDeleted,
TapFireNotificationFunction onTap) {
String prefix = '';
// FIXME (this can fails if you don't have a location for this notif, for instance during tests)
final YourLocation yl =
yourLocations.singleWhere((YourLocation yl) => yl.id == notif.subsId);
prefix = '${yl.description}. ';
return ListTile(
dense: true,
leading: const Icon(Icons.whatshot),
title: Text('$prefix${notif.description}',
style: TextStyle(
fontWeight: notif.read ? FontWeight.normal : FontWeight.bold)),
subtitle: Text(Moment.now().from(context, notif.when)),
onLongPress: () {
showSnackMsg(S.of(context).toDeleteThisNotification);
},
onTap: () {
onTap(notif);
});
}
void showSnackMsg(String msg) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(msg),
));
}
Widget _buildSavedFireNotifications(
BuildContext context,
List<YourLocation> yourLocations,
List<FireNotification> notifList,
DeleteFireNotificationFunction onDeleted,
TapFireNotificationFunction onTap) {
return RefreshIndicator(
onRefresh: _handleRefresh,
child: ListView.builder(
padding: const EdgeInsets.all(16.0),
// reverse: true,
// shrinkWrap: true,
itemCount: notifList.length,
itemBuilder: (BuildContext context, int i) {
final ThemeData theme = Theme.of(context);
return Dismissible(
key: ObjectKey(notifList.elementAt(i)),
onDismissed: (DismissDirection direction) {
onDeleted(notifList.elementAt(i));
},
background: Container(
color: theme.primaryColor,
child: const ListTile(
leading: Icon(Icons.delete,
color: Colors.white, size: 36.0))),
secondaryBackground: Container(
color: theme.primaryColor,
child: const ListTile(
trailing: Icon(Icons.delete,
color: Colors.white, size: 36.0))),
child: Container(
decoration: BoxDecoration(
color: theme.canvasColor,
border: Border(
bottom: BorderSide(color: theme.dividerColor))),
child: _buildRow(context, yourLocations,
notifList.elementAt(i), onDeleted, onTap)));
}));
}
Future<void> _handleRefresh() async {
await Future<void>.delayed(const Duration(seconds: 1));
setState(() {});
return;
}
@override
Widget build(BuildContext context) {
return StoreConnector<AppState, _ViewModel>(
distinct: true,
converter: (Store<AppState> store) {
print(
'New ViewModel of Fires Notifications (unread: ${store.state.fireNotificationsUnread})');
return _ViewModel(
isLoaded: store.state.isLoaded,
onDeleteAll: () {
store.dispatch(DeleteAllFireNotificationAction());
},
onDelete: (FireNotification notif) {
store.dispatch(DeleteFireNotificationAction(notif));
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(S.of(context).youDeletedThisNotification),
action: SnackBarAction(
label: S.of(context).UNDO,
onPressed: () {
store.dispatch(AddFireNotificationAction(notif));
})));
},
onTap: (FireNotification notif) {
if (!notif.read) {
store.dispatch(
ReadFireNotificationAction(notif.copyWith(read: true)));
}
Timer(const Duration(milliseconds: 500), () {
gotoMap(store, notif, context);
});
},
yourLocations: store.state.yourLocations,
fireNotifications: store.state.fireNotifications,
fireNotificationsUnread: store.state.fireNotificationsUnread);
},
builder: (BuildContext context, _ViewModel view) {
final bool hasFireNotifications = view.fireNotifications.isNotEmpty;
final String title = S.of(context).fireNotificationsTitle;
print('Building Fire Notifications List');
return Scaffold(
key: _scaffoldKey,
drawer: MainDrawer(context, FireNotificationList.routeName),
appBar: AppBar(
title: Text(title),
leading: IconButton(
icon: const Icon(Icons.menu),
onPressed: () {
_scaffoldKey.currentState?.openDrawer();
},
),
actions: <Widget?>[
if (hasFireNotifications)
IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _showConfirmDialog(view))
else
null
].where((Widget? w) => w != null).cast<Widget>().toList()),
body: !view.isLoaded
? const FiresSpinner()
: !hasFireNotifications
? Padding(
padding: const EdgeInsets.all(20.0),
child: Card(
child: Padding(
padding: const EdgeInsets.all(20.0),
child: CenteredColumn(children: <Widget>[
const Icon(Icons.notifications_none,
size: 150.0, color: Colors.black26),
const SizedBox(height: 20.0),
Text(
S
.of(context)
.fireNotificationsDescription,
textAlign: TextAlign.center,
textScaleFactor: 1.1,
style: const TextStyle(
height: 1.3, color: Colors.black45))
]))))
: _buildSavedFireNotifications(
context,
view.yourLocations,
view.fireNotifications,
view.onDelete,
view.onTap));
});
}
void gotoMap(
Store<AppState> store, FireNotification notif, BuildContext context) {
store.dispatch(ShowFireNotificationMapAction(notif));
Navigator.push(
context,
MaterialPageRoute<void>(
builder: (BuildContext context) => genericMap()));
}
Future<void> _showConfirmDialog(_ViewModel view) {
return showDialog<void>(
context: context,
barrierDismissible: false, // user must tap button!
builder: (BuildContext context) {
return AlertDialog(
title: Text(S.of(context).areYouSureTitle),
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
Text(S.of(context).deleteAllFireNotificationsAlertDescription)
],
),
),
actions: <Widget>[
TextButton(
child: Text(S.of(context).DELETE),
onPressed: () {
view.onDeleteAll();
Navigator.of(context).pop();
},
),
],
);
},
);
}
}