Reference:
https://pub.dev/documentation/background_locator_2/latest/index.html
예제코드:
https://github.com/Yukams/background_locator_fixed/blob/master/example/lib/main.dart
Setting 부분은 다루지 않습니다.
예제 코드가 어떻게 실행되는 지를 설명하고, 어떤 부분을 고치면 내 의도대로 실행시킬 수 있는지 알아보겠습니다.
Future<void> _startLocator() async{
Map<String, dynamic> data = {'countInit': 1};
return await BackgroundLocator.registerLocationUpdate(LocationCallbackHandler.callback,
initCallback: LocationCallbackHandler.initCallback,
initDataCallback: data,
disposeCallback: LocationCallbackHandler.disposeCallback,
iosSettings: IOSSettings(
accuracy: LocationAccuracy.NAVIGATION,
distanceFilter: 0,
stopWithTerminate: true
),
autoStop: false,
androidSettings: AndroidSettings(
accuracy: LocationAccuracy.NAVIGATION,
interval: 5,
distanceFilter: 0,
client: LocationClient.google,
androidNotificationSettings: AndroidNotificationSettings(
notificationChannelName: 'Location tracking',
notificationTitle: 'Start Location Tracking',
notificationMsg: 'Track location in background',
notificationBigMsg:
'Background location is on to keep the app up-tp-date with your location. This is required for main features to work properly when the app is not running.',
notificationIconColor: Colors.grey,
notificationTapCallback:
LocationCallbackHandler.notificationCallback)));
registerLocationUpdate는 해당 라이브러리의 핵심 기능입니다.
그리고, callback, initCallback, initDataCallback, disposeCallback 함수를 등록해야 합니다. (+autoStop, androidSettings, iosSettings)
예제에서는 callbackHandler를 만들어 문제를 해결했습니다.
import 'dart:async';
import 'package:background_locator_2/location_dto.dart';
import 'location_service_repository.dart';
class LocationCallbackHandler {
static Future<void> initCallback(Map<dynamic, dynamic> params) async {
LocationServiceRepository myLocationCallbackRepository =
LocationServiceRepository();
await myLocationCallbackRepository.init(params);
}
static Future<void> disposeCallback() async {
LocationServiceRepository myLocationCallbackRepository =
LocationServiceRepository();
await myLocationCallbackRepository.dispose();
}
static Future<void> callback(LocationDto locationDto) async {
LocationServiceRepository myLocationCallbackRepository =
LocationServiceRepository();
await myLocationCallbackRepository.callback(locationDto);
}
static Future<void> notificationCallback() async {
print('***notificationCallback');
}
}
예제에서는 Repository 패턴을 이용해, 콜백함수를 만들었습니다.
Repository 패턴은 데이터가 있는 여러 저장소들을 추상화하여 중앙처리 집중방식을 구현할 수 있고 데이터가 어디에서 오는지 출처를 몰라도 괜찮아 비지니스 로직에만 집중할 수 있다는 장점이 있습니다.
출처 : https://4z7l.github.io/2020/11/24/repository-pattern.html
그리고 한 걸음 더 들어가면
class LocationServiceRepository {
static LocationServiceRepository _instance = LocationServiceRepository._();
LocationServiceRepository._();
factory LocationServiceRepository() {
return _instance;
}
static const String isolateName = 'LocatorIsolate';
int _count = -1;
Future<void> init(Map<dynamic, dynamic> params) async {
//TODO change logs
print("***********Init callback handler");
if (params.containsKey('countInit')) {
dynamic tmpCount = params['countInit'];
if (tmpCount is double) {
_count = tmpCount.toInt();
} else if (tmpCount is String) {
_count = int.parse(tmpCount);
} else if (tmpCount is int) {
_count = tmpCount;
} else {
_count = -2;
}
} else {
_count = 0;
}
print("$_count");
await setLogLabel("start");
final SendPort send = IsolateNameServer.lookupPortByName(isolateName);
send?.send(null);
}
Future<void> dispose() async {
print("***********Dispose callback handler");
print("$_count");
await setLogLabel("end");
final SendPort send = IsolateNameServer.lookupPortByName(isolateName);
send?.send(null);
}
Future<void> callback(LocationDto locationDto) async {
print('$_count location in dart: ${locationDto.toString()}');
await setLogPosition(_count, locationDto);
final SendPort send = IsolateNameServer.lookupPortByName(isolateName);
send?.send(locationDto);
_count++;
}
static Future<void> setLogLabel(String label) async {
final date = DateTime.now();
await FileManager.writeToLogFile(
'------------\n$label: ${formatDateLog(date)}\n------------\n');
}
static Future<void> setLogPosition(int count, LocationDto data) async {
final date = DateTime.now();
await FileManager.writeToLogFile(
'$count : ${formatDateLog(date)} --> ${formatLog(data)} --- isMocked: ${data.isMocked}\n');
}
static double dp(double val, int places) {
double mod = pow(10.0, places);
return ((val * mod).round().toDouble() / mod);
}
static String formatDateLog(DateTime date) {
return date.hour.toString() +
":" +
date.minute.toString() +
":" +
date.second.toString();
}
static String formatLog(LocationDto locationDto) {
return dp(locationDto.latitude, 4).toString() +
" " +
dp(locationDto.longitude, 4).toString();
}
}
init의 tmpCount부터 복잡해보입니다.
init의 파라미터가 countInit 이라는 key를 가지고 있다면 tmpCount를 int type이라면 그대로 사용하고 String 이거나 double 이면 int type으로 바꾸어줍니다.
타입이 int형으로 바꿀 수 없다면 count = -2, countInit이라는 key가 없다면 count = 0 입니다.
init은 count를 정하고, file에 start라는 기록을 남깁니다.
그리고 port를 찾아놓고, 해당 포트에 메시지를 보냅니다. 메시지 내용은 (null)입니다.
sendPort와 ReceivePort는 다른 isolate에 메시지를 보내 해당 이벤트를 수행하게 만듭니다. 마치 스레드를 하나 더 만들어 실행시키는 것처럼 말입니다.
(ReceivePort는 main.dart에 있으며 그곳에서 data를 받아 UI를 갱신합니다.)
아래 설명을 보시면 이해가 조금 더 편하실 수 있습니다 : https://www.youtube.com/watch?v=vl_AaCgudcY
이렇게 dispose, callback, logging을 위한 여러 가지 함수까지 핸들러에 다 들어가 있습니다.
목표: 안드로이드에서, 5분마다 내 위치를 DB(sqlite)에 기록하기
예제에서는 파일 시스템을 사용하지만, 저는 sqlite를 사용해 저장해보겠습니다.
먼저, AndroidSetting에서 interval을 300으로 변경해주었습니다.
저는 5분이라는 충분한 시간이 있어 isolate를 여러 개 사용할 필요가 없으므로, port사용을 다 없애주었습니다.
count도 필요 없는 변수이니, 삭제했습니다.
class LocationServiceRepository {
static LocationServiceRepository _instance = LocationServiceRepository._();
LocationServiceRepository._();
factory LocationServiceRepository() {
return _instance;
}
Future<void> init(Map<dynamic, dynamic> params) async {
//TODO change logs
print("***********Init callback handler");
}
Future<void> dispose() async {
}
Future<void> callback(LocationDto locationDto) async {
print('location in dart: ${locationDto.toString()}');
await setLogPosition(locationDto);
}
static Future<void> setLogPosition(LocationDto data) async {
final date = DateTime.now();
await MapState().save(data.latitude, data.longitude);
}
}
참고로, 전에는 port를 이용해 UIupdate를 해주고 있었으니 다른 방법으로 UI update가 필요할 것입니다.
DB 모델은 이렇게 구성했습니다.
저는 시간을 다 int형으로 넣어주었는데, Unixtime으로 가져와 변환을 해주어도 보기 좋을 것 같습니다. (개인의 선택입니다.)
DB에 저장한 방법입니다. 저는 따로 헬퍼 클래스를 만들어 DB에 저장해주었습니다.
Future<void> save(dynamic lat, dynamic lot) async {
DBHelper hp = DBHelper();
var time = DateTime.now();
var locate = Location(
id: idx,
year: time.year,
month: time.month,
day: time.day,
hour: time.hour,
minute: time.minute,
latitude: lat,
longitude: lot,
);
await hp.insertLocate(locate);
}
이렇게 수정해주었어도, 5분마다 실행이 잘 되는 모습을 확인할 수 있었습니다.
이렇게 글을 마칩니다. 혹시라도 잘못된 정보가 있거나, 잘 작동하지 않으면 언제든지 댓글이나 이메일로 연락 주시면 감사하겠습니다!
그리고, 앱을 release 모드로 빌드해 실제로 작동하는 모습을 확인하시는 것을 추천드립니다!!! : )
안녕하세요 혹시 initCallback이 제대로 작동했는지 알려주실 수 있나요?? 최근에 사용해보려고 하니까 다른 콜백들은 정상적으로 작동하는데 initCallback만 호출되지가 않더라구요