데이터 처리와 상태관리

gov's avatar
Dec 31, 2024
데이터 처리와 상태관리

1. Repository와 Body 기본 연결

💡
Repository와 UI가 연결되는 방식과 데이터 흐름 확인하기.
Repository가 데이터 제공 → UI가 데이터를 받아 렌더링. ViewModel을 도입하여 UI, 상태, 데이터 관리의 역할을 분리하는 것이 더 좋다.
notion image
import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:mockapp/home_repository.dart'; class HomeBody extends StatelessWidget { @override Widget build(BuildContext context) { HomeRepository repo = const HomeRepository(); int one = repo.getOne(); List<int> list = repo.getList(); return Column( children: [ Center(child: Text("${one}", style: TextStyle(fontSize: 50))), Expanded( child: ListView.builder( itemCount: list.length, itemBuilder: (context, index) { return ListTile(leading: Text("${list[index]}"), title: Text("내용")); }, ), ), ], ); } }
// SRP : 데이터를 가져오는 곳 (휴대폰 디바이스(파일), 휴대폰 DB, Firebase(외부서버), 내서버, 공공데이터서버) class HomeRepository { const HomeRepository(); // new 반복하기 List<int> getList() { return [1, 2, 3, 4]; } int getOne() { return 1; } }

2. Riverpod와 viewModel 사용

  • body와 resository 사이에 viewModel이 필요하다.
  • 화면을 레파지토리(데이터관리-통신)가 아닌 vm 보고 만들기.
💡
동작 원리
  1. HomeBody가 homeProvider를 통해 상태를 구독하고, 상태 변화에 따라 UI를 업데이트.
  1. HomePageVM은 build 시점에 getOne을 호출하여 데이터를 가져오기.
  1. HomeRepository는 데이터를 반환하며, ViewModel에서 이를 상태로 업데이트.
  1. 상태 값이 갱신되면 HomeBody는 자동으로 다시 빌드되어 새로운 UI를 렌더링.
  1. home_page : 화면 뼈대
    1. import 'package:flutter/material.dart'; import 'home_body.dart'; class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: HomeBody(), // 레이어 구분. home_body파일에서 들고옴 ); } }
  1. home_body : UI와 데이터 연결
    1. import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mockapp/home_page_vm.dart'; class HomeBody extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { int? one = ref.watch(homeProvider); // 상태 관찰 if (one == null) { // 함수가 안지남. 상태값이 null이라 로딩화면 보여주기 return Center(child: CircularProgressIndicator()); } else { // 값이 있을 때, 데이터를 활용해 UI 구성 return Column( children: [ Center(child: Text("1", style: TextStyle(fontSize: 50))), Expanded( child: ListView.builder( itemCount: 4, itemBuilder: (context, index) { return ListTile( leading: Text("${index + 1}"), title: Text("내용")); }, ), ), ], ); } } }
  1. home_page_vm : 상태 변경(데이터 갱신)
    1. import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mockapp/home_repository.dart'; // 상태 관리하는 ViewModel 정의 final homeProvider = NotifierProvider<HomePageVM, int?>(() { return HomePageVM(); }); class HomePageVM extends Notifier<int?> { HomeRepository repo = const HomeRepository(); @override int? build() { getOne(); // 상태 초기화 시작 return null; // 상태 null 초기화 } // 상태에 대한 함수 // async, await가 있으면 Future붙임 Future<void> getOne() async { int one = await repo.getOne(); // Repository로 데이터 가져옴 state = one; // 상태 업데이트 } }
  1. home_repository : 데이터 가져오기(외부 소스에서 데이터 읽어오는 역할)
    1. // SRP(단일 책임 원칙): 데이터를 가져오는 곳 // 예) 휴대폰 디바이스(파일), 휴대폰 DB, Firebase(외부서버), 내서버, 공공데이터서버 class HomeRepository { // 통신 코드로 바꾸기 const HomeRepository(); // async비동기 함수는 Future를 붙여 return한다 Future<List<int>> getList() async { // 비동기적으로 List<int> 데이터 반환 List<int> response = await Future.delayed( Duration(seconds: 3), () { // 3초후 데이터 반환 return [1, 2, 3, 4]; }, ); return response; } Future<int> getOne() async { int response = await Future.delayed(Duration(seconds: 3), () { return 5; },); return response; } }

3. Future

