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
- Flutter
- 자바 출력 방식
- JS
- ListView
- scss
- LLM
- Clean Architecture
- develop
- npm
- 엡
- riverpod
- JQ
- UI/UX
- 앱심사
- abap
- DART
- 자바스크립트
- unity
- 자바 포맷 출력
- printf
- java 콘솔 출력 차이
- java 출력
- firebase
- java
- 배포
- 단축키
- println
- react
- lifecycle
- nodejs
Archives
- Today
- Total
guricode
[클린아키텍쳐]Clean Architecture -4 본문
usecase를 작성한다
usecase는 레파지토리 인터페이스를 참조하여 메서드를 실행한다.
뷰모델에서 usecase를 사용하여 screen에 공급해준다
//fetch_movies_usecase.dart
import 'package:flutter_clean_arch/domain/entity/movie.dart';
import 'package:flutter_clean_arch/domain/repository/movie_repository.dart';
//usecase는 repository interface를 참조한다
class FetchMoviesUsecase {
final MovieRepository _movieRepository;
FetchMoviesUsecase(this._movieRepository);
Future<List<Movie>> execute() async {
return await _movieRepository.fetchMovies();
}
}
테스트
//fetch_movies_usecase_test.dart
import 'package:flutter_clean_arch/domain/entity/movie.dart';
import 'package:flutter_clean_arch/domain/repository/movie_repository.dart';
import 'package:flutter_clean_arch/domain/useacase/fetch_movies_usecase.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
class MockMovieRepository extends Mock implements MovieRepository {}
void main() {
MockMovieRepository? mockMovieRepository;
FetchMoviesUsecase? fetchMoviesusercase;
setUp(() {
mockMovieRepository = MockMovieRepository();
fetchMoviesusercase = FetchMoviesUsecase(mockMovieRepository!);
});
//
test('FetchMoviesUsecase test : fetchMovies', () async {
when(() => mockMovieRepository!.fetchMovies()).thenAnswer(
(_) async => [
Movie(
title: 'title',
released: 'released',
runtime: 'runtime',
director: 'director',
actors: 'actors',
poster: 'poster',
),
],
);
final result = await fetchMoviesusercase!.execute();
expect(result.length, 1);
expect(result.first.title, 'title');
});
}
실제사용할땐 어떤 Repository Impl이 들어왔는지에 따라 실행 로직이 바뀐다.
- 인터페이스 1개 ↔ 구현체 여러 개 가능
- 상황에 따라 Impl을 바꿔 주입하면 됨 (실제 서버용, 캐싱용, 테스트용 등)
- 이게 Clean Architecture나 DI(의존성 주입)에서 인터페이스를 쓰는 핵심 이유
이제 뷰 모델에 usecase를 붙여아한다.
그러려면 뷰 모델이 repository impl을 만들어야하고 data source impl객체도 만들어야한다.
이 작업을 따로 관리하기위해 provider에서 작업할 것이다.
Viewmodel에서 직접 객체 생성하지 않을수 있게 UseCase 공급해주는 Provider생성한다
//providers.dart
//ViewModel 내에서는 Provider에 의해서 useCase 공급받을것
import 'package:flutter/services.dart';
import 'package:flutter_clean_arch/data/data_source/movie_asset_data_source_impl.dart';
import 'package:flutter_clean_arch/data/data_source/movie_data_source.dart';
import 'package:flutter_clean_arch/data/repository/movie_repository_impl.dart';
import 'package:flutter_clean_arch/domain/repository/movie_repository.dart';
import 'package:flutter_clean_arch/domain/useacase/fetch_movies_usecase.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
//데이터소스
final movieDataSourceProvider = Provider<MovieDataSource>((ref) {
//AssetBundle 타입. MovieAssetDataSourceImpl 앱 실행 환경에서 assets 접근을 보장하기 위해 rootBundle 을 넣는 것
return MovieAssetDataSourceImpl(rootBundle);
});
//레파지토리
//객체를 공급해주는 공급자,관리자
final _movieRepositoryProvider = Provider<MovieRepository>((ref) {
final dataSource = ref.read(movieDataSourceProvider);
return MovieRepositoryImpl(dataSource);
});
//유즈케이스
final fetchMoviesUsecaseProvider = Provider((ref) {
final movieRepo = ref.read(_movieRepositoryProvider);
return FetchMoviesUsecase(movieRepo);
});
//뷰모델에 공급해주면 된다
//movie_list_view_model.dart
//1.상태클래스
// 영화 리스트 List<Movie>
import 'package:flutter_clean_arch/domain/entity/movie.dart';
import 'package:flutter_clean_arch/presentation/providers.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class MovieListViewModel extends Notifier<List<Movie>> {
@override
List<Movie> build() {
fetchMovies();
return [];
}
Future<void> fetchMovies() async {
//provider에서 fetchMoviesUsecaseProvider 구독
//fetchMoviesUsecaseProvider는 FetchMoviesUsecase제공
final fetchMoviesUsecase = ref.read(fetchMoviesUsecaseProvider);
final result = await fetchMoviesUsecase.execute();
state = result;
}
}
//뷰모델 공급자
final movieListViewMocelProvider =
NotifierProvider<MovieListViewModel, List<Movie>>(() {
return MovieListViewModel();
});
//테스트진행
이제 뷰 모델을 테스트 한다
//movie_list_view_model_test.dart
import 'package:flutter_clean_arch/domain/entity/movie.dart';
import 'package:flutter_clean_arch/domain/useacase/fetch_movies_usecase.dart';
import 'package:flutter_clean_arch/presentation/pages/movie_list/movie_list_view_model.dart';
import 'package:flutter_clean_arch/presentation/providers.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mocktail/mocktail.dart';
class MockFetchmoviesUsecase extends Mock implements FetchMoviesUsecase {}
void main() {
//리버팟테스트할떄 프로바이더 컨테이너를 이용해 데이터제공
ProviderContainer? providerContainer;
setUp(() {
//MovieListViewModel에서 fetchMoviesUsercase 달라고했을때 가짜객체 전달해주는세팅
final fetchMoviesUsecaseProviderOverride = fetchMoviesUsecaseProvider
.overrideWith((ref) {
return MockFetchmoviesUsecase();
});
providerContainer = ProviderContainer(
//프로바이더 컨테이너를 통해서 fetchMoviesUsecaseProvider에 접근해서
//fetchMoviesUsecase달라고 요청을 하면 MockFetchmoviesUsecase가 리턴된다
overrides: [fetchMoviesUsecaseProviderOverride],
);
//addTearDown으로 테스트가 끝날때 providerContainer가 dispose됨
addTearDown(providerContainer!.dispose);
});
test('MovieListViewModel test : state update after fetchMovies', () async {
when(
() => providerContainer!.read(fetchMoviesUsecaseProvider).execute(),
).thenAnswer(
(_) async => [
Movie(
title: 'title',
released: 'released',
runtime: 'runtime',
director: 'director',
actors: 'actors',
poster: 'poster',
),
],
);
//1.최초상태 빈 배열
final stateBefore = providerContainer!.read(movieListViewMocelProvider);
expect(stateBefore.length, 0);
//2. fetchMovie 호출한뒤 배열상태사이즈 1
await providerContainer!
.read(movieListViewMocelProvider.notifier)
.fetchMovies();
final stateAfter = providerContainer!.read(movieListViewMocelProvider);
expect(stateAfter.isEmpty, false);
expect(stateAfter.length, 1);
expect(stateAfter.first.title, 'title');
});
}
뷰모델까지 완성됐으니 이제 페이지에 공급해줘야한다
//movie_list_page.dart
import 'package:flutter/material.dart';
import 'package:flutter_clean_arch/presentation/pages/movie_list/movie_list_view_model.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class MovieListPage extends StatelessWidget {
const MovieListPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('영화 목록')),
body: Consumer(
//builder로 감싸준후 컨슈머로 교체 후 ref , child추가
builder: (context, ref, child) {
//movieListViewMocelProvider 뷰모델 구독
final movieList = ref.watch(movieListViewModelProvider);
return ListView.builder(
itemCount: movieList.length, //갯수
itemBuilder: (context, index) {
final movie = movieList[index]; //리스트 요소
return Container(
height: 200,
padding: const EdgeInsets.all(10),
child: Row(
children: [
AspectRatio(
aspectRatio: 2 / 3,
child: Image.network(movie.poster, fit: BoxFit.cover),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(movie.title),
Text(movie.released),
Text(movie.runtime),
Text(movie.director),
Text(movie.actors),
],
),
),
],
),
);
},
);
},
),
);
}
}
기존에 작성했던 ListView.builder를 Consumer로 감싸야하는데, Builder위젯으로 자동완성기능으로 감싸준 후 Consumer로 바꾸면 더 간단하게 바꿀수 있다.
전체적인 흐름을 정리해보자
data 레이어
- DTO = 데이터를 변환해준다. 반환타입 DTO타입
- data_source = 데이터 소스에 접근하기 위해 추상클래스로 interface 메서드를 정의한다. 반환타입 DTO타입
- data_source_impl= DataSource 클래스를 상속받아 데이터소스에 접근하기 위한 메서드를 구현한다 반환타입 DTO타입
Domain 레이어
- entity = 앱의 비즈니스 규칙을 표현하는 핵심 모델이다. 데이터의 “형태”뿐 아니라, 그 데이터가 가진 의미와 불변성, 규칙까지 포함한다. 여기서는 Dto와 같은 형태지만 비지니스에 따라 바뀐다. 반환타입 entity 타입
- repository= 레포지토리 구현하기위해 인터페이스작성 반환타입 entity 타입
data 레이어
- repository_impl= DataSource를 데이터소스 인터페이스로 접근하여 entity타입으로 변환. 반환타입 entity 타입
presintation 레이어
- fetch_usecase=레파지토리 인터페이스를 참조하여 메서드를 실행한다.뷰모델에서 usecase를 사용하여 screen에 공급해준다.들어가는 타입 Repository, 반환타입 entity 타입
- provider(아래는 Provider에 대한 설명)
final DataSourceProvider = Provider<DataSource>((ref) {
return DataSourceImpl(파라미터);
});
- DataSource Provider = 실제 데이터를 불러오는 “출처”를 주입
final _RepositoryProvider = Provider<Repository>((ref) {
final dataSource = ref.read(DataSourceProvider);
return RepositoryImpl(dataSource);
});
- Repository Provider = Repository 구현체를 만들어 Domain 계층에 공급.
- DataSourceProvider 를 읽어와서 RepositoryImpl 에 주입.
- RepositoryImpl 은 DataSource에서 가져온 DTO를 Entity로 변환
final fetchUsecaseProvider = Provider((ref) {
final Repo = ref.read(_RepositoryProvider);
return FetchUsecase(Repo);
});
- UseCase Provider = 도메인 로직(UseCase)을 Provider로 등록.
- Repository를 주입받아서, 실제 비즈니스 규칙 단위 기능을 실행할 수 있는 usecase를 만든다.
- ViewModel은 이 Provider를 구독해서 UseCase를 얻는다.
- view_model = provider에서 fUsecaseProvider 구독 UseCase 호출, 뷰모델 공급자 생성
전체 의존성 흐름
UI(ViewModel)
↓ (ref.read)
UseCase (fetchMoviesUsecaseProvider)
↓
Repository (MovieRepositoryImpl, _movieRepositoryProvider)
↓
DataSource (MovieAssetDataSourceImpl, movieDataSourceProvider)
↓
외부 자원
조금 더 복습하고 써봐야 겠지만, 이렇게 정리하니 조금 눈에 들어온다...
프로젝트가 두개정도 남았고 한개가 진행중인데...
열심히 하자 할수있다!!
'앱 > Flutter&Dart' 카테고리의 다른 글
| [flutter-sns-project - 2]Workflow 작성 방법 (0) | 2025.08.31 |
|---|---|
| [flutter-sns-project - 1] 환경설정 (1) | 2025.08.31 |
| [클린아키텍쳐]Clean Architecture -3 (0) | 2025.08.30 |
| [클린아키텍쳐]Clean Architecture -2 (1) | 2025.08.30 |
| [클린아키텍쳐]Clean Architecture -1 (1) | 2025.08.30 |