From 0f1f585b4a844924e2dacc3e8e7e1f5bd23c2064 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Thu, 4 Jun 2026 16:17:43 +0200 Subject: [PATCH 1/7] feat(android): aggiungi permesso INTERNET e canale nativo per aprire URL Aggiunge INTERNET permission e la query ACTION_VIEW/https richiesta da Android 11+ per la package visibility. Espone il MethodChannel 'com.davide.biteplan/launcher' che lancia un Intent.ACTION_VIEW con l'URL ricevuto, senza dipendere da plugin Flutter esterni. Co-Authored-By: Claude Sonnet 4.6 --- android/app/src/main/AndroidManifest.xml | 5 ++++ .../com/davide/biteplan/MainActivity.kt | 26 ++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index bbc0ef6..1942b2e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ + + + + + diff --git a/android/app/src/main/kotlin/com/davide/biteplan/MainActivity.kt b/android/app/src/main/kotlin/com/davide/biteplan/MainActivity.kt index c87d893..d14c341 100644 --- a/android/app/src/main/kotlin/com/davide/biteplan/MainActivity.kt +++ b/android/app/src/main/kotlin/com/davide/biteplan/MainActivity.kt @@ -1,5 +1,29 @@ package com.davide.biteplan +import android.content.Intent +import android.net.Uri import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.plugin.common.MethodChannel -class MainActivity : FlutterActivity() +class MainActivity : FlutterActivity() { + private val channel = "com.davide.biteplan/launcher" + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channel) + .setMethodCallHandler { call, result -> + if (call.method == "launch") { + val url = call.argument("url") + if (url != null) { + startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) + result.success(null) + } else { + result.error("INVALID_URL", "URL is null", null) + } + } else { + result.notImplemented() + } + } + } +} From 296a622d82ff89319a8531ad006bf5263aadf80f Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Thu, 4 Jun 2026 16:17:53 +0200 Subject: [PATCH 2/7] feat: aggiungi controllo aggiornamenti con dialog all'avvio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All'avvio l'app chiama l'API GitHub releases per confrontare la versione installata con l'ultima disponibile (comparazione semantica major.minor.patch). Se esiste una versione più recente, mostra un AlertDialog con i pulsanti "Più tardi" e "Scarica" — quest'ultimo apre releases/latest nel browser. Il controllo è silenzioso in caso di assenza di rete o errori, e disabilitato su Flutter web (dart:io non disponibile). Co-Authored-By: Claude Sonnet 4.6 --- lib/app.dart | 36 ++++++++++++ lib/shared/services/update_service.dart | 55 +++++++++++++++++++ lib/shared/services/url_launcher_service.dart | 9 +++ 3 files changed, 100 insertions(+) create mode 100644 lib/shared/services/update_service.dart create mode 100644 lib/shared/services/url_launcher_service.dart diff --git a/lib/app.dart b/lib/app.dart index 48844a4..ea3bf9a 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -4,6 +4,8 @@ import 'features/meal_planner/presentation/pages/meal_planner_page.dart'; import 'features/converter/presentation/pages/converter_page.dart'; import 'features/shopping_list/presentation/pages/shopping_list_page.dart'; import 'features/guide/presentation/widgets/info_bottom_sheet.dart'; +import 'shared/services/update_service.dart'; +import 'shared/services/url_launcher_service.dart'; class BitePlanApp extends StatelessWidget { const BitePlanApp({super.key}); @@ -31,6 +33,8 @@ class _MainScaffoldState extends State<_MainScaffold> { late final List _pages; static const _titles = ['Piano Pasti', 'Convertitore', 'Lista della spesa']; + static const _releasesUrl = + 'https://github.com/davide3011/biteplan/releases/latest'; @override void initState() { @@ -42,6 +46,38 @@ class _MainScaffoldState extends State<_MainScaffold> { const ConverterPage(), const ShoppingListPage(), ]; + UpdateService.checkUpdate().then((newVersion) { + if (newVersion != null && mounted) { + _showUpdateDialog(newVersion); + } + }); + } + + void _showUpdateDialog(String newVersion) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + icon: const Icon(Icons.system_update_outlined, size: 32), + title: const Text('Aggiornamento disponibile'), + content: Text( + 'È disponibile la versione $newVersion di BitePlan.\n' + 'Vuoi scaricare il nuovo APK?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('Più tardi'), + ), + FilledButton( + onPressed: () { + Navigator.of(ctx).pop(); + UrlLauncherService.launch(_releasesUrl); + }, + child: const Text('Scarica'), + ), + ], + ), + ); } @override diff --git a/lib/shared/services/update_service.dart b/lib/shared/services/update_service.dart new file mode 100644 index 0000000..c27974b --- /dev/null +++ b/lib/shared/services/update_service.dart @@ -0,0 +1,55 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:flutter/foundation.dart' show kIsWeb, visibleForTesting; + +import '../../core/constants/app_constants.dart'; + +class UpdateService { + static const _apiUrl = + 'https://api.github.com/repos/davide3011/biteplan/releases/latest'; + + /// Returns the latest version string if it's newer than the current version, + /// or null if already up to date or if the check fails. + static Future checkUpdate() async { + if (kIsWeb) return null; + try { + final client = HttpClient() + ..connectionTimeout = const Duration(seconds: 5); + final request = await client + .getUrl(Uri.parse(_apiUrl)) + .timeout(const Duration(seconds: 5)); + request.headers + ..set('Accept', 'application/vnd.github+json') + ..set('User-Agent', 'biteplan/$kAppVersion'); + final response = + await request.close().timeout(const Duration(seconds: 5)); + final body = await response.transform(utf8.decoder).join(); + client.close(); + if (response.statusCode != 200) return null; + final data = jsonDecode(body) as Map; + final tag = (data['tag_name'] as String).replaceFirst(RegExp(r'^v'), ''); + return isNewer(tag, kAppVersion) ? tag : null; + } catch (_) { + return null; + } + } + + @visibleForTesting + static bool isNewer(String remote, String local) { + final r = parseVersion(remote); + final l = parseVersion(local); + for (var i = 0; i < 3; i++) { + if (r[i] > l[i]) return true; + if (r[i] < l[i]) return false; + } + return false; + } + + @visibleForTesting + static List parseVersion(String v) { + final parts = v.split('.'); + return List.generate( + 3, (i) => i < parts.length ? (int.tryParse(parts[i]) ?? 0) : 0); + } +} diff --git a/lib/shared/services/url_launcher_service.dart b/lib/shared/services/url_launcher_service.dart new file mode 100644 index 0000000..d78d6b1 --- /dev/null +++ b/lib/shared/services/url_launcher_service.dart @@ -0,0 +1,9 @@ +import 'package:flutter/services.dart'; + +class UrlLauncherService { + static const _channel = MethodChannel('com.davide.biteplan/launcher'); + + static Future launch(String url) async { + await _channel.invokeMethod('launch', {'url': url}); + } +} From 9486a70f1f0d1663d03320defd25abd533ea87ec Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Thu, 4 Jun 2026 16:18:00 +0200 Subject: [PATCH 3/7] test: aggiungi unit test per UpdateService MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copre parseVersion (semver standard, parti mancanti, valori non numerici) e isNewer (patch/minor/major bump, versione uguale, versione più vecchia, componenti multi-cifra come 2.0.10 > 2.0.9). 10/10 test passati su ARM64. Co-Authored-By: Claude Sonnet 4.6 --- test/shared/services/update_service_test.dart | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 test/shared/services/update_service_test.dart diff --git a/test/shared/services/update_service_test.dart b/test/shared/services/update_service_test.dart new file mode 100644 index 0000000..8403097 --- /dev/null +++ b/test/shared/services/update_service_test.dart @@ -0,0 +1,51 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:biteplan/shared/services/update_service.dart'; + +void main() { + group('UpdateService.parseVersion', () { + test('parses standard semver', () { + expect(UpdateService.parseVersion('2.1.3'), [2, 1, 3]); + }); + + test('pads missing parts with zeros', () { + expect(UpdateService.parseVersion('2.1'), [2, 1, 0]); + expect(UpdateService.parseVersion('2'), [2, 0, 0]); + }); + + test('handles non-numeric parts as zero', () { + expect(UpdateService.parseVersion('2.x.3'), [2, 0, 3]); + }); + }); + + group('UpdateService.isNewer', () { + test('patch bump is newer', () { + expect(UpdateService.isNewer('2.0.1', '2.0.0'), isTrue); + }); + + test('minor bump is newer', () { + expect(UpdateService.isNewer('2.1.0', '2.0.9'), isTrue); + }); + + test('major bump is newer', () { + expect(UpdateService.isNewer('3.0.0', '2.9.9'), isTrue); + }); + + test('same version is not newer', () { + expect(UpdateService.isNewer('2.0.0', '2.0.0'), isFalse); + }); + + test('older remote is not newer', () { + expect(UpdateService.isNewer('1.9.9', '2.0.0'), isFalse); + }); + + test('remote with v-prefix stripped is handled correctly', () { + // tag_name comes in already stripped of 'v' by checkUpdate + expect(UpdateService.isNewer('2.0.1', '2.0.0'), isTrue); + }); + + test('multi-digit version components', () { + expect(UpdateService.isNewer('2.0.10', '2.0.9'), isTrue); + expect(UpdateService.isNewer('2.10.0', '2.9.0'), isTrue); + }); + }); +} From 3da8ccc4d3332e694d6ce0de68abd9b696f3c1bd Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Thu, 4 Jun 2026 16:28:13 +0200 Subject: [PATCH 4/7] refactor: estrai showUpdateDialog e parseTagName in file dedicati Sposta la logica del dialog in lib/shared/widgets/update_dialog.dart per renderla importabile nei test senza duplicare il codice. Estrae parseTagName come metodo @visibleForTesting in UpdateService isolando il parsing JSON dalla chiamata HTTP. app.dart semplificato di conseguenza. Co-Authored-By: Claude Sonnet 4.6 --- lib/app.dart | 33 ++----------------------- lib/shared/services/update_service.dart | 14 +++++++++-- lib/shared/widgets/update_dialog.dart | 31 +++++++++++++++++++++++ 3 files changed, 45 insertions(+), 33 deletions(-) create mode 100644 lib/shared/widgets/update_dialog.dart diff --git a/lib/app.dart b/lib/app.dart index ea3bf9a..83723fb 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -5,7 +5,7 @@ import 'features/converter/presentation/pages/converter_page.dart'; import 'features/shopping_list/presentation/pages/shopping_list_page.dart'; import 'features/guide/presentation/widgets/info_bottom_sheet.dart'; import 'shared/services/update_service.dart'; -import 'shared/services/url_launcher_service.dart'; +import 'shared/widgets/update_dialog.dart'; class BitePlanApp extends StatelessWidget { const BitePlanApp({super.key}); @@ -33,8 +33,6 @@ class _MainScaffoldState extends State<_MainScaffold> { late final List _pages; static const _titles = ['Piano Pasti', 'Convertitore', 'Lista della spesa']; - static const _releasesUrl = - 'https://github.com/davide3011/biteplan/releases/latest'; @override void initState() { @@ -48,38 +46,11 @@ class _MainScaffoldState extends State<_MainScaffold> { ]; UpdateService.checkUpdate().then((newVersion) { if (newVersion != null && mounted) { - _showUpdateDialog(newVersion); + showUpdateDialog(context, newVersion); } }); } - void _showUpdateDialog(String newVersion) { - showDialog( - context: context, - builder: (ctx) => AlertDialog( - icon: const Icon(Icons.system_update_outlined, size: 32), - title: const Text('Aggiornamento disponibile'), - content: Text( - 'È disponibile la versione $newVersion di BitePlan.\n' - 'Vuoi scaricare il nuovo APK?', - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(ctx).pop(), - child: const Text('Più tardi'), - ), - FilledButton( - onPressed: () { - Navigator.of(ctx).pop(); - UrlLauncherService.launch(_releasesUrl); - }, - child: const Text('Scarica'), - ), - ], - ), - ); - } - @override Widget build(BuildContext context) { return Scaffold( diff --git a/lib/shared/services/update_service.dart b/lib/shared/services/update_service.dart index c27974b..4035ded 100644 --- a/lib/shared/services/update_service.dart +++ b/lib/shared/services/update_service.dart @@ -27,14 +27,24 @@ class UpdateService { final body = await response.transform(utf8.decoder).join(); client.close(); if (response.statusCode != 200) return null; - final data = jsonDecode(body) as Map; - final tag = (data['tag_name'] as String).replaceFirst(RegExp(r'^v'), ''); + final tag = parseTagName(body); + if (tag == null) return null; return isNewer(tag, kAppVersion) ? tag : null; } catch (_) { return null; } } + @visibleForTesting + static String? parseTagName(String body) { + try { + final data = jsonDecode(body) as Map; + return (data['tag_name'] as String).replaceFirst(RegExp(r'^v'), ''); + } catch (_) { + return null; + } + } + @visibleForTesting static bool isNewer(String remote, String local) { final r = parseVersion(remote); diff --git a/lib/shared/widgets/update_dialog.dart b/lib/shared/widgets/update_dialog.dart new file mode 100644 index 0000000..eab8432 --- /dev/null +++ b/lib/shared/widgets/update_dialog.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import '../services/url_launcher_service.dart'; + +const _releasesUrl = 'https://github.com/davide3011/biteplan/releases/latest'; + +void showUpdateDialog(BuildContext context, String newVersion) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + icon: const Icon(Icons.system_update_outlined, size: 32), + title: const Text('Aggiornamento disponibile'), + content: Text( + 'È disponibile la versione $newVersion di BitePlan.\n' + 'Vuoi scaricare il nuovo APK?', + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('Più tardi'), + ), + FilledButton( + onPressed: () { + Navigator.of(ctx).pop(); + UrlLauncherService.launch(_releasesUrl); + }, + child: const Text('Scarica'), + ), + ], + ), + ); +} From b391199bce37f08d84189fdb365e6c8da3e9ce0f Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Thu, 4 Jun 2026 16:28:21 +0200 Subject: [PATCH 5/7] test: aggiungi test parsing JSON e widget test del dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit parseTagName: v-prefix, body senza prefisso, JSON malformato, tag_name assente, body vuoto. Widget test del dialog: titolo, versione nel testo, pulsanti Più tardi e Scarica, chiusura dialog, chiamata MethodChannel verificata con mock. 130/130 test passati. Co-Authored-By: Claude Sonnet 4.6 --- test/shared/services/update_service_test.dart | 22 +++++ test/shared/widgets/update_dialog_test.dart | 82 +++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 test/shared/widgets/update_dialog_test.dart diff --git a/test/shared/services/update_service_test.dart b/test/shared/services/update_service_test.dart index 8403097..462e03e 100644 --- a/test/shared/services/update_service_test.dart +++ b/test/shared/services/update_service_test.dart @@ -2,6 +2,28 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:biteplan/shared/services/update_service.dart'; void main() { + group('UpdateService.parseTagName', () { + test('strips v prefix from tag_name', () { + expect(UpdateService.parseTagName('{"tag_name":"v2.1.0"}'), '2.1.0'); + }); + + test('works without v prefix', () { + expect(UpdateService.parseTagName('{"tag_name":"2.1.0"}'), '2.1.0'); + }); + + test('returns null for malformed JSON', () { + expect(UpdateService.parseTagName('not json'), isNull); + }); + + test('returns null when tag_name is missing', () { + expect(UpdateService.parseTagName('{"message":"Not Found"}'), isNull); + }); + + test('returns null for empty body', () { + expect(UpdateService.parseTagName(''), isNull); + }); + }); + group('UpdateService.parseVersion', () { test('parses standard semver', () { expect(UpdateService.parseVersion('2.1.3'), [2, 1, 3]); diff --git a/test/shared/widgets/update_dialog_test.dart b/test/shared/widgets/update_dialog_test.dart new file mode 100644 index 0000000..777300c --- /dev/null +++ b/test/shared/widgets/update_dialog_test.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:biteplan/shared/widgets/update_dialog.dart'; + +import '../../helpers/pump_app.dart'; + +void main() { + setUp(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + const MethodChannel('com.davide.biteplan/launcher'), + (call) async => null, + ); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + const MethodChannel('com.davide.biteplan/launcher'), + null, + ); + }); + + Future openDialog(WidgetTester tester, String version) async { + await tester.pumpApp( + Builder( + builder: (ctx) => TextButton( + onPressed: () => showUpdateDialog(ctx, version), + child: const Text('apri'), + ), + ), + ); + await tester.tap(find.text('apri')); + await tester.pumpAndSettle(); + } + + testWidgets('mostra il titolo corretto', (tester) async { + await openDialog(tester, '2.1.0'); + expect(find.text('Aggiornamento disponibile'), findsOneWidget); + }); + + testWidgets('mostra la versione nel contenuto', (tester) async { + await openDialog(tester, '2.1.0'); + expect(find.textContaining('2.1.0'), findsOneWidget); + }); + + testWidgets('ha i pulsanti Più tardi e Scarica', (tester) async { + await openDialog(tester, '2.1.0'); + expect(find.text('Più tardi'), findsOneWidget); + expect(find.text('Scarica'), findsOneWidget); + }); + + testWidgets('Più tardi chiude il dialog', (tester) async { + await openDialog(tester, '2.1.0'); + await tester.tap(find.text('Più tardi')); + await tester.pumpAndSettle(); + expect(find.text('Aggiornamento disponibile'), findsNothing); + }); + + testWidgets('Scarica chiude il dialog e chiama il launcher', (tester) async { + final calls = []; + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler( + const MethodChannel('com.davide.biteplan/launcher'), + (call) async { + calls.add(call); + return null; + }, + ); + + await openDialog(tester, '2.1.0'); + await tester.tap(find.text('Scarica')); + await tester.pumpAndSettle(); + + expect(find.text('Aggiornamento disponibile'), findsNothing); + expect(calls, hasLength(1)); + expect(calls.first.method, 'launch'); + expect(calls.first.arguments['url'], + contains('github.com/davide3011/biteplan')); + }); +} From 1f4f374f6ff600ecae57622d1663fd206a80303c Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Thu, 4 Jun 2026 16:28:27 +0200 Subject: [PATCH 6/7] =?UTF-8?q?docs:=20aggiorna=20README=20e=20CLAUDE.md?= =?UTF-8?q?=20per=20funzionalit=C3=A0=20update=20checker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aggiunge sezione "Controllo aggiornamenti" nelle funzionalità del README. Aggiorna struttura test in README e CLAUDE.md con le nuove cartelle shared/services e shared/widgets. Contatore test 110 → 130. Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 31 +++++++++++++++++-------------- README.md | 35 ++++++++++++++++++++++------------- 2 files changed, 39 insertions(+), 27 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 9d074e4..9625bf8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -84,24 +84,27 @@ lib/ ## Testing -110 test (unit + widget). Non richiedono device fisico. +130 test (unit + widget). Non richiedono device fisico. ``` test/ ├── helpers/pump_app.dart # estensione pumpApp per widget test -└── features/ - ├── converter/ - │ ├── models/conversion_entry_test.dart - │ └── providers/converter_provider_test.dart - ├── meal_planner/ - │ ├── models/meal_plan_test.dart - │ ├── providers/meal_planner_provider_test.dart - │ ├── widgets/meal_card_test.dart - │ └── qr_test.dart - └── shopping_list/ - ├── models/shopping_item_test.dart - ├── providers/shopping_list_provider_test.dart - └── widgets/shopping_item_tile_test.dart +├── features/ +│ ├── converter/ +│ │ ├── models/conversion_entry_test.dart +│ │ └── providers/converter_provider_test.dart +│ ├── meal_planner/ +│ │ ├── models/meal_plan_test.dart +│ │ ├── providers/meal_planner_provider_test.dart +│ │ ├── widgets/meal_card_test.dart +│ │ └── qr_test.dart +│ └── shopping_list/ +│ ├── models/shopping_item_test.dart +│ ├── providers/shopping_list_provider_test.dart +│ └── widgets/shopping_item_tile_test.dart +└── shared/ + ├── services/update_service_test.dart + └── widgets/update_dialog_test.dart ``` I test usano `SharedPreferences.setMockInitialValues({})` per isolare lo storage. diff --git a/README.md b/README.md index ce11d73..0f4bbb2 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,10 @@ App Android per la gestione della dieta quotidiana — pianificazione pasti, con - Separazione visiva tra elementi da completare e completati - Rimozione singola e svuota lista con conferma +### Controllo aggiornamenti +- All'avvio controlla silenziosamente l'ultima release su GitHub +- Se disponibile una versione più recente, mostra un dialog con link diretto al download dell'APK + ## Stack | Livello | Tecnologia | @@ -74,19 +78,24 @@ docker run --rm -v "$(pwd):/workspace" -w /workspace biteplan-build \ test/ ├── helpers/ │ └── pump_app.dart # estensione pumpApp per widget test -└── features/ - ├── converter/ - │ ├── models/conversion_entry_test.dart - │ └── providers/converter_provider_test.dart - ├── meal_planner/ - │ ├── models/meal_plan_test.dart - │ ├── providers/meal_planner_provider_test.dart - │ ├── widgets/meal_card_test.dart - │ └── qr_test.dart - └── shopping_list/ - ├── models/shopping_item_test.dart - ├── providers/shopping_list_provider_test.dart - └── widgets/shopping_item_tile_test.dart +├── features/ +│ ├── converter/ +│ │ ├── models/conversion_entry_test.dart +│ │ └── providers/converter_provider_test.dart +│ ├── meal_planner/ +│ │ ├── models/meal_plan_test.dart +│ │ ├── providers/meal_planner_provider_test.dart +│ │ ├── widgets/meal_card_test.dart +│ │ └── qr_test.dart +│ └── shopping_list/ +│ ├── models/shopping_item_test.dart +│ ├── providers/shopping_list_provider_test.dart +│ └── widgets/shopping_item_tile_test.dart +└── shared/ + ├── services/ + │ └── update_service_test.dart # parsing versione e confronto semver + └── widgets/ + └── update_dialog_test.dart # widget test dialog aggiornamento ``` ## Build APK From 2e67e5dd4eb6f8635be18f813b59d4761c8e330f Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Fri, 5 Jun 2026 08:06:42 +0200 Subject: [PATCH 7/7] chore: ignora test/widget_test.dart generato da flutter create File boilerplate generato automaticamente dal dev container, non appartiene al progetto e fallirebbe (referenzia MyApp inesistente). Co-Authored-By: Claude Sonnet 4.6 --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index d516c35..283dd80 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ dist/ /build/ *.g.dart +# Generato da flutter create — non appartiene al progetto +test/widget_test.dart + # Piattaforme non usate /ios/ /linux/