From afd72372a1dfa132188fb7c2614d5d0cacdad49c Mon Sep 17 00:00:00 2001 From: Yellowtoast Date: Wed, 17 Jan 2024 15:20:29 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20[=EB=A9=94=EC=9D=B8]=20=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=ED=8E=98=EC=9D=B4=EC=A7=80=20Nested=20Navigation=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/app/router/app_router.dart | 65 +------ lib/app/router/app_router.g.dart | 2 +- .../scaffold_with_nested_navigation.dart | 164 ------------------ lib/features/root/root_view.dart | 17 -- lib/presentation/main/main_event.dart | 11 ++ lib/presentation/main/main_page.dart | 105 +++++++++++ .../community_view.dart} | 4 +- .../pages/sign_in/sign_in_page.dart | 8 +- .../main_bottom_navigation_provider.dart | 30 ++++ .../main_bottom_navigation_provider.g.dart | 128 ++++++++++++++ lib/presentation/widgets/base/base_page.dart | 162 +++++++++++++++++ .../widgets/base/base_statless_page.dart | 110 ++++++++++++ 12 files changed, 564 insertions(+), 242 deletions(-) delete mode 100644 lib/app/router/scaffold_with_nested_navigation.dart delete mode 100644 lib/features/root/root_view.dart create mode 100644 lib/presentation/main/main_event.dart create mode 100644 lib/presentation/main/main_page.dart rename lib/presentation/pages/{gather/gather_view.dart => community/community_view.dart} (65%) create mode 100644 lib/presentation/providers/main_bottom_navigation_provider.dart create mode 100644 lib/presentation/providers/main_bottom_navigation_provider.g.dart create mode 100644 lib/presentation/widgets/base/base_page.dart create mode 100644 lib/presentation/widgets/base/base_statless_page.dart diff --git a/lib/app/router/app_router.dart b/lib/app/router/app_router.dart index 07cc0bf..c83a56a 100644 --- a/lib/app/router/app_router.dart +++ b/lib/app/router/app_router.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:pets_next_door_flutter/app/router/scaffold_with_nested_navigation.dart'; +import 'package:pets_next_door_flutter/presentation/main/main_page.dart'; import 'package:pets_next_door_flutter/presentation/pages/chat/chat_view.dart'; -import 'package:pets_next_door_flutter/presentation/pages/gather/gather_view.dart'; +import 'package:pets_next_door_flutter/presentation/pages/community/community_view.dart'; import 'package:pets_next_door_flutter/presentation/pages/home/home_view.dart'; import 'package:pets_next_door_flutter/presentation/pages/my_info/profile_view.dart'; import 'package:pets_next_door_flutter/presentation/pages/pet/register_pet_page.dart'; @@ -26,7 +26,7 @@ enum AppRoute { signUp, phoneAuth, home, - gather, + community, chat, myInfo, profile, @@ -64,7 +64,6 @@ GoRouter goRouter(GoRouterRef ref) { child: SignInPage(), ), ), - GoRoute( path: AppRoute.signUp.path, name: AppRoute.signUp.name, @@ -73,7 +72,6 @@ GoRouter goRouter(GoRouterRef ref) { child: SignUpPage(), ), ), - GoRoute( path: AppRoute.registerPet.path, name: AppRoute.registerPet.name, @@ -82,25 +80,13 @@ GoRouter goRouter(GoRouterRef ref) { child: const RegisterPetPage(), ), ), - - // GoRoute( - // path: '/${AppRoute.registerPetDetail.name}', - // name: AppRoute.registerPetDetail.name, - // pageBuilder: (context, state) => MaterialPage( - // key: state.pageKey, - // child: const RegisterPetInitialView(), - // ), - // ), - StatefulShellRoute.indexedStack( - builder: (context, state, navigationShell) { - return ScaffoldWithNestedNavigation(navigationShell: navigationShell); - }, + builder: (context, state, navigationShell) => + MainPage(navigationShell: navigationShell), branches: [ StatefulShellBranch( navigatorKey: _homeNavigatorKey, routes: [ - // Products GoRoute( path: AppRoute.home.path, name: AppRoute.home.name, @@ -108,29 +94,18 @@ GoRouter goRouter(GoRouterRef ref) { key: state.pageKey, child: const HomeView(), ), - // routes: [ - // GoRoute( - // path: 'login', - // name: AppRoute.login.name, - // pageBuilder: (context, state) => NoTransitionPage( - // key: state.pageKey, - // child: LoginView(), - // ), - // ) - // ], ), ], ), StatefulShellBranch( navigatorKey: _gatherNavigatorKey, routes: [ - // Shopping Cart GoRoute( - path: '/gather', - name: AppRoute.gather.name, + path: AppRoute.community.path, + name: AppRoute.community.name, pageBuilder: (context, state) => NoTransitionPage( key: state.pageKey, - child: GatherView(), + child: CommunityView(), ), ), ], @@ -138,9 +113,8 @@ GoRouter goRouter(GoRouterRef ref) { StatefulShellBranch( navigatorKey: _chatNavigatorKey, routes: [ - // Shopping Cart GoRoute( - path: '/chat', + path: AppRoute.chat.path, name: AppRoute.chat.name, pageBuilder: (context, state) => NoTransitionPage( key: state.pageKey, @@ -164,27 +138,6 @@ GoRouter goRouter(GoRouterRef ref) { ), ], ), - // GoRoute( - // path: '/login', - // name: AppRoute.login.name, - // pageBuilder: (context, state) => const NoTransitionPage( - // child: LoginView(), - // ), - // ), - // GoRoute( - // path: '/phone_auth', - // name: AppRoute.phoneAuth.name, - // pageBuilder: (context, state) => const NoTransitionPage( - // child: PhoneAuthView(), - // ), - // ), - // GoRoute( - // path: '/home', - // name: AppRoute.home.name, - // pageBuilder: (context, state) => const NoTransitionPage( - // child: HomeView(), - // ), - // ), ], ); } diff --git a/lib/app/router/app_router.g.dart b/lib/app/router/app_router.g.dart index dcbab2c..d44605c 100644 --- a/lib/app/router/app_router.g.dart +++ b/lib/app/router/app_router.g.dart @@ -6,7 +6,7 @@ part of 'app_router.dart'; // RiverpodGenerator // ************************************************************************** -String _$goRouterHash() => r'bad02855d9c11d0dec0d98b0c3f67fcc64c40183'; +String _$goRouterHash() => r'cb525f9f1c8b21a43c610aa8a164ec614ec9f4c0'; /// See also [goRouter]. @ProviderFor(goRouter) diff --git a/lib/app/router/scaffold_with_nested_navigation.dart b/lib/app/router/scaffold_with_nested_navigation.dart deleted file mode 100644 index c53e291..0000000 --- a/lib/app/router/scaffold_with_nested_navigation.dart +++ /dev/null @@ -1,164 +0,0 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:go_router/go_router.dart'; -import 'package:pets_next_door_flutter/app/router/app_router.dart'; -import 'package:pets_next_door_flutter/core/constants/svgs.dart'; -import 'package:pets_next_door_flutter/core/localization/string_hardcoded.dart'; -import 'package:pets_next_door_flutter/presentation/providers/user/user_auth_provider.dart'; - -// Stateful navigation based on: -// https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/stateful_shell_route.dart -class ScaffoldWithNestedNavigation extends StatelessWidget { - const ScaffoldWithNestedNavigation({ - required this.navigationShell, - Key? key, - }) : super( - key: key ?? const ValueKey('ScaffoldWithNestedNavigation'), - ); - final StatefulNavigationShell navigationShell; - - void _goBranch(int index) { - navigationShell.goBranch( - index, - // A common pattern when using bottom navigation bars is to support - // navigating to the initial location when tapping the item that is - // already active. This example demonstrates how to support this behavior, - // using the initialLocation parameter of goBranch. - initialLocation: index == navigationShell.currentIndex, - ); - } - - @override - Widget build(BuildContext context) { - final size = MediaQuery.sizeOf(context); - if (size.width < 450) { - return ScaffoldWithBottomNavBar( - body: navigationShell, - currentIndex: navigationShell.currentIndex, - onDestinationSelected: _goBranch, - ); - } else { - return ScaffoldWithNavigationRail( - body: navigationShell, - currentIndex: navigationShell.currentIndex, - onDestinationSelected: _goBranch, - ); - } - } -} - -class ScaffoldWithBottomNavBar extends StatelessWidget { - const ScaffoldWithBottomNavBar({ - required this.body, - required this.currentIndex, - required this.onDestinationSelected, - super.key, - }); - final Widget body; - final int currentIndex; - final ValueChanged onDestinationSelected; - - @override - Widget build(BuildContext context) { - return Scaffold( - body: body, - floatingActionButton: Consumer( - builder: (BuildContext context, WidgetRef ref, Widget? child) { - return FloatingActionButton(onPressed: () async { - final signOutSucceed = - await ref.read(userAuthProvider.notifier).signOut(); - - if (signOutSucceed) ref.context.goNamed(AppRoute.signIn.name); - }); - }, - ), - bottomNavigationBar: SizedBox( - height: 60, - child: Wrap(children: [ - BottomNavigationBar( - elevation: 10, - iconSize: 32, - type: BottomNavigationBarType.fixed, - showSelectedLabels: false, - showUnselectedLabels: false, - selectedItemColor: Colors.amber, - unselectedItemColor: Colors.blueGrey.shade100, - currentIndex: currentIndex, - items: [ - // products - - BottomNavigationBarItem( - icon: SvgPicture.asset( - PNDSvgs.home, - ), - activeIcon: SvgPicture.asset( - PNDSvgs.home, - color: Color(0xffFF8B00), - ), - label: '', - ), - BottomNavigationBarItem( - icon: Icon(Icons.view_headline_outlined), label: ''), - BottomNavigationBarItem( - icon: Icon(Icons.view_headline_outlined), label: ''), - BottomNavigationBarItem( - icon: Icon(Icons.view_headline_outlined), label: ''), - ], - onTap: onDestinationSelected, - ), - ]), - ), - ); - } -} - -class ScaffoldWithNavigationRail extends StatelessWidget { - const ScaffoldWithNavigationRail({ - super.key, - required this.body, - required this.currentIndex, - required this.onDestinationSelected, - }); - final Widget body; - final int currentIndex; - final ValueChanged onDestinationSelected; - - @override - Widget build(BuildContext context) { - return Scaffold( - body: Row( - children: [ - NavigationRail( - selectedIndex: currentIndex, - onDestinationSelected: onDestinationSelected, - labelType: NavigationRailLabelType.all, - destinations: [ - NavigationRailDestination( - icon: const Icon(Icons.work_outline), - selectedIcon: const Icon(Icons.work), - label: Text('Jobs'.hardcoded), - ), - NavigationRailDestination( - icon: const Icon(Icons.view_headline_outlined), - selectedIcon: const Icon(Icons.view_headline), - label: Text('Entries'.hardcoded), - ), - NavigationRailDestination( - icon: const Icon(Icons.person_outline), - selectedIcon: const Icon(Icons.person), - label: Text('Account'.hardcoded), - ), - ], - ), - const VerticalDivider(thickness: 1, width: 1), - // This is the main content. - Expanded( - child: body, - ), - ], - ), - ); - } -} diff --git a/lib/features/root/root_view.dart b/lib/features/root/root_view.dart deleted file mode 100644 index f101334..0000000 --- a/lib/features/root/root_view.dart +++ /dev/null @@ -1,17 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:pets_next_door_flutter/app/router/scaffold_with_nested_navigation.dart'; - -class RootView extends StatelessWidget { - const RootView({super.key}); - - @override - Widget build(BuildContext context) { - return ScaffoldWithBottomNavBar( - body: SafeArea( - child: SizedBox.shrink(), - ), - currentIndex: 0, - onDestinationSelected: (int value) {}, - ); - } -} diff --git a/lib/presentation/main/main_event.dart b/lib/presentation/main/main_event.dart new file mode 100644 index 0000000..6c848ae --- /dev/null +++ b/lib/presentation/main/main_event.dart @@ -0,0 +1,11 @@ +import 'package:go_router/go_router.dart'; + +mixin class MainEvent { + void onTapBottomNavigationItem( + {required int index, required StatefulNavigationShell navigationShell}) { + navigationShell.goBranch( + index, + initialLocation: navigationShell.currentIndex == index, + ); + } +} diff --git a/lib/presentation/main/main_page.dart b/lib/presentation/main/main_page.dart new file mode 100644 index 0000000..65197ec --- /dev/null +++ b/lib/presentation/main/main_page.dart @@ -0,0 +1,105 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:pets_next_door_flutter/app/router/app_router.dart'; +import 'package:pets_next_door_flutter/core/constants/colors.dart'; +import 'package:pets_next_door_flutter/presentation/main/main_event.dart'; +import 'package:pets_next_door_flutter/presentation/providers/main_bottom_navigation_provider.dart'; +import 'package:pets_next_door_flutter/presentation/providers/user/user_auth_provider.dart'; +import 'package:pets_next_door_flutter/presentation/widgets/base/base_page.dart'; + +class MainPage extends BasePage with MainEvent { + const MainPage({super.key, required this.navigationShell}); + + final StatefulNavigationShell navigationShell; + + @override + bool get wrapWithSafeArea => true; + + @override + Widget buildPage(BuildContext context, WidgetRef ref) { + return Scaffold( + body: navigationShell, + ); + } + + @override + Widget buildBottomNavigationBar(BuildContext context) => _BottomNavigationBar( + currentTab: MainNavigationTab.values[navigationShell.currentIndex], + onTapBottomNavigationItem: (index) => onTapBottomNavigationItem( + index: index, + navigationShell: navigationShell, + ), + ); + + @override + Widget? buildFloatingActionButton(BuildContext context) => + const _FloatingActionButton(); +} + +class _FloatingActionButton extends ConsumerWidget { + const _FloatingActionButton({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return FloatingActionButton( + onPressed: () async { + final signOutSucceed = + await ref.read(userAuthProvider.notifier).signOut(); + + if (signOutSucceed) ref.context.goNamed(AppRoute.signIn.name); + }, + child: Text('로그아웃'), + ); + } +} + +class _BottomNavigationBar extends ConsumerWidget { + const _BottomNavigationBar({ + super.key, + required this.currentTab, + required this.onTapBottomNavigationItem, + }); + + final MainNavigationTab currentTab; + final void Function(int) onTapBottomNavigationItem; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return SizedBox( + height: 60, + child: Wrap( + children: [ + BottomNavigationBar( + currentIndex: currentTab.index, + backgroundColor: Colors.white, + type: BottomNavigationBarType.fixed, + showSelectedLabels: false, + showUnselectedLabels: false, + selectedItemColor: AppColor.of.primaryGreen, + unselectedItemColor: AppColor.of.gray50, + onTap: onTapBottomNavigationItem, + items: [ + ...MainNavigationTab.values.mapIndexed( + (index, e) => BottomNavigationBarItem( + label: e.label, + icon: SvgPicture.asset( + e.iconPath, + colorFilter: ColorFilter.mode( + currentTab.index == index + ? AppColor.of.primaryGreen + : AppColor.of.gray50, + BlendMode.srcIn, + ), + ), + ), + ), + ], + ), + ], + ), + ); + } +} diff --git a/lib/presentation/pages/gather/gather_view.dart b/lib/presentation/pages/community/community_view.dart similarity index 65% rename from lib/presentation/pages/gather/gather_view.dart rename to lib/presentation/pages/community/community_view.dart index 5297927..9821103 100644 --- a/lib/presentation/pages/gather/gather_view.dart +++ b/lib/presentation/pages/community/community_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -class GatherView extends StatelessWidget { - const GatherView({super.key}); +class CommunityView extends StatelessWidget { + const CommunityView({super.key}); @override Widget build(BuildContext context) { diff --git a/lib/presentation/pages/sign_in/sign_in_page.dart b/lib/presentation/pages/sign_in/sign_in_page.dart index a9af3fe..93c9978 100644 --- a/lib/presentation/pages/sign_in/sign_in_page.dart +++ b/lib/presentation/pages/sign_in/sign_in_page.dart @@ -7,12 +7,16 @@ import 'package:pets_next_door_flutter/core/constants/svgs.dart'; import 'package:pets_next_door_flutter/presentation/pages/sign_in/widgets/start_with_apple_button.dart'; import 'package:pets_next_door_flutter/presentation/pages/sign_in/widgets/start_with_google_button.dart'; import 'package:pets_next_door_flutter/presentation/pages/sign_in/widgets/start_with_kakao_button.dart'; +import 'package:pets_next_door_flutter/presentation/widgets/base/base_statless_page.dart'; -class SignInPage extends StatelessWidget { +class SignInPage extends BaseStatelessWidget { const SignInPage({super.key}); @override - Widget build(BuildContext context) { + bool get preventAutoUnfocus => false; + + @override + Widget buildPage(BuildContext context) { return Scaffold( backgroundColor: Colors.white, body: Consumer( diff --git a/lib/presentation/providers/main_bottom_navigation_provider.dart b/lib/presentation/providers/main_bottom_navigation_provider.dart new file mode 100644 index 0000000..2beef96 --- /dev/null +++ b/lib/presentation/providers/main_bottom_navigation_provider.dart @@ -0,0 +1,30 @@ +import 'package:go_router/go_router.dart'; +import 'package:pets_next_door_flutter/core/constants/svgs.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'main_bottom_navigation_provider.g.dart'; + +enum MainNavigationTab { + home('홈', PNDSvgs.home), + community('모임', PNDSvgs.community), + chat('채팅', PNDSvgs.chat), + myInfo('내 정보', PNDSvgs.user); + + final String label; + final String iconPath; + + const MainNavigationTab( + this.label, + this.iconPath, + ); +} + +@Riverpod(keepAlive: true) +class MainBottomNavigation extends _$MainBottomNavigation { + @override + MainNavigationTab build(StatefulNavigationShell shell) { + return MainNavigationTab.values[shell.currentIndex]; + } + + set tab(MainNavigationTab value) => state = value; +} diff --git a/lib/presentation/providers/main_bottom_navigation_provider.g.dart b/lib/presentation/providers/main_bottom_navigation_provider.g.dart new file mode 100644 index 0000000..51e0836 --- /dev/null +++ b/lib/presentation/providers/main_bottom_navigation_provider.g.dart @@ -0,0 +1,128 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'main_bottom_navigation_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$mainBottomNavigationHash() => + r'd4bf07b2dfcd303b88483cd8758c1ee85d83e329'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$MainBottomNavigation + extends BuildlessNotifier { + late final StatefulNavigationShell shell; + + MainNavigationTab build( + StatefulNavigationShell shell, + ); +} + +/// See also [MainBottomNavigation]. +@ProviderFor(MainBottomNavigation) +const mainBottomNavigationProvider = MainBottomNavigationFamily(); + +/// See also [MainBottomNavigation]. +class MainBottomNavigationFamily extends Family { + /// See also [MainBottomNavigation]. + const MainBottomNavigationFamily(); + + /// See also [MainBottomNavigation]. + MainBottomNavigationProvider call( + StatefulNavigationShell shell, + ) { + return MainBottomNavigationProvider( + shell, + ); + } + + @override + MainBottomNavigationProvider getProviderOverride( + covariant MainBottomNavigationProvider provider, + ) { + return call( + provider.shell, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'mainBottomNavigationProvider'; +} + +/// See also [MainBottomNavigation]. +class MainBottomNavigationProvider + extends NotifierProviderImpl { + /// See also [MainBottomNavigation]. + MainBottomNavigationProvider( + this.shell, + ) : super.internal( + () => MainBottomNavigation()..shell = shell, + from: mainBottomNavigationProvider, + name: r'mainBottomNavigationProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$mainBottomNavigationHash, + dependencies: MainBottomNavigationFamily._dependencies, + allTransitiveDependencies: + MainBottomNavigationFamily._allTransitiveDependencies, + ); + + final StatefulNavigationShell shell; + + @override + bool operator ==(Object other) { + return other is MainBottomNavigationProvider && other.shell == shell; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, shell.hashCode); + + return _SystemHash.finish(hash); + } + + @override + MainNavigationTab runNotifierBuild( + covariant MainBottomNavigation notifier, + ) { + return notifier.build( + shell, + ); + } +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member diff --git a/lib/presentation/widgets/base/base_page.dart b/lib/presentation/widgets/base/base_page.dart new file mode 100644 index 0000000..3f2b79a --- /dev/null +++ b/lib/presentation/widgets/base/base_page.dart @@ -0,0 +1,162 @@ +import 'package:cupertino_will_pop_scope/cupertino_will_pop_scope.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:pets_next_door_flutter/core/constants/colors.dart'; + +/// +/// 앱의 화면 페이지를 생성하는 유틸리티 클래스 +/// [HookConsumerWidget]을 상속하여 hook과 WidetRef로직에 접근할 수 있음 +/// +abstract class BasePage extends HookConsumerWidget { + const BasePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context, WidgetRef ref) { + /// 페이지의 초기화 및 해제를 처리 + useEffect(() { + onInit(ref); + return () => onDispose(ref); + }); + + /// 앱의 라이플 사이클 변화를 처리 + useOnAppLifecycleStateChange((previousState, state) { + switch (state) { + case AppLifecycleState.resumed: + onResumed(ref); + break; + case AppLifecycleState.paused: + onPaused(ref); + break; + case AppLifecycleState.inactive: + onInactive(ref); + break; + case AppLifecycleState.detached: + onDetached(ref); + break; + case AppLifecycleState.hidden: + // TODO: Handle this case. + } + }); + + /// + /// Swipe Back 제스처 이벤트를 관리 + /// [preventSwipeBack]의 속성 값은 통해 + /// 플랫폼별 Swipe Back 제스쳐 작동 여부를 설정할 수 있음. + /// + return ConditionalWillPopScope( + shouldAddCallback: preventSwipeBack, + onWillPop: () async { + return false; + }, + child: GestureDetector( + onTap: !preventAutoUnfocus + ? () => FocusManager.instance.primaryFocus?.unfocus() + : null, + child: Container( + color: unSafeAreaColor, + child: wrapWithSafeArea + ? SafeArea( + top: setTopSafeArea, + bottom: setBottomSafeArea, + child: _buildScaffold(context, ref), + ) + : _buildScaffold(context, ref), + ), + ), + ); + } + + Widget _buildScaffold(BuildContext context, WidgetRef ref) { + return Scaffold( + extendBody: extendBodyBehindAppBar, + resizeToAvoidBottomInset: resizeToAvoidBottomInset, + appBar: buildAppBar(context, ref), + body: buildPage(context, ref), + backgroundColor: screenBackgroundColor, + bottomNavigationBar: buildBottomNavigationBar(context), + floatingActionButtonLocation: floatingActionButtonLocation, + floatingActionButton: buildFloatingActionButton(context), + ); + } + + /// 하단 네비게이션 바를 구성하는 위젯을 반환 + @protected + Widget? buildBottomNavigationBar(BuildContext context) => null; + + /// 화면 페이지의 본문을 구성하는 위젯을 반환 + @protected + Widget buildPage(BuildContext context, WidgetRef ref); + + /// 화면 상단에 표시될 앱 바를 구성하는 위젯을 반환 + @protected + PreferredSizeWidget? buildAppBar(BuildContext context, WidgetRef ref) => null; + + /// 화면에 표시될 플로팅 액션 버튼을 구성하는 위젯을 반환 + @protected + Widget? buildFloatingActionButton(BuildContext context) => null; + + /// 뷰의 안전 영역 밖의 배경색을 설정 + @protected + Color? get unSafeAreaColor => AppColor.of.white; + + /// 키보드가 화면 하단에 올라왔을 때 페이지의 크기를 조정하는 여부를 설정 + @protected + bool get resizeToAvoidBottomInset => true; + + /// 플로팅 액션 버튼의 위치를 설정 + @protected + FloatingActionButtonLocation? get floatingActionButtonLocation => null; + + /// 앱 바 아래의 콘텐츠가 앱 바 뒤로 표시되는지 여부를 설정 + @protected + bool get extendBodyBehindAppBar => false; + + /// Swipe Back 제스처 동작을 막는지 여부를 설정 + @protected + bool get preventSwipeBack => false; + + /// 화면의 배경색을 설정 + @protected + Color? get screenBackgroundColor => AppColor.of.white; + + /// SafeArea로 감싸는 여부를 설정 + @protected + bool get wrapWithSafeArea => true; + + /// 뷰의 안전 영역 아래에 SafeArea를 적용할지 여부를 설정 + @protected + bool get setBottomSafeArea => true; + + /// 뷰의 안전 영역 위에 SafeArea를 적용할지 여부를 설정 + @protected + bool get setTopSafeArea => true; + + /// 화면 클릭 시 자동으로 포커스를 해제할지 여부를 설정 + @protected + bool get preventAutoUnfocus => false; + + /// 앱이 활성화된 상태로 돌아올 때 호출 + @protected + void onResumed(WidgetRef ref) {} + + /// 앱이 일시 정지될 때 호출 + @protected + void onPaused(WidgetRef ref) {} + + /// 앱이 비활성 상태로 전환될 때 호출 + @protected + void onInactive(WidgetRef ref) {} + + /// 앱이 분리되었을 때 호출 + @protected + void onDetached(WidgetRef ref) {} + + /// 페이지 초기화 시 호출 + @protected + void onInit(WidgetRef ref) {} + + /// 페이지 해제 시 호출 + @protected + void onDispose(WidgetRef ref) {} +} diff --git a/lib/presentation/widgets/base/base_statless_page.dart b/lib/presentation/widgets/base/base_statless_page.dart new file mode 100644 index 0000000..aedbb38 --- /dev/null +++ b/lib/presentation/widgets/base/base_statless_page.dart @@ -0,0 +1,110 @@ +import 'package:cupertino_will_pop_scope/cupertino_will_pop_scope.dart'; +import 'package:flutter/material.dart'; +import 'package:pets_next_door_flutter/core/constants/colors.dart'; + +/// +/// 앱의 화면 페이지를 생성하는 유틸리티 클래스 +/// + +abstract class BaseStatelessWidget extends StatelessWidget { + const BaseStatelessWidget({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + /// + /// Swipe Back 제스처 이벤트를 관리 + /// [preventSwipeBack]의 속성 값은 통해 + /// 플랫폼별 Swipe Back 제스쳐 작동 여부를 설정할 수 있음. + /// + return ConditionalWillPopScope( + shouldAddCallback: preventSwipeBack, + onWillPop: () async { + return false; + }, + child: GestureDetector( + onTap: !preventAutoUnfocus + ? () => FocusManager.instance.primaryFocus?.unfocus() + : null, + child: Container( + color: unSafeAreaColor, + child: wrapWithSafeArea + ? SafeArea( + top: setTopSafeArea, + bottom: setBottomSafeArea, + child: _buildScaffold(context), + ) + : _buildScaffold(context), + ), + ), + ); + } + + Widget _buildScaffold(BuildContext context) { + return Scaffold( + extendBody: extendBodyBehindAppBar, + resizeToAvoidBottomInset: resizeToAvoidBottomInset, + appBar: buildAppBar(context), + body: buildPage(context), + backgroundColor: screenBackgroundColor, + bottomNavigationBar: buildBottomNavigationBar(context), + floatingActionButtonLocation: floatingActionButtonLocation, + floatingActionButton: buildFloatingActionButton(context), + ); + } + + /// 하단 네비게이션 바를 구성하는 위젯을 반환 + @protected + Widget? buildBottomNavigationBar(BuildContext context) => null; + + /// 화면 페이지의 본문을 구성하는 위젯을 반환 + @protected + Widget buildPage(BuildContext context); + + /// 화면 상단에 표시될 앱 바를 구성하는 위젯을 반환 + @protected + PreferredSizeWidget? buildAppBar(BuildContext context) => null; + + /// 화면에 표시될 플로팅 액션 버튼을 구성하는 위젯을 반환 + @protected + Widget? buildFloatingActionButton(BuildContext context) => null; + + /// 뷰의 안전 영역 밖의 배경색을 설정 + @protected + Color? get unSafeAreaColor => AppColor.of.white; + + /// 키보드가 화면 하단에 올라왔을 때 페이지의 크기를 조정하는 여부를 설정 + @protected + bool get resizeToAvoidBottomInset => true; + + /// 플로팅 액션 버튼의 위치를 설정 + @protected + FloatingActionButtonLocation? get floatingActionButtonLocation => null; + + /// 앱 바 아래의 콘텐츠가 앱 바 뒤로 표시되는지 여부를 설정 + @protected + bool get extendBodyBehindAppBar => false; + + /// Swipe Back 제스처 동작을 막는지 여부를 설정 + @protected + bool get preventSwipeBack => false; + + /// 화면의 배경색을 설정 + @protected + Color? get screenBackgroundColor => AppColor.of.white; + + /// SafeArea로 감싸는 여부를 설정 + @protected + bool get wrapWithSafeArea => true; + + /// 뷰의 안전 영역 아래에 SafeArea를 적용할지 여부를 설정 + @protected + bool get setBottomSafeArea => true; + + /// 뷰의 안전 영역 위에 SafeArea를 적용할지 여부를 설정 + @protected + bool get setTopSafeArea => true; + + /// 화면 클릭 시 자동으로 포커스를 해제할지 여부를 설정 + @protected + bool get preventAutoUnfocus => false; +}