Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
Tags
- 배포
- ListView
- react
- npm
- nodejs
- Clean Architecture
- abap
- unity
- 자바 출력 방식
- JQ
- printf
- 자바스크립트
- riverpod
- DART
- 엡
- LLM
- firebase
- java 콘솔 출력 차이
- java 출력
- java
- UI/UX
- lifecycle
- Flutter
- scss
- 자바 포맷 출력
- 앱심사
- JS
- develop
- println
- 단축키
Archives
- Today
- Total
guricode
[자취의 정석] Flutter 앱에 "전체" 카테고리 탭 추가하기: Clean Architecture 기반 구현 본문
앱/Flutter&Dart
[자취의 정석] Flutter 앱에 "전체" 카테고리 탭 추가하기: Clean Architecture 기반 구현
agentrakugaki 2025. 10. 12. 20:14Flutter 앱의 커뮤니티 화면에 "전체" 카테고리 탭을 추가하는 과정을 Clean Architecture 패턴을 기반으로 구현해보겠습니다. 이 기능을 통해 사용자는 자신의 지역에 있는 모든 카테고리의 게시글을 한 번에 볼 수 있게 됩니다.
자취의 정석 커뮤니티는 탭이 2depth로 되어있다

메인 카테고리가 있고 서브카테고리가 있어서 이용자가 탭을 선택해서 볼수 있다
그런데 디자이너에게 전체탭을 추가해 달라는 요청이 있었다.
그래서 로직을 몇가지 수정하고 전체탭을 추가해야했다.
개선 클린 아키텍처 구조
lib/
├── domain/
│ ├── entities/community.dart
│ ├── repositories/community_repository.dart
│ └── usecases/fetch_communities.dart
├── data/
│ ├── datasources/community_data_source.dart
│ ├── datasources/community_data_source_impl.dart
│ └── repositories/community_repository_impl.dart
└── presentation/
├── screens/community/community_screen.dart
└── screens/community/vm/community_list_vm.dart
1단계: Domain Layer 수정
기존에 만들어진 코드 중 몇가지를 수정했다
원래는 카테고리코드가 nullable이 아니었다.
하지만 이제 전체를 조회해야하기때문에 사용가능하게 바꿔야했다.
따라서 null값이 가능하도록 교체해줬다.
UseCase
// lib/domain/usecases/fetch_communities.dart
class FetchCommunities {
final CommunityRepository repo;
FetchCommunities(this.repo);
Future<(List<Community> items, DocumentSnapshot? lastDoc, bool hasMore)>
call({
int? categoryCode, // nullable로 변경
int? categoryDetailCode, // nullable로 변경
required String location,
int limit = 10,
DocumentSnapshot? startAfter,
bool desc = true,
}) async {
final page = await repo.fetch(
categoryCode: categoryCode,
categoryDetailCode: categoryDetailCode,
location: location,
limit: limit,
startAfter: startAfter,
desc: desc,
);
return (page.items, page.lastDoc, page.hasMore);
}
}
Repository Interface 수정
// lib/domain/repositories/community_repository.dart
abstract interface class CommunityRepository {
Future<PagedCommunity> fetch({
int? categoryCode, // nullable로 변경
int? categoryDetailCode, // nullable로 변경
required String location,
int limit,
DocumentSnapshot? startAfter,
bool desc,
});
}
2단계: Data Layer 수정
Data 레이어에는 조회쿼리가 들어가있기 때문에 이부분을 수정해줬다.
위치정보는 그대로 사용해야하기때문에 필수값이다.
DataSource Interface 수정
// lib/data/datasources/community_data_source.dart
abstract interface class CommunityDataSource {
Future<PagedResult<CommunityDto>> fetchCommunities({
int? categoryCode, // nullable로 변경
int? categoryDetailCode, // nullable로 변경
required String location,
int limit,
DocumentSnapshot? startAfterDoc,
bool orderDesc,
});
}
DataSource Implementation 수정
// lib/data/datasources/community_data_source_impl.dart
@override
Future<PagedResult<CommunityDto>> fetchCommunities({
int? categoryCode, // nullable로 변경
int? categoryDetailCode, // nullable로 변경
required String location,
int limit = 10,
DocumentSnapshot<Object?>? startAfterDoc,
bool orderDesc = true,
}) async {
Query<Map<String, dynamic>> q = col
.where('community_delete_yn', isEqualTo: false);
// 카테고리 필터링 (null이 아닐 때만)
if (categoryCode != null) {
q = q.where('category_code', isEqualTo: categoryCode);
}
if (categoryDetailCode != null) {
q = q.where('category_detail_code', isEqualTo: categoryDetailCode);
}
// 위치 필터링
if (location.isNotEmpty) {
q = q.where('location', isEqualTo: location);
}
// 정렬
q = q
.orderBy('community_create_date', descending: orderDesc)
.limit(limit);
if (startAfterDoc != null) {
q = q.startAfterDocument(startAfterDoc);
}
final snap = await q.get();
final items = snap.docs
.map((d) => CommunityDto.fromFirebase(d.id, d.data()))
.toList();
final lastDoc = snap.docs.isNotEmpty ? snap.docs.last : null;
return PagedResult(
items: items,
lastDoc: lastDoc,
hasMore: snap.docs.length == limit,
);
}
3단계: Presentation Layer 수정
ViewModel 수정
// lib/presentation/screens/community/vm/community_list_vm.dart
class CommunityListVM extends Notifier<CommunityListState> {
CommunityListVM(this.categoryCode, this.detailCode, this.location);
final int? categoryCode; // nullable로 변경
final int? detailCode; // nullable로 변경
final String location;
// ... 기존 메서드들 유지
}
/// provider 팩토리 수정
NotifierProvider<CommunityListVM, CommunityListState> communityListVmProvider({
int? categoryCode, // nullable로 변경
int? detailCode, // nullable로 변경
required String location,
}) {
return NotifierProvider<CommunityListVM, CommunityListState>(
() => CommunityListVM(categoryCode, detailCode, location),
);
}
UI 수정
// lib/presentation/screens/community/community_screen.dart
return DefaultTabController(
length: parents.length + 1, // "전체" 탭 추가
child: Scaffold(
appBar: AppBar(
// ... 기존 AppBar 설정
bottom: TabBar(
// ... 기존 TabBar 설정
tabs: [
const Tab(text: '전체'), // "전체" 탭 추가
...parents.map((p) => Tab(text: p.categoryName)).toList(),
],
),
),
body: TabBarView(
children: [
// "전체" 탭 뷰
hasLocation
? _AllPostsView(location: location)
: const NoLocationView(),
// 기존 카테고리 탭 뷰들
...parents.map((p) {
return hasLocation
? _SecondDepthTabs(
parentCode: p.categoryCode,
location: location,
)
: const NoLocationView();
}).toList(),
],
),
),
);
"전체" 탭 전용 위젯 추가
// "전체" 탭 뷰 - 모든 카테고리의 게시글을 표시
class _AllPostsView extends ConsumerStatefulWidget {
const _AllPostsView({required this.location});
final String? location;
@override
ConsumerState<_AllPostsView> createState() => _AllPostsViewState();
}
class _AllPostsViewState extends ConsumerState<_AllPostsView> {
late NotifierProvider<CommunityListVM, CommunityListState> provider;
bool _ready = false;
ProviderSubscription<int>? _changedSub;
@override
void initState() {
super.initState();
_maybeInitProviderAndLoad();
_changedSub = ref.listenManual<int>(
communityChangedTickProvider,
(prev, next) {
if (!_ready || !mounted) return;
ref.invalidate(provider);
Future.microtask(() => ref.read(provider.notifier).loadInitial(ref));
},
);
}
void _maybeInitProviderAndLoad() {
final loc = widget.location;
if (loc == null || loc.isEmpty) return;
provider = communityListVmProvider(
categoryCode: null, // 모든 카테고리
detailCode: null, // 모든 하위 카테고리
location: loc,
);
_ready = true;
Future.microtask(() {
if (!mounted) return;
ref.read(provider.notifier).loadInitial(ref);
});
}
@override
Widget build(BuildContext context) {
if (!_ready) return const NoLocationView();
final st = ref.watch(provider);
// ... 게시글 리스트 UI 구현
}
}
참고 자료
'앱 > Flutter&Dart' 카테고리의 다른 글
| [Dart] 자주쓰는 Object 키워드 (예약어) (0) | 2025.10.14 |
|---|---|
| [자취의 정석] 자취도우미 ai 챗봇 만들기 - api연결, data레이어 작성 (0) | 2025.10.13 |
| [트러블슈팅] 댓글 작성 후 리스트 반영이 안될 때 (0) | 2025.10.12 |
| [Flutter] GoRouter 라우터 정리 – push, go, pushReplacement 차이 (0) | 2025.10.10 |
| [Flutter] 글 수정 후 뒤로가기 했을 때 이전 글이 보이는 문제 해결기 (0) | 2025.10.09 |