이번에는 추가 데이터(대기질 및 미세먼지) 매핑 및 해당 데이터 모델을 만드는 방법에 대해서 알아보겠습니다.
1. 미세먼지 관련 api 추가
2. 데이터 모델링 추가(참고자료 - [Flutter] App Weather - 3.5단계 model of openweathermap)
3. 코드 정리(리팩토링)
개발환경 : 윈도우11, 안드로이드 스튜디오(Arctic Fox 2020.3.1 Patch 4), flutter 2.8.1
소스코드 위치 - Release airData_Model · mike-bskim/weather (github.com)
1. current_air.dart - 미세먼지 모델링을 추가합니다.
관련 api 사이트 - https://openweathermap.org/api/air-pollution
샘플 api - [https://api.openweathermap.org/data/2.5/air_pollution?lat=45.4642033&lon=9.1899817&appid=1e1a2b8f6d9b5311cd82d001e7b20131&units=metric&lang=kr]
샘플 api 의 응답 json 데이터는 아래와 같다.
응답데이트를 이용해서 "Json To Dart" 변환시, 오류가 발생하는데, 원인은 JSON 데이터중 키 값이 "list" 로 된 부분이 있어서 Dart 문법 예약어 때문에 오류가 발생하여 "listed" 로 변경하고 변환하면 오류가 없어진다.
대신, 해당 클레스는 아래와 같이 수정이 필요하다.
// json 형식의 응답 데이터
{
coord: {lon: 9.19, lat: 45.4642},
list: [
{main: {aqi: 3},
components: {
co: 337.12, no: 0.46, no2: 5.48, o3: 144.48, so2: 1.88,
pm2_5: 24.62, pm10: 26.92, nh3: 9.25},
dt: 1651237200
}
]
}
모델링 파일을 만들때 키값을 "list" 에서 "listed" 라고 변경하였기 때문에 실제 데이터를 파싱할때도 "listed" 를 키값으로 파싱을 시도하기 때문에(데이터를 찾기 때문에) 실제 데이터를 가져오는 부분에서는 키값을 "list" 로 다시 수정하였음.
상세 코드는 "더보기" 클릭하세요.
/// coord : {"lon":9.19,"lat":45.4642}
/// listed : [{"main":{"aqi":3},"components":{"co":337.12,"no":0.46,"no2":5.48,"o3":144.48,"so2":1.88,"pm2_5":24.62,"pm10":26.92,"nh3":9.25},"dt":1651237200}]
class CurrentAir {
Coord? coord;
List<Listed>? listed;
CurrentAir({
this.coord,
this.listed,});
CurrentAir.fromJson(dynamic json) {
coord = json['coord'] != null ? Coord.fromJson(json['coord']) : null;
if (json['list'] != null) { // 여기를 다시 수정함
listed = [];
json['list'].forEach((v) { // 여기를 다시 수정함
listed?.add(Listed.fromJson(v));
});
}
}
// Map<String, dynamic> toJson() {
// final map = <String, dynamic>{};
// if (coord != null) {
// map['coord'] = coord?.toJson();
// }
// if (listed != null) {
// map['listed'] = listed?.map((v) => v.toJson()).toList();
// }
// return map;
// }
}
/// main : {"aqi":3}
/// components : {"co":337.12,"no":0.46,"no2":5.48,"o3":144.48,"so2":1.88,"pm2_5":24.62,"pm10":26.92,"nh3":9.25}
/// dt : 1651237200
class Listed {
Listed({
this.main,
this.components,
this.dt,});
Listed.fromJson(dynamic json) {
main = json['main'] != null ? Main.fromJson(json['main']) : null;
components = json['components'] != null ? Components.fromJson(json['components']) : null;
dt = json['dt'];
}
Main? main;
Components? components;
int? dt;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
if (main != null) {
map['main'] = main?.toJson();
}
if (components != null) {
map['components'] = components?.toJson();
}
map['dt'] = dt;
return map;
}
}
/// co : 337.12
/// no : 0.46
/// no2 : 5.48
/// o3 : 144.48
/// so2 : 1.88
/// pm2_5 : 24.62
/// pm10 : 26.92
/// nh3 : 9.25
class Components {
Components({
this.co,
this.no,
this.no2,
this.o3,
this.so2,
this.pm2_5,
this.pm10,
this.nh3,});
Components.fromJson(dynamic json) {
co = json['co'];
no = json['no'];
no2 = json['no2'];
o3 = json['o3'];
so2 = json['so2'];
pm2_5 = json['pm2_5'];
pm10 = json['pm10'];
nh3 = json['nh3'];
}
double? co;
double? no;
double? no2;
double? o3;
double? so2;
double? pm2_5;
double? pm10;
double? nh3;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['co'] = co;
map['no'] = no;
map['no2'] = no2;
map['o3'] = o3;
map['so2'] = so2;
map['pm2_5'] = pm2_5;
map['pm10'] = pm10;
map['nh3'] = nh3;
return map;
}
}
/// aqi : 3
class Main {
Main({
this.aqi,});
Main.fromJson(dynamic json) {
aqi = json['aqi'];
}
int? aqi;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['aqi'] = aqi;
return map;
}
}
/// lon : 9.19
/// lat : 45.4642
class Coord {
Coord({
this.lon,
this.lat,});
Coord.fromJson(dynamic json) {
lon = json['lon'];
lat = json['lat'];
}
double? lon;
double? lat;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['lon'] = lon;
map['lat'] = lat;
return map;
}
}
2. model.dart - 날씨별 이미지/아이콘/설명 위젯변환
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class Model {
// 온도표시 하단의 날씨별 이미지
Widget getWeatherIcon(int condition) {
if (condition < 300) {
return SvgPicture.asset(
'svg/climacon-cloud_lightning.svg',
// color: Colors.black87,
);
} else if (condition < 600) {
return SvgPicture.asset(
'svg/climacon-cloud_snow_alt.svg',
// color: Colors.black87,
);
} else if (condition == 800) {
return SvgPicture.asset(
'svg/climacon-sun.svg',
// color: Colors.black87,
);
} else if (condition <= 804) {
return SvgPicture.asset(
'svg/climacon-cloud_sun.svg',
// color: Colors.black87,
);
}
return SvgPicture.asset('svg/climacon-cloud_sun.svg');
}
// AQI 등급별 이미지
Widget getAirIcon(int condition) {
if (condition == 1) {
return Image.asset('image/good.png', width: 37.0, height: 35.0);
} else if (condition == 2) {
return Image.asset('image/fair.png', width: 37.0, height: 35.0);
} else if (condition == 3) {
return Image.asset('image/moderate.png', width: 37.0, height: 35.0);
} else if (condition == 4) {
return Image.asset('image/poor.png', width: 37.0, height: 35.0);
} else if (condition == 5) {
return Image.asset('image/bad.png', width: 37.0, height: 35.0);
}
return Image.asset('image/bad.png', width: 37.0, height: 35.0);
}
// AQI 등급별 이미지 설명
Widget getAirCondition(int condition) {
if (condition == 1) {
return const Text(
'"매우좋음"',
style: TextStyle(color: Colors.indigo, fontWeight: FontWeight.bold),
);
} else if (condition == 2) {
return const Text(
'"좋음"',
style: TextStyle(color: Colors.indigo, fontWeight: FontWeight.bold),
);
} else if (condition == 3) {
return const Text(
'"보통"',
style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold),
);
} else if (condition == 4) {
return const Text(
'"나븜"',
style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold),
);
} else if (condition == 5) {
return const Text(
'"매우나쁨"',
style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold),
);
}
return const Text(
'"매우나쁨"',
style: TextStyle(color: Colors.black87, fontWeight: FontWeight.bold),
);
}
}
3. newtork.dart - api 호출 추가
import 'package:http/http.dart' as http;
import 'dart:convert';
class Network {
final String urlWeather;
final String urlAir;
Network(this.urlWeather, this.urlAir);
// 날씨정보 API 호출
Future<dynamic> getWeatherData() async {
http.Response response = await http
.get(Uri.parse(urlWeather));
if (response.statusCode == 200) {
String jsonData = response.body;
var parsingData = jsonDecode(jsonData);
return parsingData;
}
}
// 대기질정보 API 호출
Future<dynamic> getAirData() async {
http.Response response = await http
.get(Uri.parse(urlAir));
if (response.statusCode == 200) {
String jsonData = response.body;
var parsingData = jsonDecode(jsonData);
return parsingData;
}
}
}
4. loading.dart - api 조립 및 파싱
import 'package:flutter/material.dart';
import 'package:weather/data/my_location.dart';
import 'package:weather/data/network.dart';
import 'package:weather/model/current_air.dart';
import 'package:weather/model/current_weather.dart';
import 'package:weather/screens/weather_screen.dart';
const apiKey = '1e1a2b8f6d9b5311cd82d001e7b20131';
class Loading extends StatefulWidget {
const Loading({Key? key}) : super(key: key);
@override
_LoadingState createState() => _LoadingState();
}
class _LoadingState extends State<Loading> {
late double latitude;
late double longitude;
@override
void initState() {
// TODO: implement initState
super.initState();
getLocation();
}
void getLocation() async {
MyLocation myLocation = MyLocation();
await myLocation.getMyCurrentLocation();
latitude = myLocation.latitude;
longitude = myLocation.longitude;
debugPrint('loading.dart >> ' + latitude.toString() + ' / ' + longitude.toString());
String _baseApiWeather = 'https://api.openweathermap.org/data/2.5/weather';
String _baseApiAir =
'https://api.openweathermap.org/data/2.5/air_pollution';
String _option = 'units=metric&lang=kr';
String _lat = 'lat=${latitude.toString()}';
String _lon = 'lon=${longitude.toString()}';
Network network = Network(
'$_baseApiWeather?$_lat&$_lon&appid=$apiKey&$_option',
'$_baseApiAir?$_lat&$_lon&appid=$apiKey&$_option');
// 날씨정보 API 호출
var weatherData = await network.getWeatherData();
debugPrint(weatherData.toString());
CurrentWeather currentWeatherData = CurrentWeather.fromJson(weatherData);
debugPrint(currentWeatherData.name);
// 대기질정보 API 호출
var airData = await network.getAirData();
debugPrint(airData.toString());
CurrentAir currentAirData = CurrentAir.fromJson(airData);
debugPrint(currentAirData.listed![0].main!.aqi!.toString());
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => WeatherScreen(
weatherData: currentWeatherData,
airData: currentAirData,
)));
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ElevatedButton(
onPressed: () {
debugPrint('ElevatedButton clicked~~');
},
child: const Text(
'Get my location',
style: TextStyle(color: Colors.white),
),
),
),
);
}
}
5. weather_screen.dart - 화면 출력 수정
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:intl/intl.dart';
import 'package:timer_builder/timer_builder.dart';
import 'package:weather/model/current_air.dart';
import 'package:weather/model/current_weather.dart';
import 'package:weather/model/model.dart';
class WeatherScreen extends StatefulWidget {
final CurrentWeather weatherData;
final CurrentAir airData;
const WeatherScreen(
{Key? key, required this.weatherData, required this.airData})
: super(key: key);
@override
State<WeatherScreen> createState() => _WeatherScreenState();
}
class _WeatherScreenState extends State<WeatherScreen> {
late String cityName;
late int temp;
late String currentDate;
var date = DateTime.now();
final Model _model = Model();
late Widget airIcon;
late Widget airState;
double? pm2_5;
double? pm10;
@override
void initState() {
// TODO: implement initState
super.initState();
updateData(widget.weatherData, widget.airData);
}
void updateData(CurrentWeather weatherData, CurrentAir airData) {
var dt = weatherData.dt!;
var timezone = weatherData.timezone!;
var tempTime = DateTime.fromMillisecondsSinceEpoch((dt + timezone) * 1000);
int index = airData.listed![0].main!.aqi!;
pm10 = airData.listed![0].components!.pm10!;
pm2_5 = airData.listed![0].components!.pm2_5!;
airIcon = _model.getAirIcon(index);
airState = _model.getAirCondition(index);
cityName = weatherData.name!;
temp = weatherData.main!.temp!.round();
currentDate = DateFormat('yyyy-MM-dd, HH:mm:ss').format(tempTime);
debugPrint('cityName[$cityName], temp[${weatherData.main!.temp}]');
debugPrint('dt[$dt], timezone[$timezone], Date[$currentDate]');
}
String getSystemTime() {
var now = DateTime.now();
return DateFormat('h:mm a').format(now);
}
@override
Widget build(BuildContext context) {
return Scaffold(
extendBodyBehindAppBar: true,
appBar: AppBar(
backgroundColor: Colors.transparent,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.near_me),
onPressed: () {},
),
actions: [
IconButton(
icon: const Icon(Icons.location_searching),
onPressed: () {},
iconSize: 30.0,
),
],
),
body: Container(
child: Stack(
children: [
Image.asset(
'image/background.jpg',
fit: BoxFit.cover,
width: double.infinity,
height: double.infinity,
),
Container(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 150.0),
Text(
'${widget.weatherData.name}',
style: GoogleFonts.lato(
fontSize: 30,
fontWeight: FontWeight.bold,
color: Colors.white),
),
const SizedBox(height: 8.0),
Row(
children: [
TimerBuilder.periodic(
const Duration(minutes: 1),
builder: (context) {
debugPrint(getSystemTime());
return Text(
getSystemTime(),
style: GoogleFonts.lato(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white),
);
},
),
Text(
DateFormat(' - EEEE, ').format(date),
style: GoogleFonts.lato(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white),
),
Text(
DateFormat('yyyy-MM-dd').format(date),
style: GoogleFonts.lato(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white),
),
],
),
],
), //City Name, Date, Time
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${widget.weatherData.main!.temp!.round().toString()}\u2103',
style: GoogleFonts.lato(
fontSize: 85,
fontWeight: FontWeight.w300,
color: Colors.white),
),
Row(
children: [
_model.getWeatherIcon(
widget.weatherData.weather![0].id!),
const SizedBox(
width: 8.0,
),
Text(
'${widget.weatherData.weather![0].description}',
style: GoogleFonts.lato(
fontSize: 16.0, color: Colors.white),
),
],
),
],
), //Temperature
],
),
),
Column(
children: [
const Divider(
height: 15.0,
thickness: 2.0,
color: Colors.white30,
), //구분자
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
children: [
Text(
'AQI(대기질지수)',
style: GoogleFonts.lato(
fontSize: 14.0, color: Colors.white),
),
const SizedBox(height: 8),
// Image.asset('image/bad.png',
// width: 37.0, height: 35.0),
airIcon,
const SizedBox(height: 8),
airState,
],
), //AQI(대기질지수)
Column(
children: [
Text(
'미세먼지',
style: GoogleFonts.lato(
fontSize: 14.0, color: Colors.white),
),
const SizedBox(height: 8),
Text(
'$pm10',
style: GoogleFonts.lato(
fontSize: 24.0, color: Colors.white),
),
const SizedBox(height: 8),
Text(
'µg/m3',
style: GoogleFonts.lato(
fontSize: 14.0,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
), //미세먼지
Column(
children: [
Text(
'초미세먼지',
style: GoogleFonts.lato(
fontSize: 14.0, color: Colors.white),
),
const SizedBox(height: 8),
Text(
'$pm2_5',
style: GoogleFonts.lato(
fontSize: 24.0, color: Colors.white),
),
const SizedBox(height: 8),
Text(
'µg/m3',
style: GoogleFonts.lato(
fontSize: 14.0,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
), //초미세먼지
],
),
],
), //extra information
],
),
),
],
),
),
);
}
}
화면출력
[참고자료] 코딩셰프
- https://www.youtube.com/watch?v=3cjktl_HWHc&list=PLQt_pzi-LLfoOpp3b-pnnLXgYpiFEftLB&index=17
'Flutter > 10 app Weather' 카테고리의 다른 글
[Flutter] App Weather - 6단계 완료(indicator 추가) (0) | 2022.05.01 |
---|---|
[Flutter] App Weather - 4단계 UI (0) | 2022.04.29 |
[Flutter] App Weather - 3.5단계 model of openweathermap (0) | 2022.04.28 |
[Flutter] App Weather - 3단계 json from openweathermap (0) | 2022.04.28 |
[Flutter] App Weather - 2단계 http & json (0) | 2022.04.27 |