[Flutter] isolate와 SharedPreferences, 그리고 파일에 대한 접근

2021. 11. 19. 17:52Programming/Flutter

반응형

예쓰! 검은곰입니다. 오늘은 Flutter의 isolateshared_preferences에 대한 내용을 정리하려고 합니다. 정확히는 이번에 굉장히 골머리를 겪게했던 이슈에 대한 정리로, 여러분들은 Isolate 내에서는 native 코드에 접근할 수 없다. shared_preferences 패키지는 native 코드의 SharedPreferences를 참조하므로, 사용할 수 없다.라는 내용만 기억하시면 됩니다. 개발자로써 코드를 작성하다보면 오만가지 코드와 상황을 만나게 되기 마련이지만, 저랑 비슷한 상황을 만나기는 쉽지 않을 거거든요.

아무튼 isolate에서 shared_preferences에 저장해놓은 값을 읽어서 분기처리해야 하는 상황에 처했습니다. 보통 직접 isolate를 생성하는 경우에는 Dart Isolate 2-Way Communication, Leland Zach와 같은 방식으로 통신을 하면 됩니다. 당연히 '요렇게 하면 됩니다!'라고 답을 제시했을 때 어렴풋이 눈치채셨겠지만, 저는 이런 방식을 사용할 수 없는 상황이었죠. FirebaseMessaging.onBackgroundMessage()의 결과로 생성된 isolate에서 값을 참조해야했기 때문입니다.


Handling messages whilst your application is in the background is a little different. Messages can be handled via the onBackgroundMessage handler. When received, an isolate is spawned (Android only, iOS/macOS does not require a separate isolate) allowing you to handle messages even when your application is not running.

공식 문서에 따르면 애석하게도 안드로이드만 isolate가 spawn된다고 기재되어있습니다. 맙소사. 아무튼 백그라운드에서 FirebaseMessage를 수신했을 때 spawn되는 isolate와 통신할 방법은 딱히 없어 보입니다.


결국 안드로이드 Natvie에서 제공하는 SharedPreferences도 어딘가에 값을 저장해놓는 형태. 이 방법 저 방법을 다 쓰다 결국 shared_preferences 패키지의 코드를 까본 뒤에야 Native 코드로 구현된 것을 발견한 저는, 그만 정신줄을 놓아버린 채 '야, 그냥 파일에 써버리면 isolate고 나발이고 다 읽을 수 있는 거 아니냐?'라는 1차원적인 결론에 도달하고 맙니다. 결론만 말하자면 isolate에서도 path_providergetApplicationSupportDirectory()에 접근이 가능하므로, 여기에 적당한 구조로 파일을 작성해서 저장하면 shared_preferences와 비슷한 방식으로 사용이 가능합니다. 아래는 단순히 Map<String, dyanmic> 타입으로 작성된 객체를 stringify된 json형식으로 만들어, 파일에 저장하고 불러오는 코드입니다. 하지만 이렇게 해서, Isolate에서도 접근 가능한 SharedPreferences 비슷한 녀석을 만들어줄 수 있죠.

Future<File> get _localFile async {
  Directory directory = await getApplicationDocumentsDirectory();
  return File("${directory.path}/settings.json");
}

Future<Map<String, dynamic>> readSettingsFromFile() async {
  final file = await _localFile;

  Map<String, dynamic> settings;
  try {
    final contents = await file.readAsString();
    settings = jsonDecode(contents);
  } catch (e) {
    settings = {
      "booleanTypeSettingA": true,
      "booleanTypeSettingB": false,
      "numberTypeSettingA": 0,
      "numberTypeSettingB": 0,
      "numberTypeSettingC": 0,
      "stringTypeSettings": "foo",
      "stringTypeSettings": "bar",
    };

    file.writeAsString(
      jsonEncode(settings),
    );
  }

  return settings;
}

Future<void> saveSettingsToFile<T>(String key, T value) async {
  final file = await _localFile;
  Map<String, dynamic> settings = await readSettingsFromFile();

  settings.containsKey(key)
      ? settings[key] = value
      : settings.addAll({key: value});

  await file.writeAsString(
    jsonEncode(settings),
  );
}
  • _localFile()
    • _localFile()settings.json을 불러오는 코드입니다. readSettingsFromFile()saveSettingsToFile()에서 공통으로 사용하는 코드이므로, 별도의 함수로 분리했습니다.
  • readSettingsFromFile()
    • settings.json 파일을 읽어서, Map<String, dynamic> 값을 반환해주는 함수입니다. localFile()을 통해 settings.json 파일을 불러오는떼 실패할 경우, 기본값이 설정된 Map<String, dyanic> 값을 생성해줍니다.
  • saveSettingsToFile<T>(String key, T value)
    • 키값과 저장할 값을 인자로 받아, settings에 키-값 쌍을 저장하고, 파일에 값을 씁니다. 만약 settings에 키값이 포함되어있지 않다면, 키값을 추가해줍니다.

지금 상태에서는 Map<String, dynamic>을 다루기때문에 키값이 항상 문자열이어서, 코드를 작성할 때 오타로 인한 휴먼 에러가 발생할 소지가 있긴 합니다. 이런 휴먼 에러를 방지하려면, 이전에 만들었던 SharedPreferencesHelper같은 녀석을 만들어주면 되겠죠.

오늘은 여기까지. Isolate에서는 native 영역에 접근할 수 없기 때문에, 패키지로 제공되는 SharedPreferences에 접근할 수 없다는 점. 그리고 이를 회피하기 위해서 파일에 값을 저장하고 써서, SharedPreferences를 대체하는 방법에 대해 알아봤습니다.

혹시 질문이나 혹은 잘못된 점은 댓글로 달아주세요!
잘못된 점은 최대한 빠르게 수정하도록 하겠습니다. ' ㅂ')/

반응형