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)
This commit is contained in:
vjrj 2026-03-06 21:44:54 +01:00
parent 956165c524
commit 7812ed951e
19 changed files with 129 additions and 111 deletions

File diff suppressed because one or more lines are too long

View file

@ -56,7 +56,7 @@ linter:
- avoid_unused_constructor_parameters - avoid_unused_constructor_parameters
- avoid_void_async - avoid_void_async
- await_only_futures - await_only_futures
# Naming conventions # Naming conventions
- camel_case_extensions - camel_case_extensions
- camel_case_types - camel_case_types
@ -67,7 +67,7 @@ linter:
- non_constant_identifier_names - non_constant_identifier_names
- package_names - package_names
- package_prefixed_library_names - package_prefixed_library_names
# Code quality and style # Code quality and style
- cast_nullable_to_non_nullable - cast_nullable_to_non_nullable
- control_flow_in_finally - control_flow_in_finally
@ -96,7 +96,7 @@ linter:
- null_closures - null_closures
- only_throw_errors - only_throw_errors
- overridden_fields - overridden_fields
# Preferences and improvements # Preferences and improvements
- prefer_adjacent_string_concatenation - prefer_adjacent_string_concatenation
- prefer_asserts_in_initializer_lists - prefer_asserts_in_initializer_lists
@ -174,4 +174,3 @@ linter:
- use_test_throws_matchers - use_test_throws_matchers
- valid_regexps - valid_regexps
- void_checks - void_checks
- avoid_flutter_imports

View file

