
데이터통신 시리즈 를 통해 플러터에서 데이터를 통신하여 UI를 그려내는 것까지 성공을 했다.
하지만 거기서 끝나는 게 아니라 구조적인 설계를 하는 것까지 나아가야 한다.
간단하게 말하면, 지금까지 UI와 로직을 한 페이지에 다 때려넣어 완성했다면 앞으로는 기능 별로 폴더를 나눠 작업을 해야한다.
그러니까 지금까지 이렇게 main, screen, model로 간단하게 나누어 개발을 했다면

앞으로는 이렇게 기능 별로 구분되어야 한다는 것이다.

난 그 중에서도 MVVM이라는 패턴을 배우고 적용했다.
★MVVM 패턴이란?
아키텍처 패턴 (Architectural Pattern) 중의 하나인데, 아키텍처 패턴이란 소프트웨어의 전체적인 구조와 조직을 다루는 패턴으로 대규모 앱의 모듈화, 유지보수성 향상, 테스트 용이성 등을 고려하여 시스템 전체의 구조를 설계하는 데 사용된다.
아키텍처 패턴을 쓰면 소프트웨어 시스템을 더 효과적으로 구성하고 관리할 수 있다. MVC (Model-View-Controller), MVVM (Model-View-ViewModel), Clean Architecture 등의 종류가 있다.
데이터통신 시리즈 에서 다뤘던 작업을 MVVM 패턴으로 리펙토링 해보자!
그런데 dto, mapper까지 한 번에 적용하면 너무 어려우니까... 오늘은 repository와 viewmodel만 다뤄볼 것이다.
Repository 란 데이터 저장소라는 뜻으로 DataSource에 접근하는 역할을 한다. 즉, 기존에 main_screen에서 곧 바로 데이터를 불러오는 함수를 작성했었는데, UI와 로직 분리를 위하여 함수를 repository로 분리시키는 것이다. 앞으로 데이터는 이 repository가 담게 된다.
class StoreRepository {
Future<List<Store>> fetch() async {
final List<Store> stores = [];
var response = await http.get(Uri.parse('http://www.example.com));
final jsonResult = jsonDecode(response.body);
final jsonStores = jsonResult['stores'];
jsonStores.forEach((e) {
stores.add(Store.fromJson(e));
});
return stores.where((e)
=> e.remainStat == 'plenty' ||
e.remainStat == 'some' ||
e.remainStat == 'few'
).toList();
}
}
view_model 파일은 한 뎁스 안에 위치해 있어 위의 MVVM 예시 사진에는 나와있지 않은데 실제 위치는 ui 폴더 안이다. view_model은 페이지별로 존재해야 하기 때문인데 이번엔 main_screen에 대한 view_model 하나만 구성했다.

class StoreModel with ChangeNotifier {
var isLoading = false;
List<Store> stores = [];
final _storeRepository = StoreRepository();
StoreModel() {
fetch2();
}
//initState에서 처음 로딩될 때 fetch를 실행시켜야 하는데
//view엔 기능을 담을 수 없다보니 여기서 함수를 실행해서 최초 앱이 실행될때 데이터가 나오게 함
Future fetch2() async {
isLoading = true;
notifyListeners();
stores = await _storeRepository.fetch();
isLoading = false;
notifyListeners();
void main() {
runApp(
ChangeNotifierProvider.value(value: StoreModel(),
child: MyApp(),
),
);
}
이제 우리의 main_screen은 오직 UI만 담고 있고, 기능은 모두 참조해서 가져다 쓰기만 하면 된다. 즉 메인이 아주 아주 간소해진 다는 것이다.
class MainPage extends StatelessWidget{
@override
Widget build(BuildContext context) {
final storeModel = Provider.of<StoreModel>(context);
return Scaffold(
appBar: AppBar(
title: Text(
'마스크 재고 있는 곳 : ${storeModel.stores.length}곳'),
actions: <Widget>[
IconButton(
onPressed: () {
storeModel.fetch2();
},
icon: const Icon(Icons.refresh)),
],
),
body: storeModel.isLoading
? const Center(child: CircularProgressIndicator())
: ListView(
children: storeModel.stores
.map((e) => ListTile(
title: Text(e.name ?? ''),
subtitle: Text(e.addr ?? ''),
trailing: RemainStatWidget(e), //e: Store 객체
)) .toList(),
),
);
}
}