미해결
[코드팩토리] [초급] Flutter 3.0 앱 개발 - 10개의 프로젝트로 오늘 초보 탈출!
[문제해결] '오늘도 출첵!' 의 171번 강의에서 중요한 문제를 발견했습니다
강의 따라서 프로젝트를 진행하다가 '출근 가능/불가능 확인 로직'에서 문제를 발견했습니다. 이것저것 시도하다가 드디어 해결해서 과정 공유해봅니다.기존 코드 /// 기존의 initState()
@override
void initState() {
super.initState();
// (아마도) 이 시점에서 오류 발생 ↓
Geolocator.getPositionStream().listen((eventPosition) {
// 목적지 위치
var start = defaultLatLng;
// 현재 위치
var end = LatLng(eventPosition.latitude, eventPosition.longitude);
/// 거리 구하기
var distance = Geolocator.distanceBetween(
start.latitude,
start.longitude,
end.latitude,
end.longitude,
);
/// 거리가 `boundary` 내부에 들어오는지 확인
setState(() => mayChoolCheck = distance <= boundary);
});
}
/// 위젯 빌드
@override
Widget build(BuildContext context) {
/// AppBar: 제목 & 액션 버튼1
var appBar = AppBar(
title: Text(
'오늘도 출근',
style: TextStyle(color: Colors.blue, fontWeight: FontWeight.w700),
),
centerTitle: true, // 안드로이드 설정
actions: <Widget>[
/// 원래 위치로 돌아오기 버튼
IconButton(
onPressed: returnToMyLocation,
icon: Icon(Icons.my_location),
color: Colors.blue,
),
],
);
return Scaffold(
backgroundColor: Colors.white,
appBar: appBar,
/// 권한 요청을 위한 빌더
body: FutureBuilder<void>(
future: checkPermission(), // 위치 권한 묻기 대화상자는 이 시점에 생성됨
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(child: Text('${snapshot.error}'));
}
// 이하 위젯 생성 코드...
},
),
);
}
///
Future<void> checkPermission() async {
/// 위치 서비스 활성화 여부
var isLocationServiceEnabled = await Geolocator.isLocationServiceEnabled();
/// 활성화되지 않았다면 에러 발생 (앱이 현재 위치를 사용할 수 없으므로)
assert(isLocationServiceEnabled, '위치 기능을 활성화하지 않았습니다.');
/// 권한 상태 확인
var checkedPermission = await Geolocator.checkPermission();
if (checkedPermission == LocationPermission.denied) {
checkedPermission = await Geolocator.requestPermission();
}
/// 시뮬레이션 상 `checkedPermission`이 위치 권한 허용 상태가 아니면 에러 유도
assert(
checkedPermission == LocationPermission.always ||
checkedPermission == LocationPermission.whileInUse,
'위치 권한을 허용해주세요.',
);
}오류 추적오류의 원인은 'User denied permissions .. device's location.' 즉, 사용자가 위치 권한을 아직 허용하지 않은 시점에 위치 권한이 필요한 함수를 호출하려고 함. [ERROR:flutter/runtime/dart_vm_initializer.cc(40)] Unhandled Exception: User denied permissions to access the device's location.
E/flutter (13339): #0 GeolocatorAndroid.getPositionStream.<anonymous closure> (package:geolocator_android/src/geolocator_android.dart:201:9)
E/flutter (13339): #1 Stream.handleError.<anonymous closure> (dart:async/stream.dart:961:16)
E/flutter (13339): #2 HandleErrorStream.handleError (dart:async/stream_pipe.dart:303:17)
...#0 에서 이미 getPositionStream().<anonymous closure> 로 추적한 걸로 보아, Geolocator.getPositionStream() 함수 호출 시점에 오류 발생한 것이 분명함.문제의 원인과 해결책 (일부 ChatGPT의 도움을 받음)문제의 가장 중요한 핵심은, 처음 앱 실행 시 위치 권한 확인 로직이 완전히 끝나기 전에 위치 권한 허용이 이루어지지 않으므로, 앱 재시작 이전까지 '사용자 현재 위치 추적 기능'이 활성화되지 않고, 추가로 관련 함수에서 오류 발생 가능성이 있음.그러므로 checkPermission() 함수는 initState()에서 최초 한 번만 실행하고, Geolocator.getpositionStream() 함수는 따로 분리하여 checkPermission()이 정확히 끝난 시점에 실행되도록 로직을 변경함. 1. checkPermission() 의 리턴 타입을 Future<void> 에서 Future<bool>로 변환 /// 위치 권한 확인 및 부여 함수
Future<bool> checkPermission() async {
/// 위치 서비스 활성화 여부
var isLocationServiceEnabled = await Geolocator.isLocationServiceEnabled();
assert(isLocationServiceEnabled, '위치 기능을 활성화하지 않았습니다.');
/// 권한 상태 확인
var checkedPermission = await Geolocator.checkPermission();
if (checkedPermission == LocationPermission.denied) {
checkedPermission = await Geolocator.requestPermission();
}
/// 위치 권한 허용 여부를 리턴
return checkedPermission == LocationPermission.always ||
checkedPermission == LocationPermission.whileInUse;
}2. initState()에서 checkPermission()과 addSubscription() 함수를 차례로 실행 (.then() 메소드 활용) @override
void initState() {
super.initState();
checkPermission().then((permitted) {
if (permitted) addSubscription();
});
}
/// addSubscriptsion 함수
void addSubscription() {
/// 리스너 함수: 출근체크 가능 여부 확인하기
Geolocator.getPositionStream().listen((eventPosition) {
// 목적지 위치
var start = defaultLatLng;
// 현재 위치
var end = LatLng(eventPosition.latitude, eventPosition.longitude);
/// 거리 구하기
var distance = Geolocator.distanceBetween(
start.latitude,
start.longitude,
end.latitude,
end.longitude,
);
/// 거리가 `boundary` 내부에 들어오는지 확인
setState(() => mayChoolCheck = distance <= boundary);
});
} 3. 위젯 재빌드 시 FutureBuilder의 future 함수 재호출을 방지하기 위해 비동기 결과를 미리 필드로 빼놓기ChatGPT는 이를 'Cashing Future' 라고 부른다고 함.(이 코드 부분에 중요한 로직이 하나 빠져서 방금 재수정했습니다.)/// 권한 확인 여부 리턴 값
late final Future<bool> permissionFuture;
@override
void initState() {
super.initState();
/// checkPermission()의 결과를 값으로 저장
permissionFuture = checkPermission().then((permitted) {
if (permitted) addSubscription();
return permitted;
});
}
/// 위젯 빌드
@override
Widget build(BuildContext context) {
/// AppBar: 제목 & 액션 버튼1
var appBar = AppBar(...);
return Scaffold(
backgroundColor: Colors.white,
appBar: appBar,
/// 권한 요청을 위한 빌더
body: FutureBuilder<bool>(
future: permissionFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting ||
!snapshot.hasData) {
return ColoredBox(
color: Colors.blue,
child: Center(child: CircularProgressIndicator()),
);
}
/// snapshot.data가 false 값인 경우는 사용자가 위치 권한 요청을 거부했을 때
if (snapshot.hasError || snapshot.data! == false) {
return Center(child: Text('위치 권한을 허용해주세요.\n${snapshot.error}'));
}
/// 이하 위젯 생성 코드 ...
},
),
);
} 이렇게 했을 때 앱 삭제 후 다시 빌드 했을 때, 그리고 재시작 했을 때도 정상적으로 잘 작동했습니다.코드가 많이 훼손된 느낌도 들긴 하지만.. 나름대로 공부가 돼서 기분은 좋네요.주저리주저리 쓰느라 정신 없었는데.. 잘 이해가 안 되거나 하고 싶은 피드백이 있다면 꼭 말씀해주세요!아무쪼록 끝까지 읽어주셔서 감사합니다.