@ -1,3 +1,9 @@
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'dev.flutter.flutter-gradle-plugin'
}
def localProperties = new Properties() def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties') def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) { if (localPropertiesFile.exists()) {
@ -6,27 +12,12 @@ if (localPropertiesFile.exists()) {
} }
} }
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new FileNotFoundException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
plugins {
id 'com.android.application'
id 'kotlin-android'
}
def keystorePropertiesFile = rootProject.file("key.properties") def keystorePropertiesFile = rootProject.file("key.properties")
def keystoreProperties = new Properties() def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
// Load Flutter's Gradle plugin
if (flutterRoot != null) {
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
}
android { android {
compileSdkVersion 34 compileSdkVersion 36
namespace "org.comunes.fires" namespace "org.comunes.fires"
lintOptions { lintOptions {
@ -37,7 +28,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "org.comunes.fires" applicationId "org.comunes.fires"
minSdkVersion flutter.minSdkVersion minSdkVersion flutter.minSdkVersion
targetSdkVersion 34 targetSdkVersion 36
versionCode 9 versionCode 9
versionName "1.9" versionName "1.9"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
@ -57,7 +48,7 @@ android {
signingConfig signingConfigs.release signingConfig signingConfigs.release
minifyEnabled true minifyEnabled true
useProguard true shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }

View file

@ -14,3 +14,6 @@
-keepnames class org.ietf.jgss.** { *; } -keepnames class org.ietf.jgss.** { *; }
-dontwarn org.apache.** -dontwarn org.apache.**
-dontwarn org.w3c.dom.** -dontwarn org.w3c.dom.**
# Google Play Core libraries - optional features
-dontwarn com.google.android.play.core.**

View file

@ -1,7 +1,7 @@
buildscript { buildscript {
repositories { repositories {
google() google()
jcenter() mavenCentral()
} }
dependencies { dependencies {
@ -13,7 +13,16 @@ buildscript {
allprojects { allprojects {
repositories { repositories {
google() google()
jcenter() mavenCentral()
}
plugins.withId('kotlin-android') {
tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
kotlinOptions {
languageVersion = '1.8'
allWarningsAsErrors = false
}
}
} }
} }

View file

@ -1,3 +1,4 @@
org.gradle.jvmargs=-Xmx1536M org.gradle.jvmargs=-Xmx4096M
android.useAndroidX=true android.useAndroidX=true
android.enableJetifier=true android.enableJetifier=false
org.jetbrains.kotlin.jvm.target=1.8

View file

@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip

View file

@ -1,23 +0,0 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
include ':app'
def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
def plugins = new Properties()
def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
if (pluginsFile.exists()) {
pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
}
plugins.each { name, path ->
def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
include ":$name"
project(":$name").projectDir = pluginDirectory
}

View file

@ -0,0 +1,40 @@
pluginManagement {
val flutterSdkPath =
run {
val properties = java.util.Properties()
file("local.properties").inputStream().use { properties.load(it) }
val flutterSdkPath = properties.getProperty("flutter.sdk")
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
flutterSdkPath
}
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.11.1" apply false
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
}
include(":app")
val flutterRoot = rootProject.projectDir.parentFile.toPath()
val pluginsFile = flutterRoot.resolve(".flutter-plugins").toFile()
val plugins = java.util.Properties()
if (pluginsFile.exists()) {
pluginsFile.inputStream().use { plugins.load(it) }
}
plugins.forEach { name, path ->
val pluginDirectory = flutterRoot.resolve(path as String).resolve("android").toFile()
include(":$name")
project(":$name").projectDir = pluginDirectory
}

View file

@ -237,7 +237,6 @@ class _CustomStepperState extends State<CustomStepper> {
case CustomStepState.error: case CustomStepState.error:
return const Text('!', style: _kCustomStepStyle); return const Text('!', style: _kCustomStepStyle);
} }
return const SizedBox.shrink();
} }
Color _circleColor(int index) { Color _circleColor(int index) {
@ -343,7 +342,7 @@ class _CustomStepperState extends State<CustomStepper> {
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints.tightFor(height: 48.0), constraints: const BoxConstraints.tightFor(height: 48.0),
child: const Row( child: const Row(
), ),
), ),
); );
@ -365,7 +364,6 @@ class _CustomStepperState extends State<CustomStepper> {
return (textTheme.bodyLarge ?? const TextStyle()) return (textTheme.bodyLarge ?? const TextStyle())
.copyWith(color: _isDark() ? _kErrorDark : _kErrorLight); .copyWith(color: _isDark() ? _kErrorDark : _kErrorLight);
} }
return const TextStyle();
} }
TextStyle _subtitleStyle(int index) { TextStyle _subtitleStyle(int index) {
@ -384,7 +382,6 @@ class _CustomStepperState extends State<CustomStepper> {
return (textTheme.bodySmall ?? const TextStyle()) return (textTheme.bodySmall ?? const TextStyle())
.copyWith(color: _isDark() ? _kErrorDark : _kErrorLight); .copyWith(color: _isDark() ? _kErrorDark : _kErrorLight);
} }
return const TextStyle();
} }
Widget _buildHeaderText(int index) { Widget _buildHeaderText(int index) {
@ -596,7 +593,6 @@ class _CustomStepperState extends State<CustomStepper> {
case CustomStepperType.horizontal: case CustomStepperType.horizontal:
return _buildHorizontal(); return _buildHorizontal();
} }
return const SizedBox.shrink();
} }
} }

View file

