1
0
mirror of https://github.com/flutter/samples.git synced 2026-06-25 07:38:26 +00:00

[compass_app] Scope LogoutViewModel to route builder — fix #2604 (#2819)

## 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].

<!-- Links -->
[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 <eric@ericwindmill.com>
This commit is contained in:
Mozammal Hossain
2026-06-20 03:11:27 +06:00
committed by GitHub
parent 07909dd07e
commit 5f4c795695
6 changed files with 61 additions and 40 deletions

10
CHANGELOG.md Normal file
View File

@@ -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)).

View File

@@ -11,6 +11,7 @@ import '../ui/activities/view_models/activities_viewmodel.dart';
import '../ui/activities/widgets/activities_screen.dart'; import '../ui/activities/widgets/activities_screen.dart';
import '../ui/auth/login/view_models/login_viewmodel.dart'; import '../ui/auth/login/view_models/login_viewmodel.dart';
import '../ui/auth/login/widgets/login_screen.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/view_models/booking_viewmodel.dart';
import '../ui/booking/widgets/booking_screen.dart'; import '../ui/booking/widgets/booking_screen.dart';
import '../ui/home/view_models/home_viewmodel.dart'; import '../ui/home/view_models/home_viewmodel.dart';
@@ -46,7 +47,14 @@ GoRouter router(AuthRepository authRepository) => GoRouter(
bookingRepository: context.read(), bookingRepository: context.read(),
userRepository: 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: [ routes: [
GoRoute( GoRoute(

View File

@@ -7,6 +7,7 @@ import 'package:go_router/go_router.dart';
import '../../../domain/models/booking/booking_summary.dart'; import '../../../domain/models/booking/booking_summary.dart';
import '../../../routing/routes.dart'; import '../../../routing/routes.dart';
import '../../auth/logout/view_models/logout_viewmodel.dart';
import '../../core/localization/applocalization.dart'; import '../../core/localization/applocalization.dart';
import '../../core/themes/colors.dart'; import '../../core/themes/colors.dart';
import '../../core/themes/dimens.dart'; import '../../core/themes/dimens.dart';
@@ -18,9 +19,14 @@ import 'home_title.dart';
const String bookingButtonKey = 'booking-button'; const String bookingButtonKey = 'booking-button';
class HomeScreen extends StatefulWidget { 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 HomeViewModel viewModel;
final LogoutViewModel logoutViewModel;
@override @override
State<HomeScreen> createState() => _HomeScreenState(); State<HomeScreen> createState() => _HomeScreenState();
@@ -88,7 +94,10 @@ class _HomeScreenState extends State<HomeScreen> {
vertical: Dimens.of(context).paddingScreenVertical, vertical: Dimens.of(context).paddingScreenVertical,
horizontal: Dimens.of(context).paddingScreenHorizontal, horizontal: Dimens.of(context).paddingScreenHorizontal,
), ),
child: HomeHeader(viewModel: widget.viewModel), child: HomeHeader(
viewModel: widget.viewModel,
logoutViewModel: widget.logoutViewModel,
),
), ),
), ),
SliverList.builder( SliverList.builder(

View File

@@ -4,7 +4,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import 'package:provider/provider.dart';
import '../../auth/logout/view_models/logout_viewmodel.dart'; import '../../auth/logout/view_models/logout_viewmodel.dart';
import '../../auth/logout/widgets/logout_button.dart'; import '../../auth/logout/widgets/logout_button.dart';
@@ -13,9 +12,14 @@ import '../../core/themes/dimens.dart';
import '../view_models/home_viewmodel.dart'; import '../view_models/home_viewmodel.dart';
class HomeHeader extends StatelessWidget { 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 HomeViewModel viewModel;
final LogoutViewModel logoutViewModel;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -37,12 +41,7 @@ class HomeHeader extends StatelessWidget {
height: Dimens.of(context).profilePictureSize, height: Dimens.of(context).profilePictureSize,
), ),
), ),
LogoutButton( LogoutButton(viewModel: logoutViewModel),
viewModel: LogoutViewModel(
authRepository: context.read(),
itineraryConfigRepository: context.read(),
),
),
], ],
), ),
const SizedBox(height: Dimens.paddingVertical), const SizedBox(height: Dimens.paddingVertical),

View File

@@ -2,16 +2,14 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // 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/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/view_models/home_viewmodel.dart';
import 'package:compass_app/ui/home/widgets/home_screen.dart'; import 'package:compass_app/ui/home/widgets/home_screen.dart';
import 'package:compass_app/utils/result.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:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:provider/provider.dart';
import '../../../../testing/app.dart'; import '../../../../testing/app.dart';
import '../../../../testing/fakes/repositories/fake_auth_repository.dart'; import '../../../../testing/fakes/repositories/fake_auth_repository.dart';
@@ -24,6 +22,7 @@ import '../../../../testing/models/booking.dart';
void main() { void main() {
group('HomeScreen tests', () { group('HomeScreen tests', () {
late HomeViewModel viewModel; late HomeViewModel viewModel;
late LogoutViewModel logoutViewModel;
late MockGoRouter goRouter; late MockGoRouter goRouter;
late FakeBookingRepository bookingRepository; late FakeBookingRepository bookingRepository;
@@ -33,6 +32,10 @@ void main() {
bookingRepository: bookingRepository, bookingRepository: bookingRepository,
userRepository: FakeUserRepository(), userRepository: FakeUserRepository(),
); );
logoutViewModel = LogoutViewModel(
authRepository: FakeAuthRepository(),
itineraryConfigRepository: FakeItineraryConfigRepository(),
);
goRouter = MockGoRouter(); goRouter = MockGoRouter();
when(() => goRouter.push(any())).thenAnswer((_) => Future.value(null)); when(() => goRouter.push(any())).thenAnswer((_) => Future.value(null));
}); });
@@ -40,12 +43,9 @@ void main() {
Future<void> loadWidget(WidgetTester tester) async { Future<void> loadWidget(WidgetTester tester) async {
await testApp( await testApp(
tester, tester,
ChangeNotifierProvider.value( HomeScreen(
value: FakeAuthRepository() as AuthRepository, viewModel: viewModel,
child: Provider.value( logoutViewModel: logoutViewModel,
value: FakeItineraryConfigRepository() as ItineraryConfigRepository,
child: HomeScreen(viewModel: viewModel),
),
), ),
goRouter: goRouter, goRouter: goRouter,
); );

View File

@@ -13,25 +13,20 @@ class SheetPage extends StatelessWidget {
onPressed: () { onPressed: () {
Navigator.of(context).push( Navigator.of(context).push(
CupertinoSheetRoute<void>( CupertinoSheetRoute<void>(
scrollableBuilder: builder: (BuildContext context) {
(BuildContext context, ScrollController controller) { return CupertinoPageScaffold(
Widget widgetBuilder(BuildContext context) { navigationBar: CupertinoNavigationBar(
return CupertinoPageScaffold( middle: const Text('Sheet'),
navigationBar: CupertinoNavigationBar( trailing: GestureDetector(
middle: const Text('Sheet'), child: const Icon(CupertinoIcons.xmark),
trailing: GestureDetector( onTap: () {
child: const Icon(CupertinoIcons.xmark), Navigator.of(context).pop();
onTap: () { },
Navigator.of(context).pop(); ),
}, ),
), child: const Center(child: Text('This is a sheet')),
), );
child: const Center(child: Text('This is a sheet')), },
);
}
return widgetBuilder(context);
},
), ),
); );
}, },