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를 사용할 수 있습니다.
builder와 pageBuilder는 모두 StatefulNavigationShell 파라미터를 추가로 가지며, 이는 커스텀 구현에서 StatefulNavigationShell을 사용하여 분기 네비게이터들을 위한 커스텀 컨테이너를 생성할 수 있음을 의미합니다.
navigatorContainerBuilder는 분기 네비게이터들을 위한 컨테이너를 구축하는 함수로, 분기 네비게이터들을 나타내는 위젯들의 리스트에 접근을 제공합니다. 이 함수는 분기 위젯들의 상태가 유지되도록, 예를 들어 위젯 트리에 포함시키는 위젯을 반환할 것으로 예상됩니다.
각 분기는 별도의 Navigator를 사용하며, StatefulShellBranch.navigatorKey로 식별됩니다. StatefulShellBranch를 생성할 때는 하위 라우트들(routes)을 제공해야 하며, 초기 위치(initialLocation)를 제공하는 것이 편리할 수도 있습니다. 초기 위치는 분기를 처음 로딩할 때 사용됩니다(예: StatefulNavigationShell의 goBranch 메소드를 사용하여 분기를 전환할 때).
이 구성을 통해 개발자는 상태 유지 중첩 네비게이션을 가진 복잡한 앱 구조를 효율적으로 구축할 수 있으며, 사용자 경험을 향상시킬 수 있습니다.
문제의 원인은 StatefulShellRoute.indexedStack를 사용할 때, 각 탭 간의 네비게이션 상태를 올바르게 관리하지 않아 발생하는 것으로 보입니다. StatefulShellRoute.indexedStack는 각각의 탭이 별도의 네비게이터를 사용하도록 설계되어 있으며, 이는 각 탭의 상태를 독립적으로 유지할 수 있게 합니다. 그러나, 첫 번째 탭을 선택하지 않고 다른 탭으로 직접 이동할 때 발생하는 에러는 탭의 초기 상태가 올바르게 설정되지 않았음을 나타냅니다.
여기에 몇 가지 해결 방안을 제시합니다:
1.
초기 탭 설정 확인: 앱이 시작될 때, 적절한 탭이 선택되어 있는지 확인해야 합니다. StatefulShellRoute.indexedStack에 initialLocation을 명시적으로 설정하여 앱이 시작할 때 첫 번째 탭이 활성화되도록 할 수 있습니다. 이는 사용자가 첫 번째 탭을 수동으로 선택하지 않았을 때, 기본 탭으로 돌아가는 것을 방지합니다.
2.
상태 복원: StatefulShellRoute.indexedStack의 restorationScopeId를 설정하여 각 탭의 네비게이션 상태를 복원할 수 있습니다. 이렇게 하면 앱이 백그라운드에서 복귀했을 때, 사용자가 마지막에 보았던 탭으로 자동으로 돌아갈 수 있습니다. 상태 복원을 사용하면 앱의 사용성이 향상될 수 있습니다.
3.
네비게이션 키 관리: rootNavigatorKey와 각 탭의 navigatorKey를 명확히 구분하여 사용합니다. 각 탭에 대해 별도의 GlobalKey<NavigatorState>를 할당하여 각 탭의 네비게이션 상태가 서로 영향을 받지 않도록 합니다. 이는 탭 간의 독립적인 네비게이션 상태를 유지하는 데 중요합니다.
4.
에러 핸들링 추가: 네비게이션 동작 중 발생할 수 있는 예외를 적절히 처리합니다. 예를 들어, 사용자가 첫 번째 탭을 선택하지 않고 바로 다른 탭으로 이동할 때 발생할 수 있는 에러를 감지하고, 사용자에게 피드백을 제공하거나 자동으로 첫 번째 탭으로 리다이렉트할 수 있습니다.
5.
네비게이션 로직 개선: pushReplacement 대신 go나 push를 사용하여 탭 간의 이동을 처리합니다. 이는 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
복사