Flutter로 날씨앱 만들기 2편

Provider와 get 사용해서 만들기

Posted by Minki on October 30, 2020

들어가기

이번 프로젝트의 목적은 State Management를 피부로 느껴보는 것이다. 따라서 기상철 날씨 파싱앱을 만들면서 두 가지 방법으로 만들었는데, 첫 번째는 State Management Packages를 안 쓰고 만드는 것이고, 두번째는 State Management Packages를 써서 만들어보는 방법이다. 이 시리즈를 보시는 분들도 나와 같이 State Management의 중요성을 체감하셨으면 좋겠다.

잘못된 내용은 언제든지 밑의 댓글로 알려주세요!

시리즈 1편은 여기서 보실 수 있습니다.

1. Provider와 getIt 패키지

1-1. Provider 패키지

Provider는 2019년 Google IO에서 발표된 State Management 패키지로 Flutter 커뮤니티에서 만들어졌으나, 패키지의 우수한 성능 및 편의성 덕에 구글에서 공식으로 추천한 패키지이다.

Provider를 앱 설계 과정에서 사용한다면 앱의 UI끼리 데이터를 넘겨주는 방식이 아닌 공유하는 방식으로 State Management를 수행할 수 있다. 또한 Bloc 패턴에 비해 코드가 비교적 간결하고 쉬워서 배우기도 빠르다는 장점도 있다. 이런 이유로 구글에서도 중소규모 프로젝트는 Provider로, 대규모 프로젝트는 Bloc 패턴을 추천하고 있다.

1-2. getIt 패키지

getIt 패키지는 Simple한 Service Locator이다. Flutter 공식 문서에서는 getItproviderInheritedWidget을 대체할 수 있다라고 표현하고 있는데, 직접 써보니까 Provider와 같이 쓴다면 더 효과적으로 사용할 수 있는 패키지인것 같다.

어떤 의미냐 하면, provider를 사용하기 위해서는 context의 위치를 provider에게 알려주어야 한다. 따라서 완전히 UI앱과 분리되기 힘들었는데 getIt을 활용하면 이런 제약에서 어느정도 자유로울 수 있다. 그럼 지금부터 바로 코드를 보도록 하자.

2. Code 설명

2-1. Provider 객체 생성

먼저 Provider를 사용하기 위해서는 Provider 객체를 생성해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class WeatherProvider with ChangeNotifier, DiagnosticableTreeMixin {
  String _locationName;
  int _locationCode;
  List<String> _locationList;
  List<dynamic> _locationCodeList;
  List<WeatherInfo> _todayweather;

  String get locationName => _locationName;
  int get locationCode => _locationCode;

  List<String> getLocationList() {
    return _locationList;
  }

  List<dynamic> getLocationCodeList() {
    return _locationCodeList;
  }

  List<WeatherInfo> getTodayWeather() {
    return _todayweather;
  }

  // locationCode와 locationName getter method
  void getNameCode(int index) {
    _locationName = _locationList[index];
    _locationCode = _locationCodeList[index];
  }

  // 각각의 list안에 필요한 정보를 넣는 과정
  Future<void> getLoadLocationData() async {
    Map<String, dynamic> weatherLocation = await parseJson();
    _locationList = weatherLocation.keys.toList();
    _locationCodeList = weatherLocation.values.toList();
  }

  final _weatherhelper = WeatherApi();
  final _streamController = StreamController<List<WeatherInfo>>.broadcast();

  Future<void> getWeatherInfo() async {
    final weatherResult =
        await _weatherhelper.getNowWeather(locationCode: _locationCode);
    _streamController.add(weatherResult);
    notifyListeners();
  }

  // 결과적으로 weatherStream에 최종 날씨 정보가 들어가 있음.
  Stream<List<WeatherInfo>> get weatherStreamController {
    return _streamController.stream;
  }
}

공유가 필요한 데이터를 Provider Class내의 지역변수로 설정해놓고 이를 getter method로 불러옴으로써 외부에서 provider 데이터들을 사용할 수 있다.

또한 지역변수를 활용해 날씨 정보를 채우는 과정까지 provider에 넣음으로써 어떤 페이지에서도 해당 지역의 날씨 정보를 가져다 쓸 수 있다.

2-2. Provider와 getIt 배치

이 다음은 위에서 생성한 provider 객체를 앱의 어디에서든 사용할 수 있도록 하기 위해 앱 Widget의 최상단에 getIt과 provider를 위치시켜준다.

1
2
3
4
5
final getIt = GetIt.instance;

void setup() {
  getIt.registerSingleton<WeatherProvider>(WeatherProvider());
}

<provider + getIt 배치하는 방법>

1
2
3
4
5
6
7
8
void main() {
  // getIt 불러오는 방법
  setup();
  // MultiProvider로 Provider 적용하는 방법
  runApp(MultiProvider(
      providers: [ChangeNotifierProvider(create: (_) => WeatherProvider())],
      child: Weather()));
}

2-3. Provider와 getIt을 사용한 SearchScreen 설계

이제 위에서 만들어놓은 provider와 getIt을 사용해서 SearchScreen을 설계해보자. SearchScreen의 주된 목적은 지역명을 이용한 ListView를 생성하는 것이다. ListView 생성을 위해 여기서는 FutuerBuilder를 사용했으므로 궁금한 사람들은 따로 찾아보면 될 것이다. 그럼 코드를 보자.

