1. Repository와 Body 기본 연결
Repository와 UI가 연결되는 방식과 데이터 흐름 확인하기.
Repository가 데이터 제공 → UI가 데이터를 받아 렌더링.
ViewModel을 도입하여 UI, 상태, 데이터 관리의 역할을 분리하는 것이 더 좋다.

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 보고 만들기.
동작 원리
- HomeBody가 homeProvider를 통해 상태를 구독하고, 상태 변화에 따라 UI를 업데이트.
- HomePageVM은 build 시점에 getOne을 호출하여 데이터를 가져오기.
- HomeRepository는 데이터를 반환하며, ViewModel에서 이를 상태로 업데이트.
- 상태 값이 갱신되면 HomeBody는 자동으로 다시 빌드되어 새로운 UI를 렌더링.
- home_page : 화면 뼈대
import 'package:flutter/material.dart';
import 'home_body.dart';
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: HomeBody(), // 레이어 구분. home_body파일에서 들고옴
);
}
}
- home_body : UI와 데이터 연결
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("내용"));
},
),
),
],
);
}
}
}
- home_page_vm : 상태 변경(데이터 갱신)
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; // 상태 업데이트
}
}
- home_repository : 데이터 가져오기(외부 소스에서 데이터 읽어오는 역할)
// 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
키워드로 결과를 처리.
- 코드
- future_page
- future_body
import 'package:flutter/material.dart';
import 'future_Body.dart';
// Riverpod 없이 통신 데이터 가져오기
class FuturePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBody(),
);
}
}
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 객체로 변환하는 과정을 거친다.
- post_page
import 'package:flutter/material.dart';
import 'package:mockapp/post_body.dart';
class PostPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: PostBody(),
);
}
}
- post_body
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(),
],
);
}
}
}
- post_page_vm
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; // 포스트를 상태에 뿌리기 -> 화면 열리면 바로됨
}
}
- post_repository : 서버에서 게시물 데이터 가져오기. Dio를 사용해 API 요청을 보냄.
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
페이지 만들어 컬렉션 뿌리기

- post_list_page
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);
},
),
);
}
}
- post_list_body
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}"),
);
},
);
}
}
}
- post_list_page_vm
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;
}
}
- post_repository
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