첫번째 Flutter App은 VS code가 자동 생성해주었습니다.
이제 직접 필요한 App을 제작해보겠습니다.
두번째 Flutter App은 환율 계산기를 만들어보려고합니다.
① App이 실행될 때,
② 사용자가 원할 때,
웹상에서 환율을 가져와서 업데이트하고,
금액을 입력하면 환율이 자동으로 변환되도록 해보겠습니다.
아래와 같은 순서로 해보겠습니다.
1. 웹에서 정보 읽어오기
2. 환율 가져오기
3. Main UI 구현
4. 설정 UI 구현
5. 환율 정보 읽기가 완료되었을 때 UI 업데이트 구현
시작해보겠습니다.
1. 웹에서 정보 읽어오기
http protocol을 사용해서 웹페이지를 읽을 수 있는 class를 제작해보겠습니다. 웹크롤링을 좀 더 전문적으로 하다보면, http contents를 좀 더 전문적으로 검색하기 위해 많은 package 설치가 필요합니다만, 여기서 아주 간단하게 html을 String으로 읽어오는 작업만 구현해볼 예정입니다.
새로운 App project 생성
VS code를 시작하고, 새로운 Flutter project를 생성합니다.
저는 exchange_rate_calculator 로 project 이름을 정했습니다.
* 참조 : 2022.09.14 - [Flutter] - [Flutter 코딩] 첫번째 Flutter App - 1. 만들어보기
웹페이지 읽기 function 구현
Explorer창(파일 폴더 구조가 보이는 좌측의 창)에서 \lib 폴더를 우클릭하고 New File... 을 클릭해줍니다.
파일명 입력칸에 web_fetcher.dart 로 파일명을 입력해줍니다.
* 혹시 Explorer창이 닫혀있다면, 좌측 Side Bar에서 최상단의 Explorer를 클릭하거나 Ctrl + Shift + E 를 입혁하면 보일 겁니다.
Web에서 정보를 http protocol을 이용해서 읽어오는 작업을 해야하니, http package를 library로 설치해줘야합니다.
설치에는 pubspec.yaml을 수정하는 방법이 있고, flutter pub add 명령을 사용하는 방법이 있습니다.
저는 두번째 방법으로 진행하겠습니다.
Ctrl+` 를 이용해서 terminal을 열고,
> flutter pub add [패키지명]↲
을 입력하면 pub.dev에서 package를 가져와서 설치해주게됩니다.
http 패키지를 설치합니다.
> flutter pub add http↲
실제 http 로 Web page를 읽어오는 class는 아래와 같습니다. 뭐 사실 한줄로도 됩니다만, 인터넷 환경이 느린 곳이나 서버가 반응이 늦은 경우 등을 대비해서 제대로 못 읽어왔을 경우 4번까지는 다시 시도하도록 code를 꾸며봤습니다.
import 'package:http/http.dart';
class WebFetcher {
static const _maxTry = 4;
static Future<Response> _getResponse(String page) async {
return await get(Uri.parse(page));
}
static Future<Response> getPage(String page) async {
Response response;
int nTry = 0;
do {
response = await _getResponse(page);
nTry++;
} while (response.statusCode != 200 && nTry < _maxTry);
return response;
}
}
몇가지 집고 넘어가겠습니다.
첫번째, 여기서 실제 class 내에서 Web page를 읽는 getPage() 함수는 return값이 Future<Response> class입니다.
Flutter에서 Future는 비동기(Asynchronous) 작업을 만들 때 사용합니다.
Future를 반환하는 async 함수들은 Main thread와 분리된 thread로 작업을 하게되며,
① 작업 중에는 Future type의 결과값을,
② 작업이 완료되면 <>안에 있는 Type의 결과값을 return합니다.
여기서는 작업이 완료되면 Response Type의 결과값을 반환합니다.
이 함수를 실행할 경우에는 작업이 끝나길 기다렸다가 결과값을 확인해야합니다. 2가지 방법이 있는데 뒤쪽에서 알아보겠습니다.
두번째, 위 code에서 retry를 하는 기준은 http status code 입니다.
_getResponse function에서 읽고자하는 page의 http 주소로 "get" 을 날렸고, 돌아온 status code가 200이면 제대로 읽혔다고 볼 수 있습니다. http status code 관련 상세내용들은 구글링해보시면 확인 가능합니다.
HTTP response status codes - HTTP | MDN
HTTP response status codes indicate whether a specific HTTP request has been successfully completed. Responses are grouped in five classes:
developer.mozilla.org
세번째, Dart 언어에서 변수 또는 함수 앞에 underbar("_")가 붙으면 internal 변수 또는 함수입니다. 해당 class 외부에서는 해당 변수/함수를 참조할 수 없습니다. WebFetcher class에서는 getPage 함수 하나만! 외부에서 사용 가능합니다.
실제 코드가 잘 동작하는지 봐야겠죠. 위에 올린 코드 블럭을 Copy해서 아까 생성한 web_fetcher.dart에 Paste해줍니다.
아마도, Response 와 get 부분에 빨간줄로 error를 표시하고 있을겁니다. error가 난 코드 위에 마우스를 가져다 올려놓으면, 아래와 같이 error의 이유와 Quick Fix... suggestion이 뜹니다. 자동으로 고쳐주겠다는거죠. 키보드 단축키로도 가능합니다. error가 발생한 코드로 커서를 이동한 후 Ctrl + . 을 입력하면 Quick Fix... 가 실행됩니다. Quick Fix... 해보겠습니다.
Quick Fix를 클릭하거나, Ctrl + . 단축키를 입력하면 아래와 같이 3가지 옵션이 보입니다.
첫번째 옵션인 Import library 'pacakge:http/http.dart' 를 선택해줍니다.
* 만약 http package가 설치되어있지않다면 아래에서 첫번째 옵션이 보이지않을겁니다. http package를 꼭 설치해주세요.
자 이제 이 코드가 실제 잘 동작하는지 시험해봅시다.
시험삼아 google 홈페이지를 읽어보겠습니다.
웹페이지 읽어보기
이제 정상 동작하는지 시험해보기위해 main.dart로 가서 floatingButton이 클릭되면 동작하는 _incrementCounter function의 첫번째 줄에 아래 코드를 추가해줍니다.
WebFetcher.getPage('https://www.google.com').then((value) => print(value.body));
main.dart 전체코드
import 'package:exchange_rate_calculator/web_fetcher.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
WebFetcher.getPage('https://www.google.com').then((value) => print(value.body));
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Invoke "debug painting" (press "p" in the console, choose the
// "Toggle Debug Paint" action from the Flutter Inspector in Android
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
// to see the wireframe for each widget.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
이제 F5로 실행 후, 실행된 App에서 FloatingButton을 클릭해보면 DEBUG CONSOLE에 google 홈페이지의 html 코드가 print되어있을겁니다.
추가 설명
자! 여기서 마지막으로 2가지 정도 언급하고 가야겠네요. 마지막에 추가한 아래 코드에 대해서요.
WebFetcher.getPage('https://www.google.com').then((value) => print(value.body));
① Flutter에서는 String을 표시할 때 홑따옴표와 겹따옴표 둘 다 사용 가능합니다. 위 코드는 아래와 완전히 동일합니다.
WebFetcher.getPage("https://www.google.com").then((value) => print(value.body));
② then은 뭘까요? 왜 일반적인 다른 programming 언어에서와 같이 아래와 같은 형태로 구현하지않았을까요?
print(WebFetcher.getPage('https://www.google.com'));
실제 위와 같이 구현시, DEBUG CONSOLE에서 아래와 같은 결과를 볼 수 있습니다.
앞서 설명했듯이 WebFetcher.getPage()는 Future를 반환하는 함수이며, asynchronous로 동작합니다. 결국 main thread에서는 언제 값이 return될지 모르고 있는 상황입니다. 그래서 위와 같이 함수의 결과값을 바로 참조하면 Future<Response> class라는 정보만 반환하게됩니다.
이런 경우, 2가지 방법이 있습니다. then을 사용하는 방법과 await을 사용하는 방법인데요. 둘 사이에 약간의 차이가 있습니다.
WebFetcher.getPage('https://www.google.com').then((value) => print(value.body));
print((await WebFetcher.getPage('https://www.google.com')).body);
둘의 차이점은 위 코드가 아닌, 뒤쪽에 따라붙는 이후 코드들이 언제 실행되느냐입니다.
then을 사용하는 경우에는, then 의 parameter에 해당하는 function을 예약해두고 뒤쪽의 코드가 지연없이 쭉 진행됩니다. Future 작업이 완료되어 값이 반환되면 그 때 then 의 parameter에 해당하는 function이 실행되게됩니다.
await을 사용하는 경우는 뒤쪽의 코드를 실행하지않고, Future가 값을 반환할때까지 그냥 쭉~ 기다리게됩니다.
감이 오시나요?
예를 들어, 웹에서 문서를 읽어오고, 읽어온 문서의 내용을 수정하는 코드가 뒤에 연달아있다면 await으로 문서를 읽어오길 기다리는게 맞을겁니다. 하지만 웹에서 문서를 읽어오는 코드 뒤쪽에 문서의 내용이 아닌 다른 할 일에 대한 코드가 연달아있다면, then으로 작업 예약을 걸어두고 다른 일을 빨리 처리하는게 맞겠죠.
Future에 대한 상세한 내용은 별도의 포스팅을 한 번 준비하겠습니다.
주의! 아래 환율 가져오기 내용에서 참조했던 KEB하나 은행 API는 해당 사이트가 더 이상 환율 제공 API 서비스를 제공하지않아서, 새로운 방법으로 작성했습니다. 아래 포스팅으로 이동하시면, 새로운 API 서비스 기반으로 작성한 앱이 있습니다. 2022.10.09 - [Flutter] - [Flutter 코딩] 두번째 Flutter App 환율 계산기 - 전체 코드 및 실행 파일 |
2. 환율 가져오기
웹페이지를 읽어오는 함수를 구현했으니, 이제 공개되어있는 환율 정보를 읽어보겠습니다.
환율을 공지하는 사이트는 여러 종류가 있습니다. Google, Naver, Daum과 같은 포털사이트에서 찾는 방법도 있겠습니다만, 한가지 문제는 웹페이지의 형태가 변동되는 경우, 매번 코드를 수정해줘야하는 문제가 발생합니다.
① 환율을 지속 업데이트해주고,
② 믿을 수 있는 정확한 정보여야하고,
③ 최대한 웹페이지 형태의 변화가 없는
사이트를 원합니다.
구글링을 통해 KEB하나에서 제공하는 페이지를 하나 찾았습니다. 정확히는...
웹페이지라기보다는 정보를 제공하는 api인데, 별도의 인증키 등이 필요없고, 일반 웹페이지와 동일하게 브라우져상에서 결과를 확인할 수 있습니다.
http://fx.kebhana.com/FER1101M.web
위 링크로 들어가보시면 아래와 같이 Map 형태의 변수값 하나를 보여주고있습니다.
var exView = { "날짜": "2022년 09월 20일 20:04", "리스트": [ { "통화명": "미국 USD", "현찰사실때":"1418.39", "현찰파실때":"1369.61", "송금_전신환보내실때":"1407.60", "송금_전신환받으실때":"1380.40", "매매기준율":"1394.00" }, { "통화명": "일본 JPY 100", "현찰사실때":"988.87", "현찰파실때":"954.87", "송금_전신환보내실때":"981.39", "송금_전신환받으실때":"962.35", "매매기준율":"971.87" }, { "통화명": "유로 EUR", "현찰사실때":"1420.95", "현찰파실때":"1365.51", "송금_전신환보내실때":"1407.16", "송금_전신환받으실때":"1379.30", "매매기준율":"1393.23" }, { "통화명": "중국 CNY", "현찰사실때":"208.58", "현찰파실때":"188.72", "송금_전신환보내실때":"200.63", "송금_전신환받으실때":"196.67", "매매기준율":"198.65" }, { "통화명": "홍콩 HKD", "현찰사실때":"181.08", "현찰파실때":"174.10", "송금_전신환보내실때":"179.36", "송금_전신환받으실때":"175.82", "매매기준율":"177.59" }, { "통화명": "태국 THB", "현찰사실때":"39.54", "현찰파실때":"35.41", "송금_전신환보내실때":"38.03", "송금_전신환받으실때":"37.29", "매매기준율":"37.66" }, { "통화명": "대만 TWD", "현찰사실때":"50.13", "현찰파실때":"41.23", "송금_전신환보내실때":"0.00", "송금_전신환받으실때":"0.00", "매매기준율":"44.33" }, { "통화명": "필리핀 PHP", "현찰사실때":"26.71", "현찰파실때":"22.30", "송금_전신환보내실때":"24.53", "송금_전신환받으실때":"24.05", "매매기준율":"24.29" }, { "통화명": "싱가포르 SGD", "현찰사실때":"1008.86", "현찰파실때":"969.50", "송금_전신환보내실때":"999.07", "송금_전신환받으실때":"979.29", "매매기준율":"989.18" }, { "통화명": "호주 AUD", "현찰사실때":"950.46", "현찰파실때":"913.74", "송금_전신환보내실때":"941.42", "송금_전신환받으실때":"922.78", "매매기준율":"932.10" }, { "통화명": "베트남 VND 100", "현찰사실때":"6.58", "현찰파실때":"5.20", "송금_전신환보내실때":"5.94", "송금_전신환받으실때":"5.84", "매매기준율":"5.89" }, { "통화명": "영국 GBP", "현찰사실때":"1617.69", "현찰파실때":"1555.19", "송금_전신환보내실때":"1602.30", "송금_전신환받으실때":"1570.58", "매매기준율":"1586.44" }, { "통화명": "캐나다 CAD", "현찰사실때":"1068.52", "현찰파실때":"1027.24", "송금_전신환보내실때":"1058.35", "송금_전신환받으실때":"1037.41", "매매기준율":"1047.88" }, { "통화명": "말레이시아 MYR", "현찰사실때":"325.67", "현찰파실때":"283.70", "송금_전신환보내실때":"0.00", "송금_전신환받으실때":"303.31", "매매기준율":"306.37" }, { "통화명": "러시아 RUB", "현찰사실때":"25.40", "현찰파실때":"19.14", "송금_전신환보내실때":"24.01", "송금_전신환받으실때":"22.39", "매매기준율":"23.20" }, { "통화명": "남아공화국 ZAR", "현찰사실때":"83.46", "현찰파실때":"72.45", "송금_전신환보내실때":"79.68", "송금_전신환받으실때":"77.80", "매매기준율":"78.74" }, { "통화명": "노르웨이 NOK", "현찰사실때":"139.06", "현찰파실때":"132.42", "송금_전신환보내실때":"137.09", "송금_전신환받으실때":"134.39", "매매기준율":"135.74" }, { "통화명": "뉴질랜드 NZD", "현찰사실때":"846.62", "현찰파실때":"813.92", "송금_전신환보내실때":"838.57", "송금_전신환받으실때":"821.97", "매매기준율":"830.27" }, { "통화명": "덴마크 DKK", "현찰사실때":"191.91", "현찰파실때":"182.75", "송금_전신환보내실때":"189.20", "송금_전신환받으실때":"185.46", "매매기준율":"187.33" }, { "통화명": "멕시코 MXN", "현찰사실때":"76.20", "현찰파실때":"63.47", "송금_전신환보내실때":"69.97", "송금_전신환받으실때":"68.59", "매매기준율":"69.28" }, { "통화명": "몽골 MNT", "현찰사실때":"0.00", "현찰파실때":"0.00", "송금_전신환보내실때":"0.00", "송금_전신환받으실때":"0.00", "매매기준율":"0.43" }, { "통화명": "바레인 BHD", "현찰사실때":"3934.36", "현찰파실때":"3401.90", "송금_전신환보내실때":"3734.68", "송금_전신환받으실때":"3660.74", "매매기준율":"3697.71" }, { "통화명": "방글라데시 BDT", "현찰사실때":"0.00", "현찰파실때":"0.00", "송금_전신환보내실때":"0.00", "송금_전신환받으실때":"0.00", "매매기준율":"13.47" }, { "통화명": "브라질 BRL", "현찰사실때":"292.36", "현찰파실때":"244.08", "송금_전신환보내실때":"0.00", "송금_전신환받으실때":"262.12", "매매기준율":"265.30" }, { "통화명": "브루나이 BND", "현찰사실때":"1028.74", "현찰파실때":"929.83", "송금_전신환보내실때":"0.00", "송금_전신환받으실때":"0.00", "매매기준율":"989.18" }, { "통화명": "사우디아라비아 SAR", "현찰사실때":"394.28", "현찰파실때":"345.33", "송금_전신환보내실때":"374.62", "송금_전신환받으실때":"367.22", "매매기준율":"370.92" }, { "통화명": "스리랑카 LKR", "현찰사실때":"0.00", "현찰파실때":"0.00", "송금_전신환보내실때":"0.00", "송금_전신환받으실때":"0.00", "매매기준율":"3.83" }, { "통화명": "스웨덴 SEK", "현찰사실때":"132.25", "현찰파실때":"125.93", "송금_전신환보내실때":"130.38", "송금_전신환받으실때":"127.80", "매매기준율":"129.09" }, { "통화명": "스위스 CHF", "현찰사실때":"1472.79", "현찰파실때":"1415.89", "송금_전신환보내실때":"1458.78", "송금_전신환받으실때":"1429.90", "매매기준율":"1444.34" }, { "통화명": "아랍에미리트공화국 AED", "현찰사실때":"400.39", "현찰파실때":"353.34", "송금_전신환보내실때":"383.31", "송금_전신환받으실때":"375.73", "매매기준율":"379.52" }, { "통화명": "알제리 DZD", "현찰사실때":"0.00", "현찰파실때":"0.00", "송금_전신환보내실때":"0.00", "송금_전신환받으실때":"0.00", "매매기준율":"9.92" }, { "통화명": "오만 OMR", "현찰사실때":"3948.15", "현찰파실때":"3407.97", "송금_전신환보내실때":"0.00", "송금_전신환받으실때":"0.00", "매매기준율":"3625.49" }, { "통화명": "요르단 JOD", "현찰사실때":"2141.13", "현찰파실때":"1808.86", "송금_전신환보내실때":"0.00", "송금_전신환받으실때":"0.00", "매매기준율":"1966.15" }, { "통화명": "이스라엘 ILS", "현찰사실때":"444.93", "현찰파실때":"372.14", "송금_전신환보내실때":"0.00", "송금_전신환받으실때":"0.00", "매매기준율":"404.49" }, { "통화명": "이집트 EGP", "현찰사실때":"0.00", "현찰파실때":"0.00", "송금_전신환보내실때":"0.00", "송금_전신환받으실때":"0.00", "매매기준율":"71.78" }, { "통화명": "인도 INR", "현찰사실때":"0.00", "현찰파실때":"0.00", "송금_전신환보내실때":"0.00", "송금_전신환받으실때":"0.00", "매매기준율":"17.48" }, { "통화명": "인도네시아 IDR 100", "현찰사실때":"9.98", "현찰파실때":"8.40", "송금_전신환보내실때":"9.42", "송금_전신환받으실때":"9.24", "매매기준율":"9.33" }, { "통화명": "체코 CZK", "현찰사실때":"61.74", "현찰파실때":"51.79", "송금_전신환보내실때":"57.53", "송금_전신환받으실때":"56.29", "매매기준율":"56.91" }, { "통화명": "칠레 CLP", "현찰사실때":"1.63", "현찰파실때":"1.39", "송금_전신환보내실때":"0.00", "송금_전신환받으실때":"0.00", "매매기준율":"1.51" }, { "통화명": "카자흐스탄 KZT", "현찰사실때":"0.00", "현찰파실때":"0.00", "송금_전신환보내실때":"0.00", "송금_전신환받으실때":"0.00", "매매기준율":"2.92" }, { "통화명": "카타르 QAR", "현찰사실때":"0.00", "현찰파실때":"0.00", "송금_전신환보내실때":"0.00", "송금_전신환받으실때":"0.00", "매매기준율":"381.32" }, { "통화명": "케냐 KES", "현찰사실때":"0.00", "현찰파실때":"0.00", "송금_전신환보내실때":"0.00", "송금_전신환받으실때":"0.00", "매매기준율":"11.56" }, { "통화명": "콜롬비아 COP", "현찰사실때":"0.00", "현찰파실때":"0.00", "송금_전신환보내실때":"0.00", "송금_전신환받으실때":"0.00", "매매기준율":"0.31" }, { "통화명": "쿠웨이트 KWD", "현찰사실때":"4804.56", "현찰파실때":"4150.43", "송금_전신환보내실때":"4556.44", "송금_전신환받으실때":"4466.22", "매매기준율":"4511.33" }, { "통화명": "탄자니아 TZS", "현찰사실때":"0.00", "현찰파실때":"0.00", "송금_전신환보내실때":"0.00", "송금_전신환받으실때":"0.00", "매매기준율":"0.60" }, { "통화명": "터키 TRY", "현찰사실때":"0.00", "현찰파실때":"0.00", "송금_전신환보내실때":"77.07", "송금_전신환받으실때":"75.41", "매매기준율":"76.24" }, { "통화명": "파키스탄 PKR", "현찰사실때":"0.00", "현찰파실때":"0.00", "송금_전신환보내실때":"0.00", "송금_전신환받으실때":"0.00", "매매기준율":"5.86" }, { "통화명": "폴란드 PLN", "현찰사실때":"319.45", "현찰파실때":"272.13", "송금_전신환보내실때":"299.04", "송금_전신환받으실때":"292.54", "매매기준율":"295.79" }, { "통화명": "헝가리 HUF", "현찰사실때":"3.79", "현찰파실때":"3.20", "송금_전신환보내실때":"3.50", "송금_전신환받으실때":"3.44", "매매기준율":"3.47" }, { "통화명": "네팔 NPR", "현찰사실때":"0.00", "현찰파실때":"0.00", "송금_전신환보내실때":"0.00", "송금_전신환받으실때":"0.00", "매매기준율":"10.92" }, { "통화명": "마카오 MOP", "현찰사실때":"0.00", "현찰파실때":"0.00", "송금_전신환보내실때":"0.00", "송금_전신환받으실때":"0.00", "매매기준율":"172.29" }, { "통화명": "캄보디아 KHR", "현찰사실때":"0.00", "현찰파실때":"0.00", "송금_전신환보내실때":"0.00", "송금_전신환받으실때":"0.00", "매매기준율":"0.34" }, { "통화명": "피지 FJD", "현찰사실때":"0.00", "현찰파실때":"0.00", "송금_전신환보내실때":"0.00", "송금_전신환받으실때":"0.00", "매매기준율":"619.35" }, { "통화명": "리비아 LYD", "현찰사실때":"0.00", "현찰파실때":"0.00", "송금_전신환보내실때":"283.90", "송금_전신환받으실때":"278.28", "매매기준율":"281.09" }, { "통화명": "루마니아 RON", "현찰사실때":"0.00", "현찰파실때":"0.00", "송금_전신환보내실때":"0.00", "송금_전신환받으실때":"0.00", "매매기준율":"282.62" }, { "통화명": "미얀마 MMK", "현찰사실때":"0.00", "현찰파실때":"0.00", "송금_전신환보내실때":"0.00", "송금_전신환받으실때":"0.00", "매매기준율":"0.66" }, { "통화명": "에티오피아 ETB", "현찰사실때":"0.00", "현찰파실때":"0.00", "송금_전신환보내실때":"0.00", "송금_전신환받으실때":"0.00", "매매기준율":"26.39" }, { "통화명": "우즈베키스탄 UZS", "현찰사실때":"0.00", "현찰파실때":"0.00", "송금_전신환보내실때":"0.00", "송금_전신환받으실때":"0.00", "매매기준율":"0.13" }, ] }
자 이제 저 값을 이용해서 환율을 어떻게 읽어올지 작전을 짜봐야겠습니다.
우선 위와 같은 형태의 데이터를 json format이라고합니다. json(JavaScript Object Notation) 은 다양한 상황에서 데이터 저장하기/읽어오기 할 때 사용하게됩니다. google에서 제공하는 firestore 데이터베이스에서도 json format으로 데이터를 저장하고 읽어옵니다. firebase 데이터 베이스는 다음 앱에서 다뤄보려고합니다.
json 포맷에 대해서도 포스팅 여러 개가 필요한 정도의 분량입니다만, 이 포스팅에서는 json 자체에 대해서는 다루지않겠습니다.
먼저 Regular Expression을 사용해 각국의 환율 정보가 담긴 String을 분리해내고,
다음에는 jsonDecode 함수를 사용해서 String을 Map으로 변환해보겠습니다.
환율 읽어오기 구현
앞서 만들었던 exchange_rate_calculator 프로젝트에서
새로운 dart 파일을 하나 생성하고, exchange_rate.dart라는 이름을 붙여주겠습니다.
그리고 생성한 파일에 환율 정보를 관리할 class를 하나 만듭니다.
전체 코드는 아래와 같습니다.
readRate() 함수를 실행하면, 간단하게 WebFetcher.getPage로 환율 사이트에서 정보를 읽어오고, console창에 출력하는 코드입니다.
import 'package:exchange_rate_calculator/web_fetcher.dart';
import 'package:http/http.dart';
class ExchangeRate{
final String _webPage = 'http://fx.kebhana.com/FER1101M.web';
Future<void> readRate() async{
Response page = await WebFetcher.getPage(_webPage);
print(page.body);
}
}
한 번 실행해보겠습니다.
main.dart로 이동해서, FloatingButton 클릭시 실행되는 _incrementCounter() 함수의 첫번째 줄에 아래 코드를 추가합니다.
ExchangeRate rate = ExchangeRate();
rate.readRate();
main.dart의 전체 코드는 아래와 같습니다.
import 'package:exchange_rate_calculator/exchange_rate.dart';
import 'package:exchange_rate_calculator/web_fetcher.dart';
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
// This is the theme of your application.
//
// Try running your application with "flutter run". You'll see the
// application has a blue toolbar. Then, without quitting the app, try
// changing the primarySwatch below to Colors.green and then invoke
// "hot reload" (press "r" in the console where you ran "flutter run",
// or simply save your changes to "hot reload" in a Flutter IDE).
// Notice that the counter didn't reset back to zero; the application
// is not restarted.
primarySwatch: Colors.blue,
),
home: const MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key, required this.title});
// This widget is the home page of your application. It is stateful, meaning
// that it has a State object (defined below) that contains fields that affect
// how it looks.
// This class is the configuration for the state. It holds the values (in this
// case the title) provided by the parent (in this case the App widget) and
// used by the build method of the State. Fields in a Widget subclass are
// always marked "final".
final String title;
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
ExchangeRate rate = ExchangeRate();
rate.readRate();
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
}
@override
Widget build(BuildContext context) {
// This method is rerun every time setState is called, for instance as done
// by the _incrementCounter method above.
//
// The Flutter framework has been optimized to make rerunning build methods
// fast, so that you can just rebuild anything that needs updating rather
// than having to individually change instances of widgets.
return Scaffold(
appBar: AppBar(
// Here we take the value from the MyHomePage object that was created by
// the App.build method, and use it to set our appbar title.
title: Text(widget.title),
),
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Invoke "debug painting" (press "p" in the console, choose the
// "Toggle Debug Paint" action from the Flutter Inspector in Android
// Studio, or the "Toggle Debug Paint" command in Visual Studio Code)
// to see the wireframe for each widget.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
실행 후 FloatingButton을 클릭해주면 잘(?) 실행됩니다.
간단히 될 줄 알았는데... ㅠㅠ 문제가 발생했습니다. charset이 맞지않아서 문자가 다 깨져서나오네요.
dart언어에서는 unicode 문자를 사용하는데, 읽어온 페이지가 unicode를 쓰지않고 있습니다.
졸지에, 변환기도 만들어야겠네요.
까짓거 해보겠습니다.
좀 복잡한 방법으로 해당 charset이 EUC-KR 이라는걸 알았습니다.
EUC-KR의 확장판이 CP949이고, CP949와 unicode간의 변환테이블을 구글링해서 찾고, 해당 맵을 이용해서 변환용 코드를 만들어봤습니다. 너무 코드가 길어져서 포스팅이 느려지니, 코드는 줄여서 올리고 전체 파일을 올려놓겠습니다.
var _cp949toUniConversionTable = {
0x00: 0x0000,
0x01: 0x0001,
0x02: 0x0002,
0x03: 0x0003,
// omitted~~~~~~~~~~~
0xFDFB: 0x79A7,
0xFDFC: 0x7A00,
0xFDFD: 0x7FB2,
0xFDFE: 0x8A70
};
String cp949toUni(List<int> codeBytes) {
List<int> uniCodes = [];
bool isExtended = false;
int previousChar = 0;
for (var char in codeBytes) {
if (isExtended) {
isExtended = false;
uniCodes.add(_cp949toUniConversionTable[(previousChar << 8) + char]!);
continue;
} else if (char >= 0x80) {
isExtended = true;
previousChar = char;
continue;
} else {
isExtended = false;
uniCodes.add(_cp949toUniConversionTable[char]!);
}
}
return String.fromCharCodes(uniCodes);
}
뭐 사실 역변환 코드도 만들고해야겠지만서도... 여기서는 딱 필요한 cp949 → utf-8 변환만 만들었습니다.
실제 변환 코드는 아주 간단합니다.
① 문자를 1byte단위로 읽고,
② 해당 byte가 8bit(=1byte) 코드면 8bit 코드로
③ 16bit 코드면(0x80보다 크면) 다음 byte를 한 번 더 읽어서 합쳐서 변환합니다.
위 첨부 파일을 다운로드해서 VS code상의 Explorer(좌측 사이드바)에 \lib 폴더 안에 끌어다가(Drag & Drop) 넣습니다.
해당 변환을 적용해서 다시 한 번 실행해보겠습니다.
exchage_rate.dart에서 charset 변환함수를 사용하기 위해 아래와 같이 수정합니다.
import 'package:exchange_rate_calculator/cp949_uni_conversion.dart';
import 'package:exchange_rate_calculator/web_fetcher.dart';
import 'package:http/http.dart';
class ExchangeRate{
final String _webPage = 'http://fx.kebhana.com/FER1101M.web';
Future<void> readRate() async{
Response page = await WebFetcher.getPage(_webPage);
print(cp949toUni(page.body.codeUnits));
}
}
F5키로 실행하고 FloatingButton을 클릭하면 이제 문자가 깨지지않고 잘 보일겁니다.
자! 이제 json 포맷의 String을 Map 형태로 변경하면 됩니다.
ExchangeRate class안 에 json 변환기를 만들어보았습니다.
① Regular Expression을 사용해서 각 국의 환율 정보가 담긴 String을 국별로 분리해내고,
② 추출된 String은 dart에서 기본적으로 제공하는 jsonDecode를 이용해서 Map 형태로 변환합니다.
import 'dart:convert';
import 'package:exchange_rate_calculator/cp949_uni_conversion.dart';
import 'package:exchange_rate_calculator/web_fetcher.dart';
import 'package:http/http.dart';
class ExchangeRate{
final String _webPage = 'http://fx.kebhana.com/FER1101M.web';
Future<void> readRate() async{
Response page = await WebFetcher.getPage(_webPage);
String pageString = cp949toUni(page.body.codeUnits);
List rates = _readJson(pageString);
print(rates);
}
List _readJson(String json){
List rates = [];
RegExp re = RegExp(r'\{[^\{\}]*\}');
Iterable list = re.allMatches(json);
for (var item in list){
rates.add(jsonDecode(item.group(0).toString()));
}
return rates;
}
}
Regular Expression도 포스팅 몇개는 필요한데... 오늘은 자세한 설명을 Skip하겠습니다.
위에서 사용한 Regular Expression( \{[^\{\}]*\} )만 간단히 해석해보죠.
\{ : { 로 시작하고,
[^\{\}]* : { 또는 } 가 아닌 문자가 다수 개(*) 존재하고,
\} : }로 끝날 것
요걸로 원래의 String을 쪼개고, 쪼개진 String은 jsonDecode를 써서 Map 형태로 변환했습니다.
이제 실행해보면, 한 국가의 환율정보가 하나의 Map에 들어가고, 국가별 환율정보 Map이 하나의 List로 모여있습니다.
자, 이제 잘 실행되는 건 알았으니, 마지막으로 값을 잘 Return하도록 readRate 함수를 살짝 변경합니다.
Return값 변경
우선, 첫번째로 각 국가별로 나누어진 Map에서 필요한 정보만 추출합니다. 통화명과 매매기준율만 추출하게 됩니다.
아래 부분입니다.
List<Map<String, double>> rates = fullRates.map((e) {
return {e.values.first.toString():double.parse(e.values.last.trim())};
},).toList();
fullRates는 각 국가별 정보가 모여있는 Map의 List이고,
map이라는 List class의 method로 List 안의 각 element 즉 국가별 정보 Map에 작업을 합니다.
해당 작업은 Map의 값들 중 첫번째 값을 key, Map의 값들 중 마지막 값을 value로 갖는 Map을 반환하는 작업이고,
작업된 return값들을 모아 List로 변환해줍니다.
한국 통화 추가
두번째로, 한국의 통화를 추가합니다. 현재 데이타에는 한국의 통화 정보가 없으니까요.
rates.insert(0, {'대한민국 KRW':1.0});
static으로 변경
세번째로, 해당 class의 함수들을 class instance를 만들지 않고도 바로바로 쓸 수 있도록 static으로 변화해주겠습니다.
class 함수들은 static인 경우와 아닌 경우, 해당 함수들을 쓰는 방법의 차이가 발생합니다.
static이 아닌 경우
ExchangeRate exchangeRates = ExchangeRate();
Future<List<Map<String, double> rates = exchangeRates.readRate();
static인 경우
Future<List<Map<String, double> rates = ExchangeRates.readRate();
위와같이 static 인 경우 별도로 instance를 생성하지않아도되므로, class method(함수)를 주로 사용하는 경우 저는 static으로 구현하고 있습니다.
Locale 코드 추가
추후에 자세한 설명을 하겠습니다만, 최종 UI에서 각국의 통화별로 화폐단위를 제대로 표현해주려고합니다.
그러려면 intl 이라는 library를 사용하게되는데, 각 국가의 Locale 정보가 필요합니다.
googling을 통해 각 국가별로 Locale을 확인했고, 변수로 등록 시켜 줍니다.
static const currencyLocale = {
"대한민국 KRW": "ko_KR",
"미국 USD": "en_US",
"일본 JPY 100": "ja_JP",
"유로 EUR": "en_IE",
"중국 CNY": "zh_CN",
"홍콩 HKD": "zh_HK",
"태국 THB": "th_TH",
"대만 TWD": "zh_TW",
"필리핀 PHP": "en_PH",
"싱가포르 SGD": "zh_SG",
"호주 AUD": "en_AU",
"베트남 VND 100": "vi_VN",
"영국 GBP": "en_GB",
"캐나다 CAD": "en_CA",
"말레이시아 MYR": "ms_MY",
"러시아 RUB": "ru_RU",
"남아공화국 ZAR": "en_ZA",
"노르웨이 NOK": "nb_NO",
"뉴질랜드 NZD": "en_NZ",
"덴마크 DKK": "da_DK",
"멕시코 MXN": "es_MX",
"몽골 MNT": "mn_MN",
"바레인 BHD": "ar_BH",
"방글라데시 BDT": "bn_BD",
"브라질 BRL": "pt_BR",
"브루나이 BND": "ms_BN",
"사우디아라비아 SAR": "ar_SA",
"스리랑카 LKR": "si_LK",
"스웨덴 SEK": "sv_SE",
"스위스 CHF": "de_CH",
"아랍에미리트공화국 AED": "ar_AE",
"알제리 DZD": "ar_DZ",
"오만 OMR": "ar_OM",
"요르단 JOD": "ar_JO",
"이스라엘 ILS": "he_IL",
"이집트 EGP": "ar_EG",
"인도 INR": "hi_IN",
"인도네시아 IDR 100": "id_ID",
"체코 CZK": "cs_CZ",
"칠레 CLP": "es_CL",
"카자흐스탄 KZT": "kk_KZ",
"카타르 QAR": "ar_QA",
"케냐 KES": "sw_KE",
"콜롬비아 COP": "es_CO",
"쿠웨이트 KWD": "ar_KW",
"탄자니아 TZS": "en_TZ",
"터키 TRY": "tr_TR",
"파키스탄 PKR": "ur_PK",
"폴란드 PLN": "pl_PL",
"헝가리 HUF": "hu_HU",
"네팔 NPR": "ne_NP",
"마카오 MOP": "en_MO",
"캄보디아 KHR": "km_KH",
"피지 FJD": "en_FJ",
"리비아 LYD": "ar_LY",
"루마니아 RON": "ro_RO",
"미얀마 MMK": "my_MM",
"에티오피아 ETB": "am_ET",
"우즈베키스탄 UZS": "uz_UZ",
};
전체 코드는 아래와 같습니다.
import 'dart:convert';
import 'package:exchange_rate_calculator/cp949_uni_conversion.dart';
import 'package:exchange_rate_calculator/web_fetcher.dart';
import 'package:http/http.dart';
class ExchangeRate {
static const currencyLocale = {
"대한민국 KRW": "ko_KR",
"미국 USD": "en_US",
"일본 JPY 100": "ja_JP",
"유로 EUR": "en_IE",
"중국 CNY": "zh_CN",
"홍콩 HKD": "zh_HK",
"태국 THB": "th_TH",
"대만 TWD": "zh_TW",
"필리핀 PHP": "en_PH",
"싱가포르 SGD": "zh_SG",
"호주 AUD": "en_AU",
"베트남 VND 100": "vi_VN",
"영국 GBP": "en_GB",
"캐나다 CAD": "en_CA",
"말레이시아 MYR": "ms_MY",
"러시아 RUB": "ru_RU",
"남아공화국 ZAR": "en_ZA",
"노르웨이 NOK": "nb_NO",
"뉴질랜드 NZD": "en_NZ",
"덴마크 DKK": "da_DK",
"멕시코 MXN": "es_MX",
"몽골 MNT": "mn_MN",
"바레인 BHD": "ar_BH",
"방글라데시 BDT": "bn_BD",
"브라질 BRL": "pt_BR",
"브루나이 BND": "ms_BN",
"사우디아라비아 SAR": "ar_SA",
"스리랑카 LKR": "si_LK",
"스웨덴 SEK": "sv_SE",
"스위스 CHF": "de_CH",
"아랍에미리트공화국 AED": "ar_AE",
"알제리 DZD": "ar_DZ",
"오만 OMR": "ar_OM",
"요르단 JOD": "ar_JO",
"이스라엘 ILS": "he_IL",
"이집트 EGP": "ar_EG",
"인도 INR": "hi_IN",
"인도네시아 IDR 100": "id_ID",
"체코 CZK": "cs_CZ",
"칠레 CLP": "es_CL",
"카자흐스탄 KZT": "kk_KZ",
"카타르 QAR": "ar_QA",
"케냐 KES": "sw_KE",
"콜롬비아 COP": "es_CO",
"쿠웨이트 KWD": "ar_KW",
"탄자니아 TZS": "en_TZ",
"터키 TRY": "tr_TR",
"파키스탄 PKR": "ur_PK",
"폴란드 PLN": "pl_PL",
"헝가리 HUF": "hu_HU",
"네팔 NPR": "ne_NP",
"마카오 MOP": "en_MO",
"캄보디아 KHR": "km_KH",
"피지 FJD": "en_FJ",
"리비아 LYD": "ar_LY",
"루마니아 RON": "ro_RO",
"미얀마 MMK": "my_MM",
"에티오피아 ETB": "am_ET",
"우즈베키스탄 UZS": "uz_UZ",
};
static const String _webPage = 'http://fx.kebhana.com/FER1101M.web';
static const currencyKorea = '대한민국 KRW';
static Future<List<Map<String, double>>> readRate() async {
Response page = await WebFetcher.getPage(_webPage);
String pageString = cp949toUni(page.body.codeUnits);
List<Map> fullRates = _readJson(pageString);
List<Map<String, double>> rates = fullRates.map(
(e) {
return {
e.values.first.toString(): 1.0 / double.parse(e.values.last.trim())
};
},
).toList();
rates.insert(0, {currencyKorea: 1.0});
return rates;
}
static List<Map> _readJson(String json) {
List<Map> rates = [];
RegExp re = RegExp(r'\{[^\{\}]*\}');
Iterable list = re.allMatches(json);
for (var item in list) {
rates.add(jsonDecode(item.group(0).toString()));
}
return rates;
}
}
마지막으로 지금까지 작업해본 dart 파일들을 첨부로 올리고, 다음 포스팅부터는 화면에 보이는 UI를 만들어보겠습니다.
좋은 하루 되세요~~
'Flutter' 카테고리의 다른 글
두번째 Flutter App - 환율 계산기 만들기 - 전체 코드 및 실행 파일 (0) | 2022.10.09 |
---|---|
두번째 Flutter App - 환율 계산기 만들기 2 (0) | 2022.09.21 |
샘플 Flutter App build/배포 하기 + Code 분석해보기 (0) | 2022.09.17 |
Flutter 설치하기 (Visual Studio Code 설치) + 첫번째 Flutter App 만들어보기 (0) | 2022.09.12 |
Flutter 설치하기 (Flutter SDK, Cross-Platform 환경) (0) | 2022.09.11 |