Develop/Flutter

[Flutter] 플러터 Provider로 공공데이터 오픈 API 활용

JunJangE 2022. 1. 5. 11:07

이번에는 Provider로 공공데이터 오픈 API를 활용해보자.

공공데이터 오픈 API 활용 신청하는 방법은 다음 링크를 통해서 확인 후 현재 페이지에서 진행하면 될 것 같다.

 

[kotlin] 코틀린 Android 공공데이터 오픈 API 활용(XML 문서)

이번에는 다양한 데이터가 있는 공공데이터 포털을 통해 오픈 API를 활용해려고한다. 공공데이터 포털 국가에서 보유하고 있는 다양한 데이터를『공공데이터의 제공 및 이용 활성화에 관한 법

fre2-dom.tistory.com

우리가 사용할 데이터는 한국전력공사에 전기차 충전소 운영정보의 오픈 API를 통해 예제를 수행하려 한다.

아래 데이터는 XML로 이루어져 있으며, XML 데이터와 JSON 데이터 모두 수행할 수 있는 코드이기 때문에 신경 쓰지 않고 예제를 수행해도 될 것 같다. 

 

한국전력공사_전기차 충전소 운영정보

전기차 충전소명 및 주소 정보

www.data.go.kr

라이브러리

아래 링크에 들어가 installing에 각 패키지 코드를 복사하여 pubspec.yaml 파일에 추가하여 다운로드한다.

 

Dart packages

Pub is the package manager for the Dart programming language, containing reusable libraries & packages for Flutter, AngularDart, and general Dart programs.

pub.dev

dependencies:
  provider: ^6.0.1 # provider 패키지 추가
  http: ^0.13.4 # http 패키지 추가
  xml2json: ^5.3.2 # xml -> json 형식으로 변환 (json 형식의 데이터를 가져올 경우 필요없음)

코드의 이해를 돕기 위해 폴더의 위치를 다음과 같이 구성한다.

폴더의 위치를 통해 간단하게 수행하는 역할을 알아보면 main.dart에서 앱을 실행하게 되고 home.dart에서 list.dart를 호출해 UI를 꾸미게 된다. ev.dart 에는 우리가 가져올 데이터를 클래스로 만들어 관리하는 것이고 ev_repository.dart에서는 오픈 api를 통해 데이터를 가져오게 된다. ev_provider에서는 가져온 데이터를 구독자에게 알려주는 역할을 한다. 

폴더의 위치를 구성하고 각 패키지를 추가했다면 본격적으로 코드를 작성하도록 하자.

main.dart

import 'package:open_api_xml_parser/src/provider/ev_provider.dart';
import 'package:flutter/material.dart';
import 'package:open_api_xml_parser/src/home.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        // MultiProvider를 통해 여러가지 Provider를 관리
        home: MultiProvider(
          
            // ChangeNotifierProvider 통해 변화에 대해 구독
            providers: [
              ChangeNotifierProvider(
                  create: (BuildContext context) => EvProvider())
            ],
            child:
                Home() // home.dart 
            ));
  }
}

main.dart는 앱을 실행하는 처음 단계이다.

위에서부터 코드를 확인하게 되면 MultiProvider를 통해 여러 가지 Provider를 관리하게 된다. 사실 위 코드에서는 ChangeNotifierProvider를 통해서만 코드를 작성해도 되지만 추가적인 Provider를 생성할 수 있으므로 MultiProvider로 감싸주었다. 그리고 Home() 위젯을 호출해 위에 Provider에 접근하게 된다.

home.dart

import 'package:flutter/material.dart';
import 'package:open_api_xml_parser/src/ui/list.dart';

class Home extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ListWidget(),
    );
  }
}

home.dart에서는 ListWidget()을 호출하게 된다. 

위 코드도 보다시피 필요한 코드는 아니지만 ListWidget()이 아닌 다른 위젯을 통해 UI를 꾸밀 수 있기 때문이 따로 home.dart를 통해 관리한다.

list.dart