1
2
3
4
5
6
7
8
9
10
11
12
class _SearchLoactionScreenState extends State<SearchLoactionScreen> {
  var _controller = TextEditingController();
  final provider = getIt.get<WeatherProvider>();

  // ListView 출력을 위한 filterLocation
  List<String> filterLocationList;

  Future loadFilterLocationList() async {
    await provider.getLoadLocationData();
    List<String> filterLocationList = provider.getLocationList();
    return filterLocationList;
  }

만약 provider만 썼다면, context가 필요하기 때문에 위의 코드처럼 불러올 수 없다. 그러나 여기서는 getIt을 함께 사용했기 때문에 비교적 class의 상단에서 provider를 호출하는 것이 가능하다. 바로 이점때문에 내가 provider와 getIt을 함께 쓰면 더 좋다는 말을 한 것이다.

1
2
3
4
5
6
7
8
9
Expanded(
  child: ListView.builder(
    padding: const EdgeInsets.all(8),
    itemCount: (snapshot.data == null
        ? 0
        : snapshot.data.length),
    itemBuilder: (BuildContext context, int index) {
      provider.getNameCode(index);
      return WeatherTile();

ListView에서는 WeatherTile 클래스에 index를 넘겨주어야 한다. index 정도는 클래스에서 final 변수로 바로 받을 수 있지만 어쨌든 provider를 사용하는 것이 본 프로젝트의 목적이므로 provider의 getNameCode 함수에서 index를 받도록 코딩했다. WeatherTile에서 받는게 더 좋을것 같기는 한데… 무엇이 더 좋은지 확실히 아시는 분은 밑의 댓글로 가르침을 주시면 너무 감사하겠다.

2-4. Provider와 getIt을 사용한 WeatherScreen 설계

WeatherScreen에서도 마찬가지로 provider 변수를 다음처럼 간단하게 공유받을 수 있다. 이후 initState()를 통해 provider.getWeatherInfo()를 앱이 build하자마자 실행되도록 하여 기상청에서 기상 데이터를 크롤링을 수행하도록 한 다음에 그 결과를 _streamController에 추가한다.

1
2
3
4
5
6
7
8
9
10
class _WeatherScreenState extends State<WeatherScreen> {
  final provider = getIt.get<WeatherProvider>();

  @override
  void initState() {
    setState(() {
      provider.getWeatherInfo();
    });
    super.initState();
  }

이렇게 까지 하면 우리가 필요한 모든 정보들이 다 만들어져있는 상태이다. 이제 바로 화면에 출력해보자. 출력할 때에는 크롤링전 날씨 정보가 Null이므로 Null Safety 구현을 위해 StreamBuilder를 사용했다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: AppBar(
        title: Text(provider.locationName),
      ),
      body: Center(
        child: StreamBuilder<Object>(
            stream: provider.weatherStreamController,
            builder: (context, AsyncSnapshot snapshot) {
              if (!snapshot.hasData) {
                return Center(
                  child: Text('날씨를 불러오고 있습니다.'),
                );
              } else {
                return Column(
                  mainAxisAlignment: MainAxisAlignment.center,
                  children: [
                    Text('지역 코드 : ${provider.locationCode}'),
                    Text('======================================='),
                    Text('time : ${snapshot.data[0].time}'),
                    Text('temp : ${snapshot.data[0].temp}'),
                    Text('feelTemp : ${snapshot.data[0].feelTemp}'),
                    Text('humid : ${snapshot.data[0].humid}'),
                    Text('windDirection : ${snapshot.data[0].windDirection}'),
                    Text('windSpeed : ${snapshot.data[0].windSpeed}'),
                    Text('rainFall : ${snapshot.data[0].rainfall}')
                    ]
                  );
                }
              }),
          ),
        );
      }
    },
  }

위의 코드를 보면 지역 명 및 코드는 provider.locationName, provider.locationCode으로 쉽게 불러올 수 있고, 날씨 정보가 담겨있는 _streamController역시 provider.weatherStreamController형태로 쉽게 불러와 snapshot으로 바로바로 사용할 수 있다. 이제 개인의 취향에 맞게 출력 화면만 디자인을 한다면, 바로 날씨 앱을 만들 수 있는 것이다.

3. 느낀점.

State Management의 사용유무에 따라 어떻게 날씨 앱을 만들 수 있는지 살펴보았다. 사실 우리가 만든 앱이 규모가 매우 작은 형태라 State Management의 필요성이 잘 체감되지 않을 수 있지만, 앱이 커지고 공유해야할 데이터가 많아진다고 상상한다면 아마 그 필요성을 느낄 수 있지 않을까 생각해본다.

현재 Flutter는 계속해서 발전중이기 때문에, Provider말고 getX라는 좋은 상태관리 패키지가 나왔다. 관심있는 분은 getX를 사용해서 자기만의 버전으로 날씨앱을 만들어보면 좋을것 같다.


  • provider 안 쓴 깃허브 코드는 여기에 있습니다.
  • provider 쓴 깃허브 코드는 여기에 있습니다.


reference

  • https://github.com/Rahiche/flutter_jobs_app/blob/master/lib/JoblistScreen.dart
  • https://itnext.io/write-your-first-web-scraper-in-dart-243c7bb4d05
  • https://medium.com/flutter-community/parsing-html-in-dart-with-html-package-cd43c29cc460
  • https://flutter-ko.dev/docs/cookbook/networking/fetch-data
  • https://stackoverflow.com/questions/60262715/a-value-of-type-futurelistquestion-cant-be-assigned-to-a-variable-of-type