Skip to content

Commit

Permalink
Merge pull request #28 from mrmilu/fex/dev-95-add-statefull-shell-route
Browse files Browse the repository at this point in the history
fix: dev-95 add statefull shell route
  • Loading branch information
hazzo authored Nov 10, 2023
2 parents e79ae20 + d068daa commit 0166d06
Show file tree
Hide file tree
Showing 11 changed files with 69 additions and 210 deletions.
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,9 @@ When create a new project, need complete this checklist

- [ ] Rename project and identifier id. To rename from flutter_base to another package name, change the pubspec.yml file and all the imports. Also if using Idea IDE's delete the .idea folder and in Project Structure... add a new root module to the project root so the IDE can detect the actual project.
- [ ] Search for all TODO comments and review and modify if necessary
- [ ] Create `env.flavor.json` per flavor with the following structure:
- [ ] For Dart compile-time variables create `env.flavor.json` per flavor with the following structure:

```json
// env.beta.json
{
"APP_NAME": "Flutter Base (beta)",
"APP_ID": "com.flutterbasemrmilu.beta",
Expand Down
2 changes: 1 addition & 1 deletion ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -304,4 +304,4 @@ SPEC CHECKSUMS:

PODFILE CHECKSUM: f028b8d13efc8d78499bf7351a167bbe4d5931b7

COCOAPODS: 1.11.3
COCOAPODS: 1.12.0
146 changes: 14 additions & 132 deletions lib/ui/features/misc/components/scaffold_with_navigation.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import 'package:flutter_base/ui/components/navigation/app_navigation_rail.dart';
import 'package:flutter_base/ui/components/views/base_adaptative_layout.dart';
import 'package:flutter_base/ui/styles/colors.dart';
import 'package:flutter_base/ui/styles/insets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';

class ScaffoldWithNavigationItem extends AppNavigationItem {
Expand Down Expand Up @@ -40,161 +39,44 @@ class ScaffoldWithNavigationItem extends AppNavigationItem {
}
}

class ScaffoldWithNavigation extends ConsumerStatefulWidget {
final Navigator currentNavigator;
class ScaffoldWithNavigation extends StatelessWidget {
final StatefulNavigationShell navigationShell;
final List<ScaffoldWithNavigationItem> tabItems;

List<Page> get pagesForCurrentRoute => currentNavigator.pages;

const ScaffoldWithNavigation({
required this.currentNavigator,
required this.navigationShell,
required this.tabItems,
super.key = const ValueKey<String>('ScaffoldWithNavBar'),
});

@override
ConsumerState<ConsumerStatefulWidget> createState() =>
ScaffoldWithNavBarState();
}

/// State for ScaffoldWithNavBar
class ScaffoldWithNavBarState extends ConsumerState<ScaffoldWithNavigation>
with SingleTickerProviderStateMixin {
late final AnimationController _animationController;
late final List<_TabPagesNavigator> _tabPages;
int _currentIndex = 0;

@override
void initState() {
super.initState();
_tabPages = widget.tabItems
.map((ScaffoldWithNavigationItem e) => _TabPagesNavigator(e))
.toList();

_animationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 400),
);
_animationController.forward();
}

int _locationToTabIndex(String location) {
final int index = _tabPages.indexWhere(
(_TabPagesNavigator t) => location.startsWith(t.rootRoutePath),
void _onTap(int index) {
navigationShell.goBranch(
index,
initialLocation: index == navigationShell.currentIndex,
);
return index < 0 ? 0 : index;
}

void _updateForCurrentTab() {
final int previousIndex = _currentIndex;
final location = GoRouter.of(context).location;
_currentIndex = _locationToTabIndex(location);

final _TabPagesNavigator tabNav = _tabPages[_currentIndex];
tabNav.pages = widget.pagesForCurrentRoute;
tabNav.lastLocation = location;

if (previousIndex != _currentIndex) {
_animationController.forward(from: 0.0);
}
}

void _onItemTapped(int index, BuildContext context) {
final tab = _tabPages[index];

GoRouter.of(context)
.go(_currentIndex == index ? tab.rootRoutePath : tab.currentLocation);
}

@override
void didUpdateWidget(covariant ScaffoldWithNavigation oldWidget) {
super.didUpdateWidget(oldWidget);
_updateForCurrentTab();
}

@override
void didChangeDependencies() {
super.didChangeDependencies();
_updateForCurrentTab();
}

@override
void dispose() {
_animationController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
final tabNavigators = _tabPages
.map(
(_TabPagesNavigator tab) => tab.buildNavigator(context),
)
.toList();

return SizedBox.expand(
child: ColoredBox(
color: FlutterBaseColors.specificBackgroundBase,
child: BaseAdaptativeLayout(
body: FadeTransition(
opacity: _animationController,
child: IndexedStack(
index: _currentIndex,
children: tabNavigators,
),
),
body: navigationShell,
navigationRail: AppNavigationRail(
items: widget.tabItems,
selectedIndex: _currentIndex,
onItemTapped: (int idx) => _onItemTapped(idx, context),
items: tabItems,
selectedIndex: navigationShell.currentIndex,
onItemTapped: _onTap,
),
bottomAppBar: Padding(
padding: Insets.a12,
child: AppNavigationBottomBar(
items: widget.tabItems,
selectedIndex: _currentIndex,
onItemTapped: (int idx) => _onItemTapped(idx, context),
items: tabItems,
selectedIndex: navigationShell.currentIndex,
onItemTapped: _onTap,
),
),
),
),
);
}
}

/// Class representing a tab along with its navigation logic
class _TabPagesNavigator {
final ScaffoldWithNavigationItem bottomNavigationTab;
List<Page> pages = <Page>[];
String lastLocation = '';
String get currentLocation =>
lastLocation.isNotEmpty && lastLocation.contains(rootRoutePath)
? lastLocation
: rootRoutePath;

String get rootRoutePath => bottomNavigationTab.rootRoutePath;

GlobalKey<NavigatorState>? get navigatorKey =>
bottomNavigationTab.navigatorKey;

_TabPagesNavigator(this.bottomNavigationTab);

Widget buildNavigator(BuildContext context) {
return pages.isNotEmpty
? ClipRect(
child: Navigator(
key: navigatorKey,
pages: pages,
clipBehavior: Clip.antiAlias,
onPopPage: (Route route, result) {
if (pages.length == 1 || !route.didPop(result)) {
return false;
}
GoRouter.of(context).pop();
return true;
},
),
)
: const SizedBox.shrink();
}
}
75 changes: 44 additions & 31 deletions lib/ui/router/app_router.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_base/ui/features/auth/views/change_password/change_password_page.dart';
import 'package:flutter_base/ui/features/auth/views/change_password/change_password_success_page.dart';
Expand All @@ -13,7 +14,6 @@ import 'package:flutter_base/ui/features/post/views/posts/post_page.dart';
import 'package:flutter_base/ui/features/profile/views/edit_avatar/edit_avatar_page.dart';
import 'package:flutter_base/ui/features/profile/views/edit_profile/edit_profile_page.dart';
import 'package:flutter_base/ui/features/profile/views/profile_page.dart';
import 'package:flutter_base/ui/router/bottom_tab_bar_shell_route.dart';
import 'package:flutter_base/ui/router/guards/auth_guard.dart';
import 'package:flutter_base/ui/router/utils.dart';
import 'package:go_router/go_router.dart';
Expand All @@ -33,8 +33,6 @@ final _bottomBarItems = [

final GlobalKey<NavigatorState> rootNavigatorKey =
GlobalKey<NavigatorState>(debugLabel: 'root');
final GlobalKey<NavigatorState> _appShellNavigatorKey =
GlobalKey<NavigatorState>(debugLabel: 'appShell');

final GoRouter router = GoRouter(
navigatorKey: rootNavigatorKey,
Expand Down Expand Up @@ -79,40 +77,55 @@ final GoRouter router = GoRouter(
),

/// Application
BottomTabBarShellRoute(
tabs: _bottomBarItems,
navigatorKey: _appShellNavigatorKey,
routes: <RouteBase>[
GoRoute(
redirect: authGuard,
path: '/profile',
pageBuilder: (BuildContext context, GoRouterState state) =>
fadeTransitionPage(state, const ProfilePage()),
routes: <RouteBase>[
GoRoute(
path: 'edit',
builder: (BuildContext context, GoRouterState state) =>
const EditProfilePage(),
),
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) {
return ScaffoldWithNavigation(
tabItems: _bottomBarItems
.map((tab) => tab.copyWith(text: tab.text.tr()))
.toList(),
navigationShell: navigationShell,
);
},
branches: <StatefulShellBranch>[
StatefulShellBranch(
routes: [
GoRoute(
path: 'avatar',
parentNavigatorKey: rootNavigatorKey,
builder: (context, state) => EditAvatarPage(
avatar: (state.extra as EditAvatarPageData).avatar,
),
redirect: authGuard,
path: '/profile',
pageBuilder: (BuildContext context, GoRouterState state) =>
fadeTransitionPage(state, const ProfilePage()),
routes: <RouteBase>[
GoRoute(
path: 'edit',
builder: (BuildContext context, GoRouterState state) =>
const EditProfilePage(),
),
GoRoute(
path: 'avatar',
parentNavigatorKey: rootNavigatorKey,
builder: (context, state) => EditAvatarPage(
avatar: (state.extra as EditAvatarPageData).avatar,
),
),
],
),
],
),
GoRoute(
redirect: authGuard,
path: '/home',
pageBuilder: (BuildContext context, GoRouterState state) =>
fadeTransitionPage(state, const PostPage()),
StatefulShellBranch(
routes: [
GoRoute(
path: ':id',
builder: (context, state) =>
DetailPostPage(id: int.parse(state.params['id'] ?? '')),
redirect: authGuard,
path: '/home',
pageBuilder: (BuildContext context, GoRouterState state) =>
fadeTransitionPage(state, const PostPage()),
routes: [
GoRoute(
path: ':id',
builder: (context, state) => DetailPostPage(
id: int.parse(state.pathParameters['id'] ?? ''),
),
),
],
),
],
),
Expand Down
33 changes: 0 additions & 33 deletions lib/ui/router/bottom_tab_bar_shell_route.dart

This file was deleted.

4 changes: 2 additions & 2 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -691,10 +691,10 @@ packages:
dependency: "direct main"
description:
name: go_router
sha256: "7f25c4bb371b74b6f32f74946707db4595fa66e68ddc7f0da4a4dafb18a21480"
sha256: b33a88c67816312597e5e0f5906c5139a0b9bd9bb137346e872c788da7af8ea0
url: "https://pub.dev"
source: hosted
version: "6.5.1"
version: "9.0.3"
google_identity_services_web:
dependency: transitive
description:
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ dependencies:
flutter_svg: 2.0.4
freezed_annotation: 2.2.0
get_it: 7.2.0
go_router: 6.5.1
go_router: 9.0.3
google_sign_in: 6.0.2
image: 4.0.15
image_editor: 1.3.0
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import 'package:flutter_base/core/auth/domain/interfaces/auth_repository.dart';
import 'package:flutter_base/ui/features/auth/views/forgot_password/forgot_password_confirm_page.dart';
import 'package:flutter_base/ui/features/auth/views/forgot_password/forgot_password_page.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:go_router/go_router.dart';
import 'package:mocktail/mocktail.dart';

import '../../../../helpers/expects.dart';
Expand Down Expand Up @@ -35,7 +34,6 @@ void main() {
await tester.pumpAndSettle();
await tester.tap(button);
await tester.pumpAndSettle();
expect(getIt<GoRouter>().location, '/forgot-password/confirm');
expect(find.byType(ForgotPasswordConfirmPage), findsOneWidget);
},
);
Expand Down
Loading

0 comments on commit 0166d06

Please sign in to comment.