Weather app

07 Sep 2024  Khlee  11 mins read.

다음 플러터 강좌들을 보고 정리한 내용입니다.

Widget lifecycle

Stateless widget은 한 번 생성되면 바뀌지 않기 때문에 바꾸고 싶다면 destroy 하고 rebuild해야 한다. 그리고 Stateful widget은 state object와 결합하여 위젯이 변경될 때 state object가 이를 감지해서 내용을 업데이트할 수 있다. 따라서 Stateful widget은 Stateless widget 보다 더 긴 생명주기를 가지고 더 많은 lifecycle method를 갖는다. 다음은 대표적인 3개의 lifecycle method다.

  • initState(): state가 최초로 초기화될 때 호출됨
  • build(): 위젯이 빌드될 때 호출됨
  • dispose(): 위젯이 제거될 때 호출됨
import 'package:flutter/material.dart';

class ScreenB extends StatefulWidget {
  const ScreenB({super.key});

  @override
  State<ScreenB> createState() => _ScreenBState();
}

class _ScreenBState extends State<ScreenB> {
  @override
  void initState() {
    super.initState();
    print('initState is called');
  }

  @override
  void dispose() {
    super.dispose();
    print('dispose is called');
  }

  @override
  Widget build(BuildContext context) {
    print('build is called');
    return Scaffold(
      appBar: AppBar(
        title: const Text('Screen B'),
        backgroundColor: Colors.blue,
        foregroundColor: Colors.white,
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            Navigator.pop(context);
          },
          child: const Text(
            'Go to the Screen A',
            style: TextStyle(
              fontSize: 24.0,
            ),
          ),
        ),
      ),
    );
  }
}

위 코드는 StatefulWidget의 lifecycle method를 사용하는 방법을 보여준다.

Navigator를 통해 다른 페이지에서 Screen B로 진입할 때 initState()build()가 순서대로 호출되어서 로그에 initState is calledbuild is called가 출력된다. 그 다음 Go to the Screen A 버튼을 클릭해서 Screen B에서 벗어나면 dispose()가 호출되어서 로그에 dispose is called가 출력된다.

현재 위치 가져오기

geolocator 패키지를 설치한다. pubspec.yaml 파일에 다음과 같이 geolocator: ^13.0.1를 추가하면 된다.

dependencies:
  flutter:
    sdk: flutter
  geolocator: ^13.0.1

그리고 안드로의드의 경우 android/app/src/main/AndroidManifest.xml 파일을 열어서 아래와 같이 android.permission.ACCESS_FINE_LOCATION 권한을 추가한다.

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 생략 -->
    <queries>
        <intent>
            <action android:name="android.intent.action.PROCESS_TEXT"/>
            <data android:mimeType="text/plain"/>
        </intent>
    </queries>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <!-- 이 줄을 추가 -->
</manifest>

geolocator를 사용해 현재 위치를 가져오는 코드를 다음과 같이 Loading 위젯에 작성한다.

import 'package:flutter/material.dart';
import 'package:geolocator/geolocator.dart';

class Loading extends StatefulWidget {
  const Loading({super.key});

  @override
  State<Loading> createState() => _LoadingState();
}

class _LoadingState extends State<Loading> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            getLocation();
          },
          child: Text('Get my location'),
        ),
      ),
    );
  }

  void getLocation() async {
    LocationPermission permission = await Geolocator.requestPermission();

    final LocationSettings locationSettings = LocationSettings(
      accuracy: LocationAccuracy.high,
      distanceFilter: 100,
    );
    Position position = await Geolocator.getCurrentPosition(locationSettings: locationSettings);

    print(position);
  }
}

사용자가 ElevatedButton 버튼을 눌렀을 때 getLocation() 함수가 호출되며, getLocation() 함수는 위치 관련 권한을 요청하고 위치 값을 가져 온 후 이 값을 출력한다.

아래는 권한을 요청하는 화면이다.

위치 권한 요청

