[Flutter] Repository (Isar, Get it)

Comely·2024년 11월 15일

Flutter

목록 보기
10/26

이론 설명

HTTP 요청 및 Dio

Dio는 HTTP 요청을 비동기적으로 보내고 JSON 데이터를 쉽게 처리할 수 있도록 도와주는 라이브러리입니다.

Isar 데이터베이스 필터링

Isar는 Flutter용 NoSQL 데이터베이스로, 비동기 필터링과 트랜잭션을 지원하여 데이터의 안정적인 저장 및 조회를 제공합니다.

//pubspec.yaml
dio: ^5.4.2+1
isar: ^3.1.0+1
isar_flutter_libs: ^3.1.0+1
path_provider: ^2.1.2
get_it: ^7.6.8

StatRepository 전체 코드

class StatRepository {
  static Future<void> fetchData() async {
    final isar = GetIt.I<Isar>();

    final now = DateTime.now();
    final compareDateTimeTarget = DateTime(
      now.year,
      now.month,
      now.day,
      now.hour,
    );

    final count = await isar.statModels
        .filter()
        .dateTimeEqualTo(compareDateTimeTarget)
        .count();

    if(count > 0){
      print('데이터가 존재합니다 : count: $count');
      return; //현재 시간에 해당하는 데이터가 있는지 체크하고 API 요청을 아낄 수 있다
    }

    for (ItemCode itemCode in ItemCode.values) {
      await fetchDataByItemCode(itemCode: itemCode);
    }
  }

  static Future<List<StatModel>> fetchDataByItemCode({
    required ItemCode itemCode,
  }) async {
    final response = await Dio().get(
      'http://apis.data.go.kr/B552584/ArpltnStatsSvc/getCtprvnMesureLIst',
      queryParameters: {
        'serviceKey':
            'WydwQBToSVQvB5peTjNMmNPaOk+kkHCdlSVz5AZgCJrqftRz+TnHdOTer/9a+dEw10Y66GQHZrEmHQpFlbqiNA==',
        'returnType': 'json',
        'numOfRows': 100,
        'pageNo': 1,
        'itemCode': itemCode.name,
        'dataGubun': 'HOUR',
        'searchCondition': 'WEEK',
      },
    );

    final rawItemsList = response.data['response']['body']['items'];

    List<StatModel> stats = [];

    final List<String> skipKeys = [
      'dataGubun',
      'dataTime',
      'itemCode',
    ];

    for (Map<String, dynamic> item in rawItemsList) {
      final dateTime = DateTime.parse(item['dataTime']);

      for (String key in item.keys) {
        if (skipKeys.contains(key)) {
          continue;
        }

        final regionStr = key;
        final stat = double.parse(item[regionStr]);
        final region = Region.values.firstWhere((e) => e.name == regionStr);

        final statModel = StatModel()
          ..region = region
          ..stat = stat
          ..dateTime = dateTime
          ..itemCode = itemCode;

        final isar = GetIt.I<Isar>();
        
        //statmodel을 collection에 넣었기 때문에 statModels collection 자동생성
        final count = await isar.statModels
            .filter()
            .regionEqualTo(region)
            .dateTimeEqualTo(dateTime)
            .itemCodeEqualTo(itemCode)
            .count();

        if (count > 0) {
          continue;
        }

        await isar.writeTxn(
          () async {
            await isar.statModels.put(statModel);
          },
        );
      }
    }

    return stats;
  }
}

StatRepository 클래스

fetchDatafetchDataByItemCode 메서드를 통해
공공 API로부터 대기 오염 데이터를 가져와 로컬 데이터베이스(Isar)에 저장하는 기능을 담당합니다.
이를 위해 dio 라이브러리를 사용해 HTTP 요청을 보내고,
isar 데이터베이스를 통해 데이터의 캐싱을 수행합니다.

아래는 코드의 각 부분에 대한 기능과 이론을 상세하게 설명합니다.

1. fetchData 메서드

fetchData 메서드는 공공 API를 통해 데이터를 가져오고 로컬에 저장하는 작업을 시작하는 역할을 합니다.