@ -63,8 +63,12 @@ class FireNotificationList extends StatefulWidget {
class _FireNotificationListState extends State<FireNotificationList> { class _FireNotificationListState extends State<FireNotificationList> {
final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>(); final GlobalKey<ScaffoldState> _scaffoldKey = GlobalKey<ScaffoldState>();
Widget _buildRow(BuildContext context, List<YourLocation> yourLocations, Widget _buildRow(
FireNotification notif, onDeleted, onTap) { BuildContext context,
List<YourLocation> yourLocations,
FireNotification notif,
DeleteFireNotificationFunction onDeleted,
TapFireNotificationFunction onTap) {
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)
@ -97,8 +101,8 @@ class _FireNotificationListState extends State<FireNotificationList> {
BuildContext context, BuildContext context,
List<YourLocation> yourLocations, List<YourLocation> yourLocations,
List<FireNotification> notifList, List<FireNotification> notifList,
onDeleted, DeleteFireNotificationFunction onDeleted,
onTap) { TapFireNotificationFunction onTap) {
return RefreshIndicator( return RefreshIndicator(
onRefresh: _handleRefresh, onRefresh: _handleRefresh,
child: ListView.builder( child: ListView.builder(

View file

@ -20,7 +20,6 @@ import 'objectIdUtils.dart';
import 'redux/actions.dart'; import 'redux/actions.dart';
class _ViewModel { class _ViewModel {
_ViewModel({required this.isLoaded}); _ViewModel({required this.isLoaded});
final bool isLoaded; final bool isLoaded;
@ -36,7 +35,6 @@ class _ViewModel {
} }
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
const HomePage({super.key}); const HomePage({super.key});
static const String routeName = '/home'; static const String routeName = '/home';
@ -72,7 +70,8 @@ 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) {
final FireNotification? notif = _notifForMessage(message.data, store.state.isLoaded); final FireNotification? notif =
_notifForMessage(message.data, store.state.isLoaded);
if (notif != null) { if (notif != null) {
_showItemDialog(message.data, notif); _showItemDialog(message.data, notif);
} }
@ -160,8 +159,7 @@ class _HomePageState extends State<HomePage> {
}, },
heroTag: 'notifyFire', heroTag: 'notifyFire',
backgroundColor: fires600, backgroundColor: fires600,
label: Text(S.of(context).notifyAFire, label: Text(S.of(context).notifyAFire, style: _btnFont),
style: _btnFont),
icon: icon:
const Icon(Icons.notifications_active, size: 32.0), const Icon(Icons.notifications_active, size: 32.0),
), ),
@ -172,15 +170,14 @@ class _HomePageState extends State<HomePage> {
: SafeArea( : SafeArea(
child: Center( child: Center(
child: CenteredColumn(children: <Widget>[ child: CenteredColumn(children: <Widget>[
Row( Row(children: <Widget>[
children: <Widget>[ IconButton(
IconButton( onPressed: () {
onPressed: () { _scaffoldKey.currentState?.openDrawer();
_scaffoldKey.currentState?.openDrawer(); },
}, icon: const Icon(Icons.menu,
icon: const Icon(Icons.menu, size: 30.0, color: Colors.black38)),
size: 30.0, color: Colors.black38)), ]),
]),
Expanded( Expanded(
child: FractionallySizedBox( child: FractionallySizedBox(
alignment: FractionalOffset.center, alignment: FractionalOffset.center,
@ -238,7 +235,9 @@ class _HomePageState extends State<HomePage> {
if (shouldNavigate ?? false) { if (shouldNavigate ?? false) {
_navigateToItemDetail(message); _navigateToItemDetail(message);
} }
}).catchError((e) => print('$e')); }).catchError((Object e) {
print('$e');
});
} }
Widget _buildDialog(BuildContext context, FireNotification item) { Widget _buildDialog(BuildContext context, FireNotification item) {

View file

@ -97,20 +97,14 @@ Widget mainDrawer(BuildContext context, String currentRoute) {
leading: const Icon(Icons.notifications), leading: const Icon(Icons.notifications),
selected: currentRoute == FireNotificationList.routeName, selected: currentRoute == FireNotificationList.routeName,
title: Text(S.of(context).fireNotificationsTitleShort), title: Text(S.of(context).fireNotificationsTitleShort),
trailing: SizedBox( trailing: badges_pkg.Badge(
width: 100, position:
child: Row( badges_pkg.BadgePosition.topEnd(top: -10, end: -12),
mainAxisAlignment: MainAxisAlignment.end, badgeContent: Text(
children: <Widget>[ view.unreadCount.toString(),
badges_pkg.Badge( style: const TextStyle(color: Colors.white),
position: badges_pkg.BadgePosition.topEnd( ),
top: -10, end: -12), child: const Icon(Icons.notifications)),
badgeContent: Text(
view.unreadCount.toString(),
style: const TextStyle(color: Colors.white),
),
child: const Icon(Icons.notifications))
])),
// Text(S.of(context).fireNotificationsTitleShort), // Text(S.of(context).fireNotificationsTitleShort),
onTap: () { onTap: () {

View file

@ -6,7 +6,7 @@ part of 'fireNotification.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
FireNotification _$FireNotificationFromJson(Map json) => $checkedCreate( FireNotification _$FireNotificationFromJson(Map<dynamic,dynamic> json) => $checkedCreate(
'FireNotification', 'FireNotification',
json, json,
($checkedConvert) { ($checkedConvert) {

View file

@ -1,28 +1,28 @@
import '../models/appState.dart'; import '../models/appState.dart';
import 'actions.dart'; import 'actions.dart';
AppState appReducer(AppState state, action) { AppState appReducer(AppState state, dynamic action) {
if (action is FetchYourLocationsSucceededAction) { if (action is FetchYourLocationsSucceededAction) {
return state.copyWith(yourLocations: action.fetchedYourLocations); return state.copyWith(yourLocations: action.fetchedYourLocations);
} }
if (action is FetchFireNotificationsSucceededAction) { if (action is FetchFireNotificationsSucceededAction) {
return state.copyWith(fireNotifications: action.fetchedFireNotifications, return state.copyWith(
fireNotificationsUnread: action.unreadCount); fireNotifications: action.fetchedFireNotifications,
fireNotificationsUnread: action.unreadCount);
} }
if (action is FetchMonitoredAreasSucceededAction) { if (action is FetchMonitoredAreasSucceededAction) {
return state.copyWith(monitoredAreas: action.monitoredAreas); return state.copyWith(monitoredAreas: action.monitoredAreas);
} }
if (action is AddedFireNotificationAction) if (action is AddedFireNotificationAction)
return state.copyWith( return state.copyWith(
fireNotificationsUnread: state.fireNotificationsUnread + 1); fireNotificationsUnread: state.fireNotificationsUnread + 1);
if (action is ReadedFireNotificationAction) if (action is ReadedFireNotificationAction)
return state.copyWith( return state.copyWith(
fireNotificationsUnread: state.fireNotificationsUnread - 1); fireNotificationsUnread: state.fireNotificationsUnread - 1);
if (action is DeleteAllFireNotificationAction) if (action is DeleteAllFireNotificationAction)
return state.copyWith( return state.copyWith(fireNotificationsUnread: 0);
fireNotificationsUnread: 0);
return state; return state;
} }

View file

@ -28,12 +28,13 @@ FiresApi api = GetIt.instance<FiresApi>();
// Simple debounce mechanism // Simple debounce mechanism
Timer? _locationUpdateDebounceTimer; Timer? _locationUpdateDebounceTimer;
void debounceLocationUpdate(Duration duration, Function() callback) { void debounceLocationUpdate(Duration duration, void Function() callback) {
_locationUpdateDebounceTimer?.cancel(); _locationUpdateDebounceTimer?.cancel();
_locationUpdateDebounceTimer = Timer(duration, callback); _locationUpdateDebounceTimer = Timer(duration, callback);
} }
void fetchDataMiddleware(Store<AppState> store, action, NextDispatcher next) { void fetchDataMiddleware(
Store<AppState> store, dynamic action, NextDispatcher next) {
// If our Middleware encounters a `FetchYourLocationAction` // If our Middleware encounters a `FetchYourLocationAction`
if (action is OnUserLangAction) { if (action is OnUserLangAction) {
@ -112,7 +113,8 @@ void fetchDataMiddleware(Store<AppState> store, action, NextDispatcher next) {
lat: action.loc.lat, lat: action.loc.lat,
lon: action.loc.lon, lon: action.loc.lon,
distance: action.loc.distance) distance: action.loc.distance)
.then((UpdateFireMapStatsAction result) => store.dispatch(result))); .then(
(UpdateFireMapStatsAction result) => store.dispatch(result)));
store.dispatch(UpdatedYourLocationAction(action.loc)); store.dispatch(UpdatedYourLocationAction(action.loc));
persistYourLocations(store.state.yourLocations); persistYourLocations(store.state.yourLocations);
} }
@ -162,7 +164,8 @@ void fetchDataMiddleware(Store<AppState> store, action, NextDispatcher next) {
print('Local persisted: ${localLocations.length}'); print('Local persisted: ${localLocations.length}');
for (final YourLocation subsLoc in subscribedLocations) { for (final YourLocation subsLoc in subscribedLocations) {
final YourLocation locSubs = localLocations.firstWhere( final YourLocation locSubs = localLocations.firstWhere(
(YourLocation localLocation) => localLocation.id == subsLoc.id, orElse: () { (YourLocation localLocation) => localLocation.id == subsLoc.id,
orElse: () {
localLocations.add(subsLoc); localLocations.add(subsLoc);
return subsLoc; return subsLoc;
}); });
@ -188,10 +191,10 @@ void fetchDataMiddleware(Store<AppState> store, action, NextDispatcher next) {
final Completer<void>? completer = action.refreshCallback; final Completer<void>? completer = action.refreshCallback;
completer?.complete(null); completer?.complete(null);
}); });
}).catchError((Exception onError) { }).catchError((Object 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.
store.dispatch(FetchYourLocationsFailedAction(onError)); store.dispatch(FetchYourLocationsFailedAction(onError as Exception));
}); });
} }
@ -257,15 +260,16 @@ void getFiresStatsInFire(Store<AppState> store, FireNotification notif) {
.then((UpdateFireMapStatsAction result) => store.dispatch(result)); .then((UpdateFireMapStatsAction result) => store.dispatch(result));
} }
void unsubsViaApi(Store<AppState> store, ObjectId id, onUnsubs) { void unsubsViaApi(
Store<AppState> store, ObjectId id, void Function() onUnsubs) {
api.unsubscribe(store.state, id.hexString).then((bool res) { api.unsubscribe(store.state, id.hexString).then((bool res) {
onUnsubs(); onUnsubs();
persistYourLocations(store.state.yourLocations); persistYourLocations(store.state.yourLocations);
}); });
} }
void subscribeViaApi( void subscribeViaApi(Store<AppState> store, YourLocation loc,
Store<AppState> store, YourLocation loc, Function(YourLocation) onSubs) { void Function(YourLocation) onSubs) {
api.subscribe(store.state, loc).then((String subsId) { api.subscribe(store.state, loc).then((String subsId) {
final YourLocation sub = loc; final YourLocation sub = loc;
// if (loc.id != subsId) { // if (loc.id != subsId) {

View file

@ -9,7 +9,7 @@ import 'userReducer.dart';
import 'yourLocationsReducer.dart'; import 'yourLocationsReducer.dart';
// We create the State reducer by combining many smaller reducers into one! // We create the State reducer by combining many smaller reducers into one!
AppState appStateReducer(AppState prevState, action) { AppState appStateReducer(AppState prevState, dynamic action) {
final AppState state = appReducer(prevState, action); final AppState state = appReducer(prevState, action);
return AppState( return AppState(
yourLocations: yourLocationsReducer(state.yourLocations, action), yourLocations: yourLocationsReducer(state.yourLocations, action),

View file

@ -1,8 +1,9 @@
import '../models/user.dart'; import '../models/user.dart';
import 'actions.dart'; import 'actions.dart';
User userReducer(User user, action) { User userReducer(User user, dynamic action) {
if (action is OnUserCreatedAction) return user.copyWith(userId: action.userId); if (action is OnUserCreatedAction)
return user.copyWith(userId: action.userId);
if (action is OnUserTokenAction) return user.copyWith(token: action.token); if (action is OnUserTokenAction) return user.copyWith(token: action.token);
if (action is OnUserLangAction) return user.copyWith(lang: action.lang); if (action is OnUserLangAction) return user.copyWith(lang: action.lang);
return user; return user;

View file

@ -24,7 +24,7 @@ class Sandbox extends StatelessWidget {
body: ElevatedButton( body: ElevatedButton(
child: const Text('Press'), child: const Text('Press'),
onPressed: () { onPressed: () {
showDialog( /*showDialog(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return const SimpleDialog( return const SimpleDialog(
@ -35,7 +35,7 @@ class Sandbox extends StatelessWidget {
child: Text('hhh'), child: Text('hhh'),
), ),
]); ]);
}); });*/
})); }));
} }
} }