버튼을 눌렀을 때 위치를 가져오는 것이 아닌, 앱을 실행했을 때 위치를 가져오도록 getLocation() 함수를 호출하는 위치를 initState() 함수로 바꾸었다.

class _LoadingState extends State<Loading> {

  @override
  void initState() {
    super.initState();
    getLocation();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: ElevatedButton(
          onPressed: null,
          child: Text('Get my location'),
        ),
      ),
    );
  }

  void getLocation() async {
    // ...
  }
}

Troubleshooting

2024년 8월, Flutter SDK 3.24.0과 geolocator 13.0.1을 사용하여 바로 빌드하면 아래와 같은 오류 메시지를 출력한다.

e: C:/Users/user/.gradle/caches/transforms-3/604f9fb74816ffb3b0ff4f95586be650/transformed/jetified-kotlin-stdlib-common-1.9.0.jar!/META-INF/kotlin-stdlib-common.kotlin_module: Module was compiled with an incompatible version of Kotlin. The binary version of its metadata is 1.9.0, expected version is 1.7.1.
e: C:/Users/user/.gradle/caches/transforms-3/6de1df31c3f1c35e3c78be91db4e53b8/transformed/jetified-kotlin-stdlib-1.9.0.jar!/META-INF/kotlin-stdlib-jdk7.kotlin_module: Module was compiled with an incompatible version of Kotlin. The binary version of its metadata is 1.9.0, expected version is 1.7.1.
e: C:/Users/user/.gradle/caches/transforms-3/6de1df31c3f1c35e3c78be91db4e53b8/transformed/jetified-kotlin-stdlib-1.9.0.jar!/META-INF/kotlin-stdlib-jdk8.kotlin_module: Module was compiled with an incompatible version of Kotlin. The binary version of its metadata is 1.9.0, expected version is 1.7.1.
e: C:/Users/user/.gradle/caches/transforms-3/6de1df31c3f1c35e3c78be91db4e53b8/transformed/jetified-kotlin-stdlib-1.9.0.jar!/META-INF/kotlin-stdlib.kotlin_module: Module was compiled with an incompatible version of Kotlin. The binary version of its metadata is 1.9.0, expected version is 1.7.1.

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':app:compileDebugKotlin'.
> A failure occurred while executing org.jetbrains.kotlin.compilerRunner.GradleCompilerRunnerWithWorkers$GradleKotlinCompilerWorkAction
   > Compilation error. See log for more details

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 9s

┌─ Flutter Fix ────────────────────────────────────────────────────────────────────────────────┐
│ [!] Your project requires a newer version of the Kotlin Gradle plugin.                       │
│ Find the latest version on https://kotlinlang.org/docs/releases.html#release-details, then   │
│ update the                                                                                   │
│ version number of the plugin with id "org.jetbrains.kotlin.android" in the plugins block of  │
│ C:\Users\user\Projects\Flutter\flutter-test\weather_app\android\settings.gradle.             │
│                                                                                              │
│ Alternatively (if your project was created before Flutter 3.19), update                      │
│ C:\Users\user\Projects\Flutter\flutter-test\weather_app\android\build.gradle                 │
│ ext.kotlin_version = '<latest-version>'                                                      │
└──────────────────────────────────────────────────────────────────────────────────────────────┘
Error: Gradle task assembleDebug failed with exit code 1

Exited (1).

오류 메시지의 가이드에 따라 플러터 프로젝트의 android/settings.gradle 파일에서 org.jetbrains.kotlin.android의 버전을 다음과 같이 1.7.10에서 2.0.10로 변경하면 해결된다.

id "org.jetbrains.kotlin.android" version "2.0.10" apply false

Exception handling

Dart에서 다음과 같이 try, catch 구문을 사용해서 예외를 처리할 수 있다.

try {
  Position position = await Geolocator.getCurrentPosition(locationSettings: locationSettings);
  print(position);
}
catch(e) {
  print('There was a problem with the internet connection.');
}

http package