- isar 초기화

GetIt.I() 구문을 통해 Isar 데이터베이스 인스턴스를 가져옵니다.
GetIt은 의존성 주입 도구로, Isar 인스턴스를 전역적으로 사용할 수 있게 해줍니다.

- 현재 시간 설정 및 비교

현재 시간을 얻고, 비교할 시간의 목표 지점을 설정합니다.
시간 단위로 데이터를 확인하기 때문에 compareDateTimeTarget은 분과 초가 0으로 설정되어 있습니다.

final now = DateTime.now();
final compareDateTimeTarget = DateTime(now.year, now.month, now.day, now.hour);

- 로컬 데이터 확인

statModels 컬렉션에서 현재 시간에 해당하는 데이터가 있는지 필터링하고, 일치하는 데이터의 개수를 계산합니다.
count > 0이면 이미 데이터가 저장되어 있으므로 새 데이터를 가져올 필요가 없습니다.

- 데이터 수집 시작: 데이터가 없을 경우

- ItemCode의 모든 값에 대해 fetchDataByItemCode 메서드를 호출하여 데이터를 가져옵니다.
final count = await isar.statModels
    .filter()
    .dateTimeEqualTo(compareDateTimeTarget)
    .count();

2. fetchDataByItemCode 메서드

이 메서드는 특정 ItemCode를 기준으로 공공 API에서 데이터를 가져와 StatModel 형태로 변환하고 저장합니다.

- API 요청 보내기

final response = await Dio().get(
    'http://apis.data.go.kr/B552584/ArpltnStatsSvc/getCtprvnMesureLIst',
    queryParameters: {
      'serviceKey': '<API_KEY>',
      'returnType': 'json',
      'numOfRows': 100,
      'pageNo': 1,
      'itemCode': itemCode.name,
      'dataGubun': 'HOUR',
      'searchCondition': 'WEEK',
    },
  );

Dio를 사용해 공공 API로부터 데이터를 받아옵니다.
queryParameters에는 필요한 API 키와 데이터 필터링 조건을 포함하여 응답 형태와 데이터를 세부적으로 조정합니다.
itemCode는 요청하는 데이터 항목의 종류를 나타냅니다.

- 데이터 처리

final rawItemsList = response.data['response']['body']['items'];

response 객체에서 JSON 데이터의 items 배열을 추출합니다.
items는 Key는 각 지역이며, Value는 시간대별로 측정된 대기오염 데이터를 포함합니다.

- 스킵할 키 정의

final List<String> skipKeys = ['dataGubun', 'dataTime', 'itemCode'];

데이터 처리를 위한 반복문에서 제외할 키들을 정의합니다.
dataTime, dataGubun, itemCode는 측정 항목 데이터에는 필요 없기 때문에 스킵합니다.

- 반복문을 통한 데이터 처리

