들어가기
이번 프로젝트의 목적은 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 공식 문서에서는 getIt
이 provider
와 InheritedWidget
을 대체할 수 있다라고 표현하고 있는데, 직접 써보니까 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를 사용해서 자기만의 버전으로 날씨앱을 만들어보면 좋을것 같다.
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