
Row, Column, Flex 안에서 사용
남은 공간을 유연하게 분배하는 위젯
자식 위젯의 크기를 강제하지 않음
Flexible(
child: Widget(),
)
Row(
children: [
Flexible(child: Container()),
Flexible(child: Container()),
],
)
남은 공간 분배 비율
기본값: 1
Flexible(
flex: 2,
child: Container(),
)
공간을 채우는 방식 결정
| 값 | 설명 |
|---|---|
FlexFit.loose | 필요한 만큼만 사용 (기본값) |
FlexFit.tight | 남은 공간을 꽉 채움 |
Flexible(
fit: FlexFit.loose,
child: Widget(),
)
남은 공간을 무조건 채움
내부적으로 Flexible + FlexFit.tight
Expanded(
child: Widget(),
)
| 항목 | Flexible | Expanded |
|---|---|---|
| 공간 강제 | ❌ | ⭕ |
| 자식 크기 존중 | ⭕ | ❌ |
| 기본 fit | loose | tight |
| 사용 목적 | 유연한 크기 | 공간 채우기 |
현재 디바이스의 화면 정보를 가져오는 위젯
화면 크기, 방향, 안전 영역, 텍스트 배율 등 확인 가능
반응형 UI 만들 때 필수
👉 “이 기기가 어떤 화면 상태인지 알려주는 도구”
기기마다 화면 크기가 다르기 때문
고정 px 사용 시 레이아웃 깨짐 방지
비율 기반 UI 설계 가능
final size = MediaQuery.of(context).size;
context 기준으로 현재 화면 정보 가져옴
보통 build() 안에서 사용
MediaQuery.of(context).size.width
MediaQuery.of(context).size.height
| 속성 | 설명 |
|---|---|
| width | 화면 가로 길이 |
| height | 화면 세로 길이 |
MediaQuery.of(context).orientation
Orientation.portrait : 세로
Orientation.landscape : 가로
textScaleFactor
MediaQuery.of(context).textScaleFactor
사용자가 시스템에서 설정한 글자 크기 비율
접근성 대응에 중요
TextScaler
MediaQuery.of(context).textScaler
텍스트 크기 계산 로직을 객체로 관리
선형 / 비선형 스케일링 가능
위젯 단위 제어 가능
① 시스템 기본
TextScaler.noScaling
스케일링 ❌
접근성 무시 (주의)
② 선형 스케일링
TextScaler.linear(1.2)
기존 textScaleFactor와 동일한 개념
모든 텍스트를 동일 비율로 확대
③ MediaQuery 기반 (실전)
final textScaler = MediaQuery.of(context).textScaler;
접근성 대응 O
가장 안전한 방식
MediaQuery.of(context).padding
상태바, 노치, 홈 인디케이터 영역 포함
블로그 앱 UI 기초 구현
TextFormField 포함 레이아웃 구성
MVVM 구조 + Riverpod 기반 프로젝트 세팅
페이지 구성
Home : 글 목록
Detail : 글 상세
Write : 글 작성 / 수정
→ 글쓰기 & 글수정은 구조 동일 → 하나의 페이지 재사용
상단: AppBar
본문: 입력폼(TextFormField), 버튼 등
공통 구조는 위젯 분리해서 재사용
lib/
┣ data/
┃ ┣ model/ → 데이터 모델
┃ ┗ repository/ → 데이터 가공 및 변환
┣ ui/
┃ ┣ pages/
┃ ┃ ┣ home/
┃ ┃ ┣ detail/
┃ ┃ ┗ write/
┃ ┗ widgets/ → 공용 위젯
┗ main.dart
Page : UI 담당
ViewModel : 상태 & 로직 담당
widgets/ : 페이지 전용 / 공용 위젯 분리
Scaffold
├ AppBar
└ Padding
└ Column
├ Text ("최근 글")
└ Expanded (ListView)
Column 안에서 ListView 사용 시 반드시 Expanded 필요
ListView
├ Image.network
├ SizedBox
└ Padding
└ Column
├ 제목
├ 작성자
├ 날짜
└ 본문
전체 스크롤 → ListView
이미지에는 패딩 ❌ → 텍스트 영역만 Padding 적용
padding: EdgeInsets.only(bottom: 300)
하단 여유 공간 확보 (추후 버튼/입력 UI 대비)
TextField + 유효성 검사 기능
validator 함수로 검증
❌ 실패 → 에러 문자열 반환
⭕ 성공 → null 반환
왜 Form으로 감싸야 하나?
여러 TextFormField를 한 번에 검증
Form
└ ListView
└ TextFormField들
특정 Form 위젯의 상태(FormState)에 접근
formKey.currentState?.validate() 호출 가능
formKey.currentState!.validate();
내부의 모든 TextFormField의 validator 실행
실패 시 에러 메시지 표시
GestureDetector
└ Scaffold
├ AppBar
└ Form
└ Padding
└ ListView
├ 작성자
├ 제목
├ 내용(멀티라인)
└ 이미지 업로드 영역
작성자 / 제목
단일 라인 입력
TextInputAction.done
내용
여러 줄 입력
maxLines: null
expands: true
keyboardType: multiline
서버 구현 없이 앱 개발을 도와주는 구글의 백엔드 플랫폼
모바일 / 웹 앱에서 로그인, 데이터 저장, 파일 업로드 등을 쉽게 구현 가능
서버 직접 구현 ❌
설정 + 앱 코드만으로 실제 서비스 구조 구현 가능
로그인 / 회원가입 구현용
앱 데이터 저장용 데이터베이스
이미지, 파일 저장소
NoSQL 기반 데이터베이스
JSON 형태 (key : value)로 데이터 저장
실시간 동기화 지원
오프라인에서도 동작 가능
다양한 조건 검색 가능
Collection : 문서들의 묶음
Document : 실제 데이터 단위 (JSON 형태)
Collection : Posts
Document : 각각의 블로그 글
이미지 / 동영상 / 파일 저장소
Firestore에는 파일 URL만 저장
실제 파일은 Storage에 저장
Firestore는 파일 저장 용량 제한 (약 1MB)
Storage는 파일 전용
Firebase Console → 프로젝트 만들기
Analytics 비활성화
Cloud Firestore 생성 (지역: 서울)
Firebase 설정을 쉽게 해주는 도구
firebase login
dart pub global activate flutterfire_cli
flutterfire configure
Firebase 프로젝트 선택
플랫폼 선택 (Android / iOS)
패키지명 입력
예: com.example.flutter_blog_app
✔️ 결과
firebase_options.dart 생성
android / ios 설정 파일 자동 추가
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(const ProviderScope(child: MyApp()));
}
ios/Podfile
iOS 최소 버전 15 이상으로 변경
flutter pub add firebase_core
flutter pub add cloud_firestore
CREATE : 생성
READ : 조회
UPDATE : 수정
DELETE : 삭제
Firestore → Collection 참조 → get() → QuerySnapshot → docs → data()
핵심
collectionRef.get()
결과 타입: QuerySnapshot
문서 목록: snapshot.docs
Firestore → Collection → doc(id) → get() → DocumentSnapshot
핵심
문서 ID 필수
결과 타입: DocumentSnapshot
data()로 실제 데이터 접근
Collection → doc() → set(data)
핵심
doc() → 자동 ID 생성
set(Map) 호출 시 실제 저장
날짜는 보통 DateTime.toIso8601String() 사용
Collection → doc(id) → set(data)
주의
set()은 기존 데이터 덮어씀
일부 필드만 수정하려면 update() 사용 가능
Collection → doc(id) → delete()
Firestore → posts 컬렉션 → get()
→ QuerySnapshot → docs
→ Map 가공 → Post.fromJson → List<Post>
final docs = result.docs;
return docs.map((doc) {
final map = doc.data();
final newMap = {'id': doc.id, ...map};
return Post.fromJson(newMap);
}).toList();
Firestore 문서에는 두 덩어리가 있음
doc
├─ doc.id ← 문서 고유 ID
└─ doc.data() ← 실제 데이터 (Map)
❗ doc.data() 안에는 id가 없음
Post.fromJson({
'id': '문서ID',
'title': '제목',
'content': '내용',
});
👉 id + 데이터가 하나의 Map으로 필요
final map = doc.data();
Firestore 문서 안의 데이터만 꺼냄
타입: Map<String, dynamic>
final newMap = {'id': doc.id, ...map};
doc.id 👉 Firestore 문서 고유 ID
...map 👉 기존 데이터 전부 펼쳐서 합치기
.toList()
map() 결과는 아직 Iterable
.toList()로 List<Post>로 변환
doc.id는 data에 없으므로 직접 추가
map() → toList()로 변환
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_firebase_blog_app/data/model/post.dart';
class PostRepository {
Future<List<Post>?> getAll() async {
try {
// 1. 파이어스토어 인스턴스 가져오기
final firestore = FirebaseFirestore.instance;
// 2. 컬렉션 참조 가져오기
final collectionRef = firestore.collection('posts');
// 3. 값 불러오기
final result = await collectionRef.get();
final docs = result.docs;
return docs.map((doc) {
final map = doc.data();
final newMap = {'id': doc.id, ...map};
return Post.fromJson(newMap);
}).toList();
} catch (e) {
print(e);
return null;
}
}
// 1. Create: 데이터 쓰기
Future<bool> insert({
required String title,
required String content,
required String writer,
required String imageUrl,
}) async {
try {
// 1. 파이어스토어 인스턴스 가져오기
final firestore = FirebaseFirestore.instance;
// 2. 컬렉션 참조 가져오기
final collectionRef = firestore.collection('posts');
// 3. 문서 참조 만들기
final docRef = collectionRef.doc();
// 4. 값 쓰기
await docRef.set({
'title': title,
'content': content,
'writer': writer,
'imageUrl': imageUrl,
'cretrAt': DateTime.now().toIso8601String(),
});
return true;
} catch (e) {
print(e);
return false;
}
}
// 2. Read: 하나의 Doc 가져오기
Future<Post?> getOne(String id) async {
try {
// 1. 파이어스토어 인스턴스 가져오기
final firestore = FirebaseFirestore.instance;
// 2. 컬렉션 참조 가져오기
final collectionRef = firestore.collection('posts');
// 3. 문서 참조 만들기
final docRef = collectionRef.doc(id);
// 4. 데이터 가져오기
final doc = await docRef.get();
Post.fromJson({'id': doc.id, ...doc.data()!});
} catch (e) {
print(e);
return null;
}
}
// 3. Udate: 도큐먼트 수정
Future<bool> update({
required String id,
required String title,
required String content,
required String writer,
required String imageUrl,
}) async {
try {
// 1. 파이어스토어 인스턴스 가져오기
final firestore = FirebaseFirestore.instance;
// 2. 컬렉션 참조 가져오기
final collectionRef = firestore.collection('posts');
// 3. 문서 참조 만들기
final docRef = collectionRef.doc(id);
// 4. 값을 업데이트 해주기 (set메서드)
// 업데이트 할 값 Map 형태로 넣어주기 :id에 해당하는 문서가 없을 때 새로 생성
// docRef.set(data);
await docRef.update({
'title': title,
'content': content,
'writer': writer,
'imageUrl': imageUrl,
});
return true;
} catch (e) {
print(e);
return false;
}
}
// 4. Delete: 도큐먼트 삭제
Future<bool> delete(String id) async {
try {
// 1. 파이어스토어 인스턴스 가져오기
final firestore = FirebaseFirestore.instance;
// 2. 컬렉션 참조 가져오기
final collectionRef = firestore.collection('posts');
// 3. 문서 참조 만들기
final docRef = collectionRef.doc(id);
// 4. 삭제
await docRef.delete();
return true;
} catch (e) {
print(e);
return false;
}
}
}
상태(State) : List<Post>
역할 : Firestore에서 전체 포스트 조회 → 상태 갱신
Firestore → Repository → ViewModel → UI(ListView)
ViewModel은 상태만 관리, UI는 상태를 그리기만 한다
arg를 build()에서 받지 않고
Notifier 생성자에서 직접 받는 방식으로 변경됨
상태(State) : Post?
입력값(arg) : Post
기능
상세 화면에서 사용할 Post 유지
해당 Post 삭제 (Firestore)
class DetailViewModel extends Notifier<Post?> {
final Post arg;
DetailViewModel(this.arg);
Post? build() {
return arg;
}
Future<bool> deletePost() async {
final postRepository = PostRepository();
return await postRepository.delete(arg.id);
}
}
arg는 선택된 Post
build()에서 해당 Post를 그대로 state로 설정
삭제 시 arg.id 사용
final detailViewModelProvider =
NotifierProvider.autoDispose.family<DetailViewModel, Post?, Post>(
(arg) {
return DetailViewModel(arg);
},
);
| 요소 | 의미 |
|---|---|
family | Post마다 다른 ViewModel 생성 |
autoDispose | DetailPage 종료 시 자동 해제 |
<DetailViewModel, Post?, Post> | ViewModel / 상태 / 전달 인자 |
| 이전 방식 | 현재 방식 |
|---|---|
AutoDisposeFamilyNotifier | Notifier + family |
| build(arg) | 생성자에서 arg 받음 |
| arg를 내부 필드로 사용 | 더 직관적 |