HTTP 요청을 사용하려면 먼저 http package를 설치해야 한다. 해당 페이지에 방문해서 최신 버전을 확인하고 다음과 같이 pubspec.yaml 파일에 dependency를 추가한다.

dependencies:
  http: ^1.2.2

이 패키지를 사용하려면 다음과 같이 import 하면 된다.

import 'package:http/http.dart' as http;

그리고 아래와 같이 get() 함수를 호출해서 HTTP 요청을 보낼 수 있다. 그리고 응답은 Response 클래스의 bodystatusCode 값을 통해 가져올 수 있다.

void fetchData() async {
  Uri uri = Uri.parse('https://samples.openweathermap.org/data/2.5/weather?q=London&appid=b1b15e88fa797225412429c1c50c122a1');
  http.Response response = await http.get(uri);
  print(response.body);
  print(response.statusCode);
}

Json parsing

Json parsing을 하기 위해서 다음 패키지를 import 해야 한다.

import 'dart:convert';

그리고 다음과 같이 parsing할 수 있다.

if(response.statusCode == 200) {
  String jsonData = response.body;
  var myJson = jsonDecode(jsonData);
  var description = myJson['weather'][0]['description'];
  var wind = myJson['wind']['speed'];
  var id = myJson['id'];
  print('description: $description\nwind: $wind\nid: $id');
}
I/flutter ( 8295): description: light intensity drizzle
I/flutter ( 8295): wind: 4.1
I/flutter ( 8295): id: 2643743

OpenWeather API 사용하기

OpenWeather에 로그인하고 API 키를 받자. 나의 경우에는 이메일 인증 후 시간이 좀 지나서야 API 키를 사용할 수 있었다.

다음과 같이 특정 위도/경도의 날씨 정보를 가져올 수 있다. latitudelongitude는 geolocator를 통해 가져 온 position 값을 통해 가져올 수 있다.

String url = 'https://api.openweathermap.org/data/2.5/weather?lat=$latitude&lon=$longitude&appid=$apiKey';
Network network = Network(url);
var weatherData = await network.getJsonData();
print(weatherData);

섭씨 온도로 받아오려면 url 뒤에 &units=metric를 붙인다.

String url = 'https://api.openweathermap.org/data/2.5/weather?lat=$latitude&lon=$longitude&appid=$apiKey&units=metric';

Passing data

위젯간 데이터를 전달하는 방법 중 하나로 생성자 파라미터를 통해 전달하는 방법이 있다.

class WeatherScreen extends StatefulWidget {
  final dynamic parseWeatherData;

  const WeatherScreen({
    super.key,
    this.parseWeatherData,
  });

  @override
  State<WeatherScreen> createState() => _WeatherScreenState();
}

class _WeatherScreenState extends State<WeatherScreen> {
  @override
  void initState() {
    super.initState();
    print(widget.parseWeatherData);
  }

  @override
  Widget build(BuildContext context) {
    return SomeWidget();
  }
}

위 코드 예시와 같이 StatefulWidget의 경우에는 해당하는 state 클래스에서 widget 변수를 통해 그 전달받은 데이터를 확인할 수 있다. (_WeatherScreenState.initState() 함수 참조)

간단한 팁

double 타입의 변수를 int 타입으로 변환하려면 두 가지 방법이 있다.

int temp = tempDouble.toInt();
int temp = tempDouble.round();

UI 꿀팁

앱바에 아이콘 버튼은 있지만 투명하게 하고 싶은 경우 아래와 같이 한다.

@override
Widget build(BuildContext context) {
  return Scaffold(
    extendBodyBehindAppBar: true,
    appBar: AppBar(
      // title: Text(''),
      backgroundColor: Colors.transparent,
      elevation: 0.0,
      leading: IconButton(),
      actions: [
        IconButton(),
      ],
    ),
    body: Container(),
  );
}

extendBodyBehindAppBar, backgroundColor, elevation 속성들을 활용하면 된다.

khlee
khlee