From 5f4c7956953f5ac79399a9bb598cb88c789b86e4 Mon Sep 17 00:00:00 2001 From: Mozammal Hossain <152433210+mozammal-hossain@users.noreply.github.com> Date: Sat, 20 Jun 2026 03:11:27 +0600 Subject: [PATCH 1/4] =?UTF-8?q?[compass=5Fapp]=20Scope=20LogoutViewModel?= =?UTF-8?q?=20to=20route=20builder=20=E2=80=94=20fix=20#2604=20(#2819)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description **Problem:** `LogoutViewModel` was built inside `HomeHeader.build()`, so each rebuild could create a new instance and a new `logout` command. That does not match how other Compass view models are scoped (created in the GoRouter route `builder` and passed in), and it can affect an in-flight logout when the home screen rebuilds. **Change:** Create `LogoutViewModel` once in the `/home` route next to `HomeViewModel`, pass it through `HomeScreen` → `HomeHeader` → `LogoutButton`, and remove inline construction in `home_title.dart`. Update `home_screen_test.dart` to build `LogoutViewModel` with fakes and remove `Provider` wrappers that only supported `context.read()` in the header. **Result:** Logout view model lifetime aligns with the home route; no visual or copy changes (screenshots not needed). Fixes https://github.com/flutter/samples/issues/2604 --- ## Pre-launch Checklist - [x] I read the [Flutter Style Guide] _recently_, and have followed its advice. - [x] I signed the [CLA]. - [x] I read the [Contributors Guide]. - [x] I have added sample code updates to the [changelog]. - [x] I updated/added relevant documentation (doc comments with `///`). If you need help, consider asking for advice on the #hackers-devrel channel on [Discord]. [Flutter Style Guide]: https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md [CLA]: https://cla.developers.google.com/ [Discord]: https://github.com/flutter/flutter/blob/master/docs/contributing/Chat.md [Contributors Guide]: https://github.com/flutter/samples/blob/main/CONTRIBUTING.md [changelog]: ../CHANGELOG.md --------- Co-authored-by: Eric Windmill --- CHANGELOG.md | 10 ++++++ compass_app/app/lib/routing/router.dart | 10 +++++- .../app/lib/ui/home/widgets/home_screen.dart | 13 ++++++-- .../app/lib/ui/home/widgets/home_title.dart | 15 ++++----- .../ui/home/widgets/home_screen_test.dart | 20 +++++------ cupertino_gallery/lib/widgets/sheet_page.dart | 33 ++++++++----------- 6 files changed, 61 insertions(+), 40 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..3bb4049a4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,10 @@ +# Changelog + +Notable changes to the samples in this repository are listed here. + +## [Unreleased] + +### compass_app + +* Scope `LogoutViewModel` to the home route so it is not recreated on every + `HomeHeader` rebuild ([#2604](https://github.com/flutter/samples/issues/2604)). diff --git a/compass_app/app/lib/routing/router.dart b/compass_app/app/lib/routing/router.dart index 1ed94ed02..284706850 100644 --- a/compass_app/app/lib/routing/router.dart +++ b/compass_app/app/lib/routing/router.dart @@ -11,6 +11,7 @@ import '../ui/activities/view_models/activities_viewmodel.dart'; import '../ui/activities/widgets/activities_screen.dart'; import '../ui/auth/login/view_models/login_viewmodel.dart'; import '../ui/auth/login/widgets/login_screen.dart'; +import '../ui/auth/logout/view_models/logout_viewmodel.dart'; import '../ui/booking/view_models/booking_viewmodel.dart'; import '../ui/booking/widgets/booking_screen.dart'; import '../ui/home/view_models/home_viewmodel.dart'; @@ -46,7 +47,14 @@ GoRouter router(AuthRepository authRepository) => GoRouter( bookingRepository: context.read(), userRepository: context.read(), ); - return HomeScreen(viewModel: viewModel); + final logoutViewModel = LogoutViewModel( + authRepository: context.read(), + itineraryConfigRepository: context.read(), + ); + return HomeScreen( + viewModel: viewModel, + logoutViewModel: logoutViewModel, + ); }, routes: [ GoRoute( diff --git a/compass_app/app/lib/ui/home/widgets/home_screen.dart b/compass_app/app/lib/ui/home/widgets/home_screen.dart index fefe78003..09d1e49ba 100644 --- a/compass_app/app/lib/ui/home/widgets/home_screen.dart +++ b/compass_app/app/lib/ui/home/widgets/home_screen.dart @@ -7,6 +7,7 @@ import 'package:go_router/go_router.dart'; import '../../../domain/models/booking/booking_summary.dart'; import '../../../routing/routes.dart'; +import '../../auth/logout/view_models/logout_viewmodel.dart'; import '../../core/localization/applocalization.dart'; import '../../core/themes/colors.dart'; import '../../core/themes/dimens.dart'; @@ -18,9 +19,14 @@ import 'home_title.dart'; const String bookingButtonKey = 'booking-button'; class HomeScreen extends StatefulWidget { - const HomeScreen({super.key, required this.viewModel}); + const HomeScreen({ + super.key, + required this.viewModel, + required this.logoutViewModel, + }); final HomeViewModel viewModel; + final LogoutViewModel logoutViewModel; @override State createState() => _HomeScreenState(); @@ -88,7 +94,10 @@ class _HomeScreenState extends State { vertical: Dimens.of(context).paddingScreenVertical, horizontal: Dimens.of(context).paddingScreenHorizontal, ), - child: HomeHeader(viewModel: widget.viewModel), + child: HomeHeader( + viewModel: widget.viewModel, + logoutViewModel: widget.logoutViewModel, + ), ), ), SliverList.builder( diff --git a/compass_app/app/lib/ui/home/widgets/home_title.dart b/compass_app/app/lib/ui/home/widgets/home_title.dart index 165e7ffe5..16755793a 100644 --- a/compass_app/app/lib/ui/home/widgets/home_title.dart +++ b/compass_app/app/lib/ui/home/widgets/home_title.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; -import 'package:provider/provider.dart'; import '../../auth/logout/view_models/logout_viewmodel.dart'; import '../../auth/logout/widgets/logout_button.dart'; @@ -13,9 +12,14 @@ import '../../core/themes/dimens.dart'; import '../view_models/home_viewmodel.dart'; class HomeHeader extends StatelessWidget { - const HomeHeader({super.key, required this.viewModel}); + const HomeHeader({ + super.key, + required this.viewModel, + required this.logoutViewModel, + }); final HomeViewModel viewModel; + final LogoutViewModel logoutViewModel; @override Widget build(BuildContext context) { @@ -37,12 +41,7 @@ class HomeHeader extends StatelessWidget { height: Dimens.of(context).profilePictureSize, ), ), - LogoutButton( - viewModel: LogoutViewModel( - authRepository: context.read(), - itineraryConfigRepository: context.read(), - ), - ), + LogoutButton(viewModel: logoutViewModel), ], ), const SizedBox(height: Dimens.paddingVertical), diff --git a/compass_app/app/test/ui/home/widgets/home_screen_test.dart b/compass_app/app/test/ui/home/widgets/home_screen_test.dart index 24890be41..a99981100 100644 --- a/compass_app/app/test/ui/home/widgets/home_screen_test.dart +++ b/compass_app/app/test/ui/home/widgets/home_screen_test.dart @@ -2,16 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -import 'package:compass_app/data/repositories/auth/auth_repository.dart'; -import 'package:compass_app/data/repositories/itinerary_config/itinerary_config_repository.dart'; import 'package:compass_app/routing/routes.dart'; +import 'package:compass_app/ui/auth/logout/view_models/logout_viewmodel.dart'; import 'package:compass_app/ui/home/view_models/home_viewmodel.dart'; import 'package:compass_app/ui/home/widgets/home_screen.dart'; import 'package:compass_app/utils/result.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; -import 'package:provider/provider.dart'; import '../../../../testing/app.dart'; import '../../../../testing/fakes/repositories/fake_auth_repository.dart'; @@ -24,6 +22,7 @@ import '../../../../testing/models/booking.dart'; void main() { group('HomeScreen tests', () { late HomeViewModel viewModel; + late LogoutViewModel logoutViewModel; late MockGoRouter goRouter; late FakeBookingRepository bookingRepository; @@ -33,6 +32,10 @@ void main() { bookingRepository: bookingRepository, userRepository: FakeUserRepository(), ); + logoutViewModel = LogoutViewModel( + authRepository: FakeAuthRepository(), + itineraryConfigRepository: FakeItineraryConfigRepository(), + ); goRouter = MockGoRouter(); when(() => goRouter.push(any())).thenAnswer((_) => Future.value(null)); }); @@ -40,12 +43,9 @@ void main() { Future loadWidget(WidgetTester tester) async { await testApp( tester, - ChangeNotifierProvider.value( - value: FakeAuthRepository() as AuthRepository, - child: Provider.value( - value: FakeItineraryConfigRepository() as ItineraryConfigRepository, - child: HomeScreen(viewModel: viewModel), - ), + HomeScreen( + viewModel: viewModel, + logoutViewModel: logoutViewModel, ), goRouter: goRouter, ); diff --git a/cupertino_gallery/lib/widgets/sheet_page.dart b/cupertino_gallery/lib/widgets/sheet_page.dart index 9d27c0bd2..103d1bdcc 100644 --- a/cupertino_gallery/lib/widgets/sheet_page.dart +++ b/cupertino_gallery/lib/widgets/sheet_page.dart @@ -13,25 +13,20 @@ class SheetPage extends StatelessWidget { onPressed: () { Navigator.of(context).push( CupertinoSheetRoute( - scrollableBuilder: - (BuildContext context, ScrollController controller) { - Widget widgetBuilder(BuildContext context) { - return CupertinoPageScaffold( - navigationBar: CupertinoNavigationBar( - middle: const Text('Sheet'), - trailing: GestureDetector( - child: const Icon(CupertinoIcons.xmark), - onTap: () { - Navigator.of(context).pop(); - }, - ), - ), - child: const Center(child: Text('This is a sheet')), - ); - } - - return widgetBuilder(context); - }, + builder: (BuildContext context) { + return CupertinoPageScaffold( + navigationBar: CupertinoNavigationBar( + middle: const Text('Sheet'), + trailing: GestureDetector( + child: const Icon(CupertinoIcons.xmark), + onTap: () { + Navigator.of(context).pop(); + }, + ), + ), + child: const Center(child: Text('This is a sheet')), + ); + }, ), ); }, From 8da6d98439a0459ab26c3bf49ebcd3fbecbc5495 Mon Sep 17 00:00:00 2001 From: Niaz Sagor <50655067+NiazSagor@users.noreply.github.com> Date: Sat, 20 Jun 2026 03:13:45 +0600 Subject: [PATCH 2/4] Show placeholder for broken activity images (#2769) Hello, I noticed that there are several image URLs that fail to load on the Activities screen of the compass_app. The image shows the default error widget right now. This PR proposes to display a placeholder with a "broken image" icon instead of the default error widget. This provides better visual feedback to the user. Current: image After fix: image @ericwindmill, please take a look and see if it is appropriate. ## Pre-launch Checklist - [x] I read the [Flutter Style Guide] _recently_, and have followed its advice. - [x] I signed the [CLA]. - [x] I read the [Contributors Guide]. - [x] I have added sample code updates to the [changelog]. - [x] I updated/added relevant documentation (doc comments with `///`). [Flutter Style Guide]: https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md [CLA]: https://cla.developers.google.com/ [Discord]: https://github.com/flutter/flutter/blob/master/docs/contributing/Chat.md [Contributors Guide]: https://github.com/flutter/samples/blob/main/CONTRIBUTING.md [changelog]: ../CHANGELOG.md Co-authored-by: Eric Windmill --- .../ui/activities/widgets/activity_entry.dart | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/compass_app/app/lib/ui/activities/widgets/activity_entry.dart b/compass_app/app/lib/ui/activities/widgets/activity_entry.dart index 300e560a7..f75643f18 100644 --- a/compass_app/app/lib/ui/activities/widgets/activity_entry.dart +++ b/compass_app/app/lib/ui/activities/widgets/activity_entry.dart @@ -30,6 +30,7 @@ class ActivityEntry extends StatelessWidget { ClipRRect( borderRadius: BorderRadius.circular(8), child: CachedNetworkImage( + errorWidget: (context, url, error) => _ActivityErrorPlaceholder(), imageUrl: activity.imageUrl, height: 80, width: 80, @@ -66,3 +67,23 @@ class ActivityEntry extends StatelessWidget { ); } } + +class _ActivityErrorPlaceholder extends StatelessWidget { + const _ActivityErrorPlaceholder({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: BorderRadius.circular(8), + ), + child: Icon( + Icons.broken_image_outlined, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ); + } +} From f755c81579172def489e38d5d2c6c4778b330e05 Mon Sep 17 00:00:00 2001 From: Eric Windmill Date: Fri, 19 Jun 2026 14:14:31 -0700 Subject: [PATCH 3/4] Fix image loading error (#2865) *Replace this paragraph with a description of what this PR is changing or adding, and why. Consider including before/after screenshots.* *List which issues are fixed by this PR. For larger changes, raising an issue first helps reduce redundant work.* ## Pre-launch Checklist - [ ] I read the [Flutter Style Guide] _recently_, and have followed its advice. - [ ] I signed the [CLA]. - [ ] I read the [Contributors Guide]. - [ ] I have added sample code updates to the [changelog]. - [ ] I updated/added relevant documentation (doc comments with `///`). If you need help, consider asking for advice on the #hackers-devrel channel on [Discord]. [Flutter Style Guide]: https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md [CLA]: https://cla.developers.google.com/ [Discord]: https://github.com/flutter/flutter/blob/master/docs/contributing/Chat.md [Contributors Guide]: https://github.com/flutter/samples/blob/main/CONTRIBUTING.md [changelog]: ../CHANGELOG.md --------- Co-authored-by: NiazSagor Co-authored-by: Niaz Sagor <50655067+NiazSagor@users.noreply.github.com> --- compass_app/app/lib/ui/activities/widgets/activity_entry.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compass_app/app/lib/ui/activities/widgets/activity_entry.dart b/compass_app/app/lib/ui/activities/widgets/activity_entry.dart index f75643f18..b561844f9 100644 --- a/compass_app/app/lib/ui/activities/widgets/activity_entry.dart +++ b/compass_app/app/lib/ui/activities/widgets/activity_entry.dart @@ -69,7 +69,7 @@ class ActivityEntry extends StatelessWidget { } class _ActivityErrorPlaceholder extends StatelessWidget { - const _ActivityErrorPlaceholder({super.key}); + const _ActivityErrorPlaceholder(); @override Widget build(BuildContext context) { From 8bfffd4a84d83260eaa9fea12f806f206bafa9df Mon Sep 17 00:00:00 2001 From: Harsh Yadav Date: Sat, 20 Jun 2026 02:46:31 +0530 Subject: [PATCH 4/4] docs: clarify HomeViewModel lifecycle in Compass sample (#2838) Adds a comment explaining why `HomeViewModel` is not disposed in the Compass sample. This clarifies that the current implementation is safe because the ViewModel does not manage disposable resources. It also highlights that in real applications, proper lifecycle management is required when working with streams, controllers, or subscriptions. Fixes https://github.com/flutter/samples/issues/2788 ## Pre-launch Checklist * [x] I read the [[Flutter Style Guide](https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md)] recently, and have followed its advice. * [x] I signed the [[CLA](https://cla.developers.google.com/)]. * [ ] I have added sample code updates to the [[changelog](https://chatgpt.com/c/CHANGELOG.md)]. * [x] I updated/added relevant documentation (doc comments with `///`). [Flutter Style Guide]: https://github.com/flutter/flutter/blob/master/docs/contributing/Style-guide-for-Flutter-repo.md [CLA]: https://cla.developers.google.com/ [changelog]: ./CHANGELOG.md --------- Co-authored-by: Eric Windmill --- compass_app/app/lib/routing/router.dart | 3 +- .../home/widgets/home_screen_container.dart | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 compass_app/app/lib/ui/home/widgets/home_screen_container.dart diff --git a/compass_app/app/lib/routing/router.dart b/compass_app/app/lib/routing/router.dart index 284706850..05cad62b0 100644 --- a/compass_app/app/lib/routing/router.dart +++ b/compass_app/app/lib/routing/router.dart @@ -14,8 +14,7 @@ import '../ui/auth/login/widgets/login_screen.dart'; import '../ui/auth/logout/view_models/logout_viewmodel.dart'; import '../ui/booking/view_models/booking_viewmodel.dart'; import '../ui/booking/widgets/booking_screen.dart'; -import '../ui/home/view_models/home_viewmodel.dart'; -import '../ui/home/widgets/home_screen.dart'; +import '../ui/home/widgets/home_screen_container.dart'; import '../ui/results/view_models/results_viewmodel.dart'; import '../ui/results/widgets/results_screen.dart'; import '../ui/search_form/view_models/search_form_viewmodel.dart'; diff --git a/compass_app/app/lib/ui/home/widgets/home_screen_container.dart b/compass_app/app/lib/ui/home/widgets/home_screen_container.dart new file mode 100644 index 000000000..c62fbc3c1 --- /dev/null +++ b/compass_app/app/lib/ui/home/widgets/home_screen_container.dart @@ -0,0 +1,39 @@ +import 'package:flutter/widgets.dart'; +import 'package:provider/provider.dart'; + +import '../../../data/repositories/booking/booking_repository.dart'; +import '../../../data/repositories/user/user_repository.dart'; +import '../view_models/home_viewmodel.dart'; +import 'home_screen.dart'; + +class HomeScreenContainer extends StatefulWidget { + const HomeScreenContainer({super.key}); + + @override + State createState() => _HomeScreenContainerState(); +} + +class _HomeScreenContainerState extends State { + late final HomeViewModel _viewModel; + + @override + void initState() { + super.initState(); + + _viewModel = HomeViewModel( + bookingRepository: context.read(), + userRepository: context.read(), + ); + } + + @override + Widget build(BuildContext context) { + return HomeScreen(viewModel: _viewModel); + } + + @override + void dispose() { + _viewModel.dispose(); + super.dispose(); + } +}