import 'package:open_api_xml_parser/src/model/ev.dart';
import 'package:open_api_xml_parser/src/provider/ev_provider.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class ListWidget extends StatelessWidget {
  ListWidget({Key? key}) : super(key: key);

  late EvProvider _evProvider; // EvProvider 호출

  Widget _makeEvOne(Ev ev) {
    return Row(
      children: [
        Expanded(
            child: Padding(
          padding: EdgeInsets.all(15.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              // 충전소 주소
              Text(
                ev.addr.toString(),
                style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
              ),
              SizedBox(
                height: 10,
              ),

              // 충전기 타입
              Text(
                ev.chargeTp.toString(),
                style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
              ),
              SizedBox(
                height: 10,
              ),

              // 충전기 명칭
              Text(
                "충전기 명칭 : " + ev.cpNm.toString(),
                style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
              ),
              SizedBox(
                height: 10,
              ),

              // 충전기 상태 코드
              Text(
                ev.cpStat.toString(),
                style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
              ),
              SizedBox(
                height: 10,
              ),

              // 충전 방식
              Text(
                ev.cpTp.toString(),
                style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
              ),
              SizedBox(
                height: 10,
              ),

              // 충전소 명칭
              Text(
                "충전소 명칭 : " + ev.csNm.toString(),
                style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
              ),
              SizedBox(
                height: 10,
              ),

              // 위도
              Text(
                "위도 : " + ev.lat.toString(),
                style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
              ),
              SizedBox(
                height: 10,
              ),

              // 경도
              Text(
                "경도 : " + ev.longi.toString(),
                style: TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
              ),
            ],
          ),
        ))
      ],
    );
  }

  // 리스트 뷰
  Widget _makeListView(List<Ev> evs) {
    return ListView.separated(
      itemCount: evs.length,
      itemBuilder: (BuildContext context, int index) {
        return Container(
            height: 300, color: Colors.white, child: _makeEvOne(evs[index]));
      },
      separatorBuilder: (BuildContext context, int index) {
        return Divider();
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    // Provider.of를 통해 데이터를 접근한다. builder만을 업데이트 하기 위해 listen은 false로 한다.
    _evProvider = Provider.of<EvProvider>(context, listen: false);
    _evProvider.loadEvs(); // EvProvider에 loadEvs()의 접근

    return Scaffold(
        appBar: AppBar(
          title: Text("Ev Provider"),
        ),
        // Consumer를 통해 데이터를 접근
        body: Consumer<EvProvider>(builder: (context, provider, wideget) {
          // 데이터가 있으면 _makeListView에 데이터를 전달
          if (provider.evs != null && provider.evs.length > 0) {
            return _makeListView(provider.evs);
          }

          // 데이터가 없으면 CircularProgressIndicator 수행(로딩)
          return Center(
            child: CircularProgressIndicator(),
          );
        }));
  }
}

위 코드는 UI만을 담당하는 코드이다.

코드를 보게 되면 EvProvider를 호출해 접근하고 builder만을 업데이트해주기 위해 listen은 false로 한다. listen을 true로 하게 되면 build가 계속해서 업데이트되기 때문에 무한 루프에 빠지게 된다.

다음으로 body 부분을 보게 되면 Consumer를 통해 데이터에 접근하고 데이터가 있으면 위에서 만든 _makeListView 위젯을 호출하게 된다. 데이터가 없을 경우에는 CircularProgressIndicator를 수행한다. 

_makeListView에서는 리스트 뷰를 만든 것이고 리스트 뷰에서 각 뷰를 _makeEvOned 위젯을 통해 UI에 뿌려주게 된다. _makeEvOned 위젯은 Ev를 매개변수로 받게 되고 EvProvider의 데이터를 UI에 뿌려주게 된다. 

ev_provider.dart

import 'package:open_api_xml_parser/src/model/ev.dart';
import 'package:open_api_xml_parser/src/repository/ev_repository.dart';
import 'package:flutter/material.dart';

class EvProvider extends ChangeNotifier {
  // EvRepository를 접근(데이터를 받아와야 하기 때문에)
  EvRepository _evRepository = EvRepository(); 

  List<Ev> _evs = [];
  List<Ev> get evs => _evs;

  // 데이터 로드
  loadEvs() async {
    // EvRepository 접근해서 데이터를 로드
    // listEvs에 _evs를 바로 작성해도 되지만 예외 처리와 추가적인 가공을 위해 나눠서 작성한다. 
    List<Ev>? listEvs = await _evRepository.loadEvs();
    _evs = listEvs!;
    notifyListeners(); // 데이터가 업데이트가 됐으면 구독자에게 알린다.
  }
}

위 코드는 비즈니스 로직 부분이다.

위에서부터 코드를 보게 되면 EvRepository를 호출, 접근해 데이터를 받아온다. 받아온 데이터는 listEvs에 담아 _evs로 전달해줘 구독자에게 업데이트된 값을 알려주게 된다.

listEvs에 대신 _evs로 바로 작성해도 되지만 예외 처리와 추가적인 가공을 위해 나눠서 작성한다.

ev.dart

class Ev {
  String? addr; // 충전소 주소
  String? chargeTp; // 충전기 타입
  String? cpNm; // 충전기 명칭
  String? cpStat; // 충전기 상태 코드
  String? cpTp; // 충전 방식
  String? csNm; // 충전소 명칭
  String? lat; // 위도
  String? longi; // 경도

  Ev({
    this.addr,
    this.chargeTp,
    this.cpNm,
    this.cpStat,
    this.cpTp,
    this.csNm,
    this.lat,
    this.longi,
  });

  factory Ev.fromJson(Map<String, dynamic> json) {
    // 충전기 타입
    if (json["chargeTp"] == "1") {
      json["chargeTp"] = "충전기 타입 : 완속";
    } else if (json["chargeTp"] == "2") {
      json["chargeTp"] = "충전기 타입 : 급속";
    }

    // 충전기 상태 코드
    if (json["cpStat"] == "1") {
      json["cpStat"] = "충전기 상태 : 충전 가능";
    } else if (json["cpStat"] == "2") {
      json["cpStat"] = "충전기 상태 : 충전중";
    } else if (json["cpStat"] == "3") {
      json["cpStat"] = "충전기 상태 : 고장/정검";
    } else if (json["cpStat"] == "4") {
      json["cpStat"] = "충전기 상태 : 통신장애";
    } else if (json["cpStat"] == "5") {
      json["cpStat"] = "충전기 상태 : 통신미연결";
    }

    // 충전 방식
    if (json["cpTp"] == "1") {
      json["cpTp"] = "충전 방식 : B타입(5핀)";
    } else if (json["cpTp"] == "2") {
      json["cpTp"] = "충전 방식 : C타입(5핀)";
    } else if (json["cpTp"] == "3") {
      json["cpTp"] = "충전 방식 : BC타입(5핀)";
    } else if (json["cpTp"] == "4") {
      json["cpTp"] = "충전 방식 : BC타입(5핀)";
    } else if (json["cpTp"] == "5") {
      json["cpTp"] = "충전 방식 : DC차데모";
    } else if (json["cpTp"] == "6") {
      json["cpTp"] = "충전 방식 : AC3상";
    } else if (json["cpTp"] == "7") {
      json["cpTp"] = "충전 방식 : DC콤보";
    } else if (json["cpTp"] == "8") {
      json["cpTp"] = "충전 방식 : DC차데모+DC콤보";
    } else if (json["cpTp"] == "9") {
      json["cpTp"] = "충전 방식 : DC차데모+AC3상";
    } else if (json["cpTp"] == "10") {
      json["cpTp"] = "충전 방식 : DC차데모+DC콤보+AC3상";
    }

    return Ev(
      addr: json["addr"] as String,
      chargeTp: json["chargeTp"] as String,
      cpNm: json["cpNm"] as String,
      cpStat: json["cpStat"] as String,
      cpTp: json["cpTp"] as String,
      csNm: json["csNm"] as String,
      lat: json["lat"] as String,
      longi: json["longi"] as String,
    );
  }
}

위 코드는 데이터 클래스이다.

원하는 데이터만을 가져올 수 있다.

ev_repositiory.dart

import 'dart:convert' as convert;
import 'package:open_api_xml_parser/src/model/ev.dart';
import 'package:http/http.dart' as http;
import 'package:xml2json/xml2json.dart';


class EvRepository {
  // api key
  var apiKey =
      "apiKey";

  Future<List<Ev>?> loadEvs() async {
    
    var addr = "서울";
    String baseUrl =
        "http://openapi.kepco.co.kr/service/EvInfoServiceV2/getEvSearchList?addr=$addr&pageNo=1&numOfRows=10&ServiceKey=$apiKey";
    final response = await http.get(Uri.parse(baseUrl));
    
    // 정상적으로 데이터를 불러왔다면
    if (response.statusCode == 200) {

      // 데이터 가져오기
      final body = convert.utf8.decode(response.bodyBytes);

      // xml => json으로 변환
      final xml = Xml2Json()..parse(body);
      final json = xml.toParker(); 

      // 필요한 데이터 찾기
      Map<String, dynamic> jsonResult = convert.json.decode(json);
      final jsonEv = jsonResult['response']['body']['items'];

      // 필요한 데이터 그룹이 있다면 
      if (jsonEv['item'] != null) {
        // map을 통해 데이터를 전달하기 위해 객체인 List로 만든다.
        List<dynamic> list = jsonEv['item']; 

        // map을 통해 Ev형태로 item을  => Ev.fromJson으로 전달
        return list.map<Ev>((item) => Ev.fromJson(item)).toList();

      }

    }
  }
}

위 코드는 API 서버에서 데이터를 받아오게 되는 부분이다.

비동기를 통해 http와 연결 후 데이터를 받아오게 된다. 받아온 데이터가 xml이기 때문에 json 형식으로 바꿔주기 위해 패키지를 통해 json 형식으로 바꿔준다. 데이터가 josn 형식인 경우에는 xml에서 json으로 바꿔주는 코드를 작성하지 않고 필요한 데이터만을 찾아 사용하면 된다. 

필요한 데이터 그룹이 있다면 map을 통해 데이터를 Ev.dart로 전달해주기 위해 객체인 list로 만든다. map으로 Ev형태로 item을 Ev.fromJson으로 전달하고 리턴 받게 된다. 

모든 코드를 한 번에 리뷰하면 다음과 같다.

EvProvider에서 EvRepository를 호출, 접근해 loadEvs()를 실행하게 된다. EvRepository에서는 loadEvs()를 통해 API 서버와 통신하여 데이터를 받아오게 되고 원하는 데이터 그룹이 있다면 map을 통해 Ev형태로 데이터를 Ev에 fromJson으로 전달해 다시 리턴 받게 된다. 리턴 받은 데이터는 EvRepository의 loadEvs() 호출한 EvProvider로 리턴하게 되고 EvProvider에서 리턴 받은 데이터는 listEvs에 담아 _evs로 전달해줘 구독자에게 업데이트된 값을 알려주게 된다. 업데이트 되었다고 알림을 받은 list.dart에서는 데이터를 UI에 뿌려주게 된다.

위 코드를 모두 작성했다면 다음 영상과 같이 실행되는 것을 확인할 수 있다.

참고

 

[flutter] 공공 api에서 cctv 데이터를 가져와서 지도에 표시해보자 - 1탄

앱 개발만 하고싶은데 Todo, 싱글게임 만들거 아니면 웬만해서는 서버(백엔드) 개발이 필수적이다. 요즘엔 serverless라고 해서 서버를 없애는 방식도 많이 사용하지만 이것도 백엔드의 역할을 대신

padro.tistory.com

github

 

GitHub - junjange/Flutter-Learning: 플러터 학습

플러터 학습. Contribute to junjange/Flutter-Learning development by creating an account on GitHub.

github.com