💡
Dart에서 비동기 작업을 처리하기 위한 객체로, 작업이 완료될 때까지 기다리게 지원. 비동기 작업은 시간이 걸리는 작업(예: 네트워크 요청, 파일 읽기)에 사용한다. Future와 FutureBuilder를 활용한 간단한 비동기 데이터 처리 구조.
  • 특징
    • Dart는 기본적으로 싱글 스레드 환경에 작동.
    • 시간이 오래 걸리는 작업(예: API 호출)이 UI를 차단하지 않도록, Future로 비동기 처리.
    • Future는 작업이 완료된 후 콜백을 실행하거나, await 키워드로 결과를 처리.
  • 코드
      1. future_page
        1. import 'package:flutter/material.dart'; import 'future_Body.dart'; // Riverpod 없이 통신 데이터 가져오기 class FuturePage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: FutureBody(), ); } }
      1. future_body
        1. class FutureBody extends StatelessWidget { @override Widget build(BuildContext context) { HomeRepository repo = const HomeRepository(); return Column( children: [ FutureBuilder( // 한번쓰고 안씀 -가독성 낮음 future: repo.getOne(), builder: (context, snapshot) { if (snapshot.hasData) { return Center(child: Text("1", style: TextStyle(fontSize: 50))); } else { return CircularProgressIndicator(); } }, ), Expanded( child: ListView.builder( itemCount: 4, itemBuilder: (context, index) { return ListTile(leading: Text("${index + 1}"), title: Text("내용")); }, ), ), ], ); } }

4. Post

💡
API로 받은 데이터 저장하기.
 
Post가 Map으로 받는 이유는 API 응답이 대개 JSON 형식으로 오기 때문.
즉, 서버에서 받은 JSON 데이터를 Map 형식으로 처리하고, 이를 Post 객체로 변환하는 과정을 거친다.
  1. post_page
    1. import 'package:flutter/material.dart'; import 'package:mockapp/post_body.dart'; class PostPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: PostBody(), ); } }
  1. post_body
    1. import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mockapp/post_page_vm.dart'; class PostBody extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { Post? model = ref.watch(postProvider); // Post 상태를 구독 if (model == null) { return Center(child: CircularProgressIndicator()); } else { return Column( children: [ Text("id : ${model.id}"), Text("userId : ${model.userId}"), Text("title : ${model.title}"), Text("body : ${model.body}"), Row(), ], ); } } }
  1. post_page_vm
    1. import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mockapp/post_repository.dart'; class Post { int userId; int id; String title; String body; Post(this.userId, this.id, this.title, this.body); // 서버로 받은 Map 데이터를 객체로 변환 Post.fromMap(Map<String, dynamic> map) : userId = map["userId"], id = map["id"], title = map["title"], body = map["body"]; } final postProvider = NotifierProvider<PostPageVM, Post?>(() { return PostPageVM(); }); class PostPageVM extends Notifier<Post?> { PostRepository repo = const PostRepository(); @override Post? build() { // 상태 초기화 시작 init(); // 상태 null 초기화 return null; } Future<void> init() async { Post post = await repo.getPost(); state = post; // 포스트를 상태에 뿌리기 -> 화면 열리면 바로됨 } }
  1. post_repository : 서버에서 게시물 데이터 가져오기. Dio를 사용해 API 요청을 보냄.
    1. import 'package:dio/dio.dart'; import 'package:mockapp/http_util.dart'; import 'package:mockapp/post_page_vm.dart'; class PostRepository { const PostRepository(); Future<Post> getPost() async { Response response = await dio.get("/posts/1"); // get은 map타입(중괄호)로 받음 Map<String, dynamic> body = response.data; return Post.fromMap(body); // 받은 Map 데이터를 Post 객체로 변환 } Future<List<Post>> getPostList() async { // 게시물 목록 가져오기 Response response = await dio.get("/posts"); List<dynamic> list = response.data; // fromMap을 통해 리스트의 모든 항목을 Post 객체로 변환 return list.map((e) => Post.fromMap(e)).toList(); } }

5. post_list

💡
페이지 만들어 컬렉션 뿌리기
notion image
  1. post_list_page
    1. import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mockapp/post_list_body.dart'; import 'package:mockapp/post_list_page_vm.dart'; class PostListPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return Scaffold( body: PostListBody(), floatingActionButton: FloatingActionButton( child: Text("삭제"), onPressed: () { ref.read(postListProvider.notifier).deleteById(3); }, ), ); } }
  1. post_list_body
    1. import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mockapp/post_list_page_vm.dart'; class PostListBody extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { List<Post2>? models = ref.watch(postListProvider); if (models == null) { return Center(child: CircularProgressIndicator()); } else { return ListView.builder( itemCount: models.length, itemBuilder: (context, index) { return ListTile( leading: Text("${models[index].id}"), title: Text("${models[index].title}"), subtitle: Text("${models[index].body}"), trailing: Text("유저아이디 ${models[index].userId}"), ); }, ); } } }
  1. post_list_page_vm
    1. import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mockapp/post_repository.dart'; class Post2 { int userId; int id; String title; String body; Post2(this.userId, this.id, this.title, this.body); Post2.fromMap(Map<String, dynamic> map) : userId = map["userId"], id = map["id"], title = map["title"], body = map["body"]; } final postListProvider = NotifierProvider<PostListPageVM, List<Post2>?>(() { return PostListPageVM(); }); class PostListPageVM extends Notifier<List<Post2>?> { PostRepository repo = const PostRepository(); @override List<Post2>? build() { // 상태 초기화 시작 init(); // 상태 null 초기화 return null; } Future<void> deleteById(int id) async { // 1. 통신 코드 // 2. 상태 변경 List<Post2> posts = state!; state = posts.where((p) => p.id != id).toList(); } Future<void> init() async { List<Post2> postList = await repo.getPostList(); state = postList; } }
  1. post_repository
    1. import 'package:dio/dio.dart'; import 'package:mockapp/http_util.dart'; import 'package:mockapp/post_list_page_vm.dart'; import 'package:mockapp/post_page_vm.dart'; class PostRepository { const PostRepository(); Future<Post> getPost() async{ Response response = await dio.get("/posts/1"); Map<String, dynamic> body = response.data; return Post.fromMap(body); } // Post2 추가 Future<List<Post2>> getPostList() async{ Response response = await dio.get("/posts"); List<dynamic> list = response.data; return list.map((e) => Post2.fromMap(e)).toList(); } }
Share article

goho