for (Map<String, dynamic> item in rawItemsList) {
    final dateTime = DateTime.parse(item['dataTime']);

각 항목별로 반복문을 돌며 데이터를 처리합니다.
dataTime을 DateTime 객체로 변환하여 시간을 기준으로 데이터를 저장할 수 있습니다.

- 지역 및 측정치 추출

for (String key in item.keys) {
    if (skipKeys.contains(key)) continue;
    final regionStr = key;
    final stat = double.parse(item[regionStr]);
  	//name을 사용하여 string으로 받음
    final region = Region.values.firstWhere((e) => e.name == regionStr);

지역을 나타내는 키와 해당 키의 값(측정치)을 추출합니다.
Region 열거형에서 해당 지역 문자열과 일치하는 값을 찾아 지역 코드로 변환합니다.

- StatModel 생성 및 데이터베이스 저장

final statModel = StatModel()
  ..region = region
  ..stat = stat
  ..dateTime = dateTime
  ..itemCode = itemCode;

final isar = GetIt.I<Isar>();

//statmodel을 collection에 넣었기 때문에 statModels collection 자동생성
//count, filter: 중복으로 데이터 저장하지 않고 패스하는 로직
final count = await isar.statModels
    .filter()
    .regionEqualTo(region)
    .dateTimeEqualTo(dateTime)
    .itemCodeEqualTo(itemCode)
    .count();

if (count > 0) continue;

//데이터 생성
await isar.writeTxn(
    () async {
        await isar.statModels.put(statModel);
    },
);

statModel 객체를 생성하여 데이터를 할당하고,
isar 인스턴스를 통해 해당 데이터가 이미 있는지 확인합니다.
일치하는 데이터가 없다면 Isar의 writeTxn을 통해 트랜잭션 내에서 데이터를 저장합니다.
writeTxn은 데이터를 안전하게 저장하기 위한 트랜잭션 메서드입니다.

3. stat_model.dart (Isar)

import 'package:isar/isar.dart';

part 'stat_model.g.dart';

enum Region {
  daegu, chungnam, incheon, daejeon, gyeongbuk, Sejong,
  gwangju, jeonbuk, gangwon, ulsan, jeonnam,  
  seoul, busan, jeju, chungbuk, gyeongnam, gyeonggi;

  String get krName {
    switch (this) {
      case Region.daegu:
        return '대구';
      case Region.chungnam:
        return '충남';
      case Region.incheon:
        return '인천';
      case Region.daejeon:
        return '대전';
      case Region.gyeongbuk:
        return '경북';
      case Region.sejong:
        return '세종';
      case Region.gwangju:
        return '광주';
      case Region.jeonbuk:
        return '전북';
      case Region.gangwon:
        return '강원';
      case Region.ulsan:
        return '울산';
      case Region.jeonnam:
        return '전남';
      case Region.seoul:
        return '서울';
      case Region.busan:
        return '부산';
      case Region.jeju:
        return '제주';
      case Region.chungbuk:
        return '충북';
      case Region.gyeongnam:
        return '경남';
      case Region.gyeonggi:
        return '경기';
      default:
        throw Exception('존재하지 않는 지역 이름입니다.');
    }
  }
}

enum ItemCode {
  SO2,  CO,  O3,  NO2,  PM10,  PM25;

  String get krName {
    switch (this) {
      case ItemCode.SO2:
        return '이황산가스';
      case ItemCode.CO:
        return '일산화탄소';
      case ItemCode.O3:
        return '오존';
      case ItemCode.NO2:
        return '이산화질소';
      case ItemCode.PM10:
        return '미세먼지';
      case ItemCode.PM25:
        return '초미세먼지';
    }
  }
}

// "dataTime": "2024-04-09 10:00",
// "itemCode": "PM10"
// "dataGubun": "1",
// response['region']
// data.region;


class StatModel {
  Id id = Isar.autoIncrement;

  // 지역
  
  (unique: true, composite: [
    CompositeIndex('dateTime'),
    CompositeIndex('itemCode'),
  ])
  late Region region;

  // 통계 값
  late double stat;

  // 날짜
  late DateTime dateTime;

  // 미세먼지 / 초미세먼지
  
  late ItemCode itemCode;
}

4. Main.dart (Isar 설정)

  • Isar @collection으로 자동으로 생성된 StatModelSchema를 추가할 directory을 입력합니다.

    • isar 인스턴스를 통해 Isar 사용하는 코드를 작성하였습니다.
  • Get_It으로 Isar를 사용합니다.

    void main() async {
    //  runApp 이외에 다른 것을 먼저 시작하기 위해 필요
    WidgetsFlutterBinding.ensureInitialized();
    
    final dir = await getApplicationDocumentsDirectory();
    final isar = await Isar.open(
      [StatModelSchema],
      directory: dir.path,
    );
    
    GetIt.I.registerSingleton<Isar>(isar);
    
    runApp(
      MaterialApp(
        debugShowCheckedModeBanner: false,
        theme: ThemeData(
          fontFamily: 'sunflower',
        ),
        home: HomeScreen(),
      ),
    );
    }
  
  
- 데이터베이스 코드 수정한다면, 어플 삭제 후 재설치 하는 게 좋습니다.
  
  
profile
App, Web Developer

0개의 댓글