HOME

2024.03

FCM에러

Future<void> updateFCMTokenIfNeeded(int userId) async { if (!kIsWeb) { try { final token = await PushNotificationService.getToken(); log('FCM token: $token'); if (token != null) { await fcmApi.updateUserFCMToken(userId, {'token': token}); } } catch (e) { log('Error updating FCM token: $e'); } } }
[log] 화면 이름 [log] FCM token: fyy45dBcQZimy2DZ592WJl:APA91bHunfBlnwUZVAMR7yZxKofca5w7fetejXPdz9bz0e7NQw1mktZQeSyZg1mRuQuhNVVOT_4Fsm_-3RONX3Jq2AzBwnbZLfemqDCGR3qVRjBiJlIeEZvt_8TUHhTwG_6rorfriHjc [log] [REQ 시작] https://dev.gocho-back.com/v1/users/46498/fcm-token [log] [REQ 종료] {Content-Type: application/json, requires-token: true, x-access-token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJzdWIiOiI0NjQ5OCIsImF1ZCI6IlVTRVIiLCJpYXQiOjE3MTA5OTM3NDQsImV4cCI6MTcxMDk5NDM0NCwiaXNzIjoiS0FLQU8ifQ.xT8UNiGNw-p_1vfY1eFfWZeCCUwq7TBmtzfyFtg0vNHt83NiM_-hIQ7QzlCKRexAOOSmmjSr3gwW6bh6qKqLzg} I/flutter ( 5943): *** Response *** I/flutter ( 5943): uri: https://dev.gocho-back.com/v1/users/46498/fcm-token I/flutter ( 5943): Response Text: I/flutter ( 5943): null I/flutter ( 5943): [log] [RES] [POST] https://dev.gocho-back.com/v1/users/46498/fcm-token null [log] Error updating FCM token: Null check operator used on a null value [log] [Route Provider] redirect: /
redirect가 null일때 /로 보내기 때문
goNamed redirect

GoNamed InitState

StatefulShellRoute로 바꾸자

개요

Error

