Flutter : background_locator2 사용법 (Flutter: how to use a background_locator2 package)

koeyhoyh·2022년 8월 7일
2

App_Flutter

목록 보기
15/19

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 모드로 빌드해 실제로 작동하는 모습을 확인하시는 것을 추천드립니다!!! : )

profile
내가 만들어낸 것들로 세계에 많은 가치를 창출해내고 싶어요.

2개의 댓글

comment-user-thumbnail
2023년 7월 6일

안녕하세요 혹시 initCallback이 제대로 작동했는지 알려주실 수 있나요?? 최근에 사용해보려고 하니까 다른 콜백들은 정상적으로 작동하는데 initCallback만 호출되지가 않더라구요

1개의 답글