본문 바로가기

Flutter/10 app Weather

[Flutter] App Weather - 5단계 추가 데이터 & 모델링

이번에는 추가 데이터(대기질 및 미세먼지) 매핑 및 해당 데이터 모델을 만드는 방법에 대해서 알아보겠습니다.

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)

 

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