invalid value: only valid value is 0: 1
첫번쨰 탭을 안누르고 다음 탭으로 넘어가면 해당 에러가 발생 합니다.
처음 [log] [Route Provider] redirect: /home 로 들어오고나서 home 탭을 누르고 난 후 다른 탭을 누르고 home탭을 누르면 정상작동하지만, 바로 다른 탭을 누르면 해당 에러가 발생합니다
의심 되는 부분
Future<void> _navigateToMainApp() async { await Future.delayed(const Duration(seconds: 2), () { context.pushReplacement('/home'); }); }
Dart
복사
splash에서 pushReplacement 하는 부분, 이 부분을 변경하기 위해
splash로 pushReplacement가 생기면서 조금 이상해진 부분이 있다.
알림 설정 모달이 두번 뜸.
rootNavigatorKey 을 같은 Key로 할당해준다.
GoRoute( parentNavigatorKey: rootNavigatorKey, path: SplashScreen.routePath, name: SplashScreen.routeName, builder: (_, __) => const SplashScreen(), ), StatefulShellRoute.indexedStack( parentNavigatorKey: rootNavigatorKey, builder: (BuildContext context, GoRouterState state, StatefulNavigationShell navigationShell) { return BottomNavigationView( navigationShell: navigationShell, ); },
Dart
복사
push로 변경했으나, Stack에 splash가 남아 있음.
다른 Key로 할당해준다.
final GlobalKey<NavigatorState> rootNavigatorKey = GlobalKey<NavigatorState>(debugLabel: 'root');
Dart
복사
StatefulShellRoute.indexedStack 로 pushReplacement가 안돼는 문제
StatefulShellRoute.indexedStack을 사용하여, 각각 별도의 Navigator를 사용하는 다른 상태 유지 라우트 분기들을 관리할 수 있습니다. 이는 예를 들어, BottomNavigationBar와 같이 화면 간 이동 시에도 화면에 남아 있는 UI 요소와 함께 중첩된 네비게이션을 구현할 때 유용합니다. StatefulShellRoute는 상태 유지 중첩 네비게이션을 가능하게 하며, 각 탭이나 섹션 간에 독립적인 네비게이션 상태를 유지합니다.
이 클래스는 StatefulNavigationShell 위젯을 사용하여 일치하는 하위 라우트들의 중첩된 네비게이션을 관리합니다. 이것은 일반적으로 쉘 라우트가 이 위젯 주위에 자신의 쉘을 구축할 때 사용됩니다. 또한, 어떤 분기가 활성화되어 있는지에 대한 정보에 접근하고, 다른 분기로 네비게이션하기 위해 StatefulNavigationShell.goBranch를 사용할 수 있습니다.
builderpageBuilder는 모두 StatefulNavigationShell 파라미터를 추가로 가지며, 이는 커스텀 구현에서 StatefulNavigationShell을 사용하여 분기 네비게이터들을 위한 커스텀 컨테이너를 생성할 수 있음을 의미합니다.
navigatorContainerBuilder는 분기 네비게이터들을 위한 컨테이너를 구축하는 함수로, 분기 네비게이터들을 나타내는 위젯들의 리스트에 접근을 제공합니다. 이 함수는 분기 위젯들의 상태가 유지되도록, 예를 들어 위젯 트리에 포함시키는 위젯을 반환할 것으로 예상됩니다.
각 분기는 별도의 Navigator를 사용하며, StatefulShellBranch.navigatorKey로 식별됩니다. StatefulShellBranch를 생성할 때는 하위 라우트들(routes)을 제공해야 하며, 초기 위치(initialLocation)를 제공하는 것이 편리할 수도 있습니다. 초기 위치는 분기를 처음 로딩할 때 사용됩니다(예: StatefulNavigationShellgoBranch 메소드를 사용하여 분기를 전환할 때).
이 구성을 통해 개발자는 상태 유지 중첩 네비게이션을 가진 복잡한 앱 구조를 효율적으로 구축할 수 있으며, 사용자 경험을 향상시킬 수 있습니다.
문제의 원인은 StatefulShellRoute.indexedStack를 사용할 때, 각 탭 간의 네비게이션 상태를 올바르게 관리하지 않아 발생하는 것으로 보입니다. StatefulShellRoute.indexedStack는 각각의 탭이 별도의 네비게이터를 사용하도록 설계되어 있으며, 이는 각 탭의 상태를 독립적으로 유지할 수 있게 합니다. 그러나, 첫 번째 탭을 선택하지 않고 다른 탭으로 직접 이동할 때 발생하는 에러는 탭의 초기 상태가 올바르게 설정되지 않았음을 나타냅니다.
여기에 몇 가지 해결 방안을 제시합니다:
1.
초기 탭 설정 확인: 앱이 시작될 때, 적절한 탭이 선택되어 있는지 확인해야 합니다. StatefulShellRoute.indexedStackinitialLocation을 명시적으로 설정하여 앱이 시작할 때 첫 번째 탭이 활성화되도록 할 수 있습니다. 이는 사용자가 첫 번째 탭을 수동으로 선택하지 않았을 때, 기본 탭으로 돌아가는 것을 방지합니다.
2.
상태 복원: StatefulShellRoute.indexedStackrestorationScopeId를 설정하여 각 탭의 네비게이션 상태를 복원할 수 있습니다. 이렇게 하면 앱이 백그라운드에서 복귀했을 때, 사용자가 마지막에 보았던 탭으로 자동으로 돌아갈 수 있습니다. 상태 복원을 사용하면 앱의 사용성이 향상될 수 있습니다.
3.
네비게이션 키 관리: rootNavigatorKey와 각 탭의 navigatorKey를 명확히 구분하여 사용합니다. 각 탭에 대해 별도의 GlobalKey<NavigatorState>를 할당하여 각 탭의 네비게이션 상태가 서로 영향을 받지 않도록 합니다. 이는 탭 간의 독립적인 네비게이션 상태를 유지하는 데 중요합니다.
4.
에러 핸들링 추가: 네비게이션 동작 중 발생할 수 있는 예외를 적절히 처리합니다. 예를 들어, 사용자가 첫 번째 탭을 선택하지 않고 바로 다른 탭으로 이동할 때 발생할 수 있는 에러를 감지하고, 사용자에게 피드백을 제공하거나 자동으로 첫 번째 탭으로 리다이렉트할 수 있습니다.
5.
네비게이션 로직 개선: pushReplacement 대신 gopush를 사용하여 탭 간의 이동을 처리합니다. 이는 StatefulShellRoute.indexedStack 내에서 탭 간의 네비게이션 상태를 올바르게 관리하는 데 도움이 될 수 있습니다. 또한, 사용자가 탭을 변경할 때 마다 새로운 네비게이션 스택을 생성하는 것을 방지하여 앱의 네비게이션 로직을 간소화할 수 있습니다.
이러한 해결 방안들은 문제의 정확한 원인에 따라 달라질 수 있으며, 상황에 맞게 적절히 조정하여 적용해야 합니다. 문제를 해결하는 과정에서 디버깅 로그와 에러 메시지를 주의 깊게 분석하여, 구체적인 문제점을 파악하는 것이 중요합니다.
splash화면의 context.pushReplacement(’/home’)을 GoRouter.of(context).go('/home');로 변경하였더니 해결되었다..
context.pushReplacement('/home')GoRouter.of(context).go('/home') 사이의 차이는 주로 사용되는 네비게이션 컨텍스트와 네비게이션 스택에 대한 처리 방식에 있습니다. 각 메서드는 Flutter에서 페이지(라우트) 전환을 다루는 데 사용되지만, 동작 방식과 사용되는 네비게이션 모델에 차이가 있습니다.
1.
context.pushReplacement('/home'): 이 방식은 일반적으로 Navigator 위젯의 pushReplacement 메서드를 직접 호출하는 것과 유사합니다. pushReplacement 메서드는 현재 라우트(화면)를 새로운 라우트로 교체하는 역할을 합니다. 이 경우, 네비게이션 스택에서 현재 페이지가 제거되고 새 페이지가 스택의 같은 위치에 추가됩니다. 이 메서드는 기존의 네비게이션 스택을 수정하는 데 사용됩니다.
2.
GoRouter.of(context).go('/home'): GoRouter는 Flutter에서 사용할 수 있는 라우팅 라이브러리 중 하나로, 앱 전체의 네비게이션 상태를 관리하는 선언적 접근 방식을 제공합니다. GoRouter.of(context).go('/home') 메서드는 GoRouter를 사용하여 지정된 경로로 앱의 상태를 변경합니다. 이 경우, go 메서드는 앱의 현재 상태를 지정된 경로의 상태로 완전히 대체합니다. GoRouter는 네비게이션 이벤트를 처리하기 위해 앱의 상위에서 설정된 라우트 구성을 기반으로 동작합니다.

차이점 요약:

네비게이션 컨텍스트와 스택 관리: pushReplacement는 현재 네비게이터 스택에 직접적으로 영향을 미치는 반면, GoRouter.of(context).go('/home')GoRouter가 관리하는 전체 앱 상태를 기반으로 작동합니다.
선언적 접근 방식: GoRouter는 더 선언적이며, 라우트의 구성과 네비게이션 로직을 분리하여 관리할 수 있게 해줍니다. 이는 앱의 네비게이션 구조를 더 명확하게 만들고, 유지 보수를 용이하게 합니다.
사용성: GoRouter.of(context).go('/home')를 사용하면 GoRouter의 기능을 최대한 활용할 수 있습니다. 예를 들어, URL을 기반으로 하는 웹 앱에서 유용하게 사용할 수 있습니다.
결론적으로, GoRouter.of(context).go('/home')를 사용하면 GoRouter의 전체적인 네비게이션 관리 기능을 활용하여 앱의 네비게이션 상태를 보다 선언적이고 효율적으로 관리할 수 있습니다.

Go_router Deeplink

변경 요청 사항 모든 딥링크 스킴 통일 gocho://jd gocho://jd/1234
push와 pushNamed
그리고 go와 goNamed의 사용처를 분명히 할것
apple-app-site-association 설정하기
<!--<meta-data android:name="flutter_deeplinking_enabled" android:value="true" />-->
Dart
복사
<key>FlutterDeepLinkingEnabled</key> <true/>
Dart
복사
route.parentNavigatorKey == navigatorKey : sub-route's parent navigator key must either be null or has the same navigator key as parent's key

Fivelines 리팩토링

import 'package:flutter/material.dart'; import 'package:flutter_gocho_app/app/layout/default_layout.dart'; import 'package:flutter_gocho_app/app/utils/data_utils.dart'; import 'package:flutter_gocho_app/domain/model/company/company_model.dart'; import 'package:flutter_gocho_app/domain/model/jd/jd_model.dart'; import 'package:flutter_gocho_app/domain/model/jd/place_model.dart'; import 'package:flutter_gocho_app/presentation/company/detail/company_info/company_info_factory_section.dart'; import 'package:flutter_gocho_app/presentation/jd/detail/components/jd_detail_working_location.dart'; import 'package:flutter_gocho_app/presentation/jd/detail/section/jd_detail_submission_guide_section.dart'; import 'package:flutter_gocho_app/presentation/jd/item/jd_horizontal_list_item.dart'; import 'package:flutter_gocho_app/presentation/position/proposal/components/position_proposal_detail_title_and_content_widgets.dart'; import 'package:flutter_gocho_app/presentation/position/proposal/components/position_proposal_recruiter_info_view.dart'; import 'package:flutter_gocho_app/presentation/position/proposal/position_proposal_view_model.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:gocho_design_system/gocho_design_system.dart'; import 'package:sizer/sizer.dart'; class PositionProposalScreen extends ConsumerWidget { const PositionProposalScreen({super.key, required this.proposalId}); static const routeName = 'position-proposal'; static const routePath = 'position-proposal/:proposalId'; final String proposalId; Widget build(BuildContext context, WidgetRef ref) { final states = ref.watch(positionProposalViewModelProvider(proposalId)); return DefaultLayout( appBar: const GCAppBar( title: '포지션 제안서', ), child: states.when( loading: () => const Center(child: CircularProgressIndicator()), error: (error) => Center(child: Text(error)), loaded: (data) => Stack( children: [ SingleChildScrollView( child: Padding( padding: horizontal16, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ size16, JdHorizontalListItem( jd: JdModel( title: data.position?.title ?? '', company: CompanyModel( name: data.company?.name ?? '', logoUrl: data.company?.logoUrl ?? '', ), ), ), size20, StartAndEndDateView( startTitle: '제안 날짜', endTitle: responseString(data.status) ?? '', startTime: DataUtils.dateYearMonthDayWeekDaysWithCut( dateTimeString: data.createdTime, cut: false), endTime: DataUtils.dateYearMonthDayWeekDaysWithCut( dateTimeString: responseTime(data.status, data.responseTime, data.responsePeriod), cut: false), ), size20, Text( data.position?.content ?? '', style: GCTextStyle.body3R1422.copyWith(color: BLACK), ), size20, Container( padding: all16, width: SizerUtil.width, decoration: BoxDecoration( border: Border.all(color: GRAY_100), borderRadius: borderRadius8, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '제안내용', style: GCTextStyle.title5B1620.copyWith(color: BLACK), ), PositionProposalDetailTitleAndContentWidgets( title: '채용직무', content: Text( '${data.position?.mainTask} > ${data.position?.subTask.join(',')}', style: GCTextStyle.body3R1422.copyWith(color: BLACK), ), ), PositionProposalDetailTitleAndContentWidgets( title: '세부직무', content: Text( '${data.position?.taskDescription.join(',')}', style: GCTextStyle.body3R1422.copyWith(color: BLACK), ), ), PositionProposalDetailTitleAndContentWidgets( title: '계약형태', content: Text( '${data.position?.contractType}', style: GCTextStyle.body3R1422.copyWith(color: BLACK), ), ), PositionProposalDetailTitleAndContentWidgets( title: '학력', content: Text( '${data.position?.requiredEducation.join(',')}', style: GCTextStyle.body3R1422.copyWith(color: BLACK), ), ), PositionProposalDetailTitleAndContentWidgets( title: '우대사항', content: Text( '${data.position?.preferredEtc.join(',')}', style: GCTextStyle.body3R1422.copyWith(color: BLACK), ), ), PositionProposalDetailTitleAndContentWidgets( title: '우대자격증', content: Text( '${data.position?.preferredCertification.join(',')}', style: GCTextStyle.body3R1422.copyWith(color: BLACK), ), ), PositionProposalDetailTitleAndContentWidgets( title: '급여', content: Text( '${data.position?.pay.join(',')}', style: GCTextStyle.body3R1422.copyWith(color: BLACK), ), ), PositionProposalDetailTitleAndContentWidgets( title: '교대형태', content: Text( '${data.position?.shift.join(',')}', style: GCTextStyle.body3R1422.copyWith(color: BLACK), ), ), PositionProposalDetailTitleAndContentWidgets( title: '근무지', content: Text( '${data.position?.place.join(',')}\n${data.position?.placeEtc}', style: GCTextStyle.body3R1422.copyWith(color: BLACK), ), ), ], ), ), size20, PositionProposalRecruiterInfoView( manager: data.manager, ), size100, ], ), )), if (data.isClose) Align( alignment: Alignment.bottomCenter, child: GCBottomButton( title: '제안서가 마감되었습니다', onPressed: () {}, )), if (!data.isClose) Align( alignment: Alignment.bottomCenter, child: Container( padding: all16, child: SizedBox( width: SizerUtil.width, child: Row( children: [ GCBoxButton( defaultStyle: GCBoxButtonDefault.grayLine, width: (SizerUtil.width / 2) - 24, title: '거절', onPressed: () { ref .read(positionProposalViewModelProvider( proposalId) .notifier) .rejectProposal(); }), size16, GCBoxButton( defaultStyle: GCBoxButtonDefault.blueLine, width: (SizerUtil.width / 2) - 24, title: '수락', onPressed: () { ref .read(positionProposalViewModelProvider( proposalId) .notifier) .rejectProposal(); }) ], ), ), ), ), ], ), )); } String? responseString(String status) { switch (status) { case '승인': return '제안 수락'; case '거절': return '제안 거절'; case '제안': return '응답 기한'; } return null; } String? responseTime( String status, String? responseTime, String responsePeriod) { switch (status) { case '승인': return responseTime; case '거절': return responseTime; case '제안': return responsePeriod; } return null; } Color responseBorderColor(String status) { switch (status) { case '승인': return BLUE_300; case '거절': return RED_50; case '제안': return GRAY_100; } return GRAY_100; } Color responseTitleColor(String status) { switch (status) { case '승인': return BLUE_300; case '거절': return RED_50; case '제안': return BLACK; } return BLACK; } }
Dart
복사