Clean Architecture와 Dependency Injection 개념적인 내용보다는 실제 샘플앱을 어떤식으로 개발했는지와 그 후기입니다.
https://github.com/cw-hanna/flutter_clean_architecture_sample
사용자가 Presentation Layer에 있는 ScreenWidget에 이벤트를 발생시키면 Bloc혹은 Provider에서 Buisiness Rule이 담긴 UseCase를 사용하여 로직을 수행한다.
이때 UseCase는 Repository 인터페이스를 주입받아 Api 개체가 주입된 Repository 구현체를 실행시킨다. (UseCase는 Repository 인터페이스만 호출할뿐 Repository구현체나 Api객체를 알 필요가 없다.)
호출 결과를 Bloc 혹은 Provider에서 notify하면서 ScreenWidget이 재 빌드되게 된다.
DataSource : API호출, DB접근, DTO
Entities : 순수한 형태의 기본 model클래스, 꼭 model이 아니어도 불변의 룰이나 핵심적인 무언가(Buisiness Object)
UseCase : 사용목적에 맞는 repository 호출로 결과값을 다루는 (Buisiness Rule)
BLOC, Provider : 저장된 값에 따라 UI를 업데이트 시키는 BLOC, Provider, Widget
config : 앱의 전반적 구성요소들.
core : 앱에서 전역적으로 사용되는 요소들
data : data layer 관련 요소들
domain : domain layer 관련 요소들
presentation : presentation layer 관련 요소들
get_it은 DI가 목적이 아니라 Service Locator패턴을 다트에 제공하는게 원래 목적이다.
쉽게 말해서 Locator라는 컨테이너 안에 객체나 서비스들을 몽땅 저장한후, 필요할때마다 Locator에서 꺼내쓰는 패턴이다.
주입될 객체들을 Locator컨테이너에 모두 등록하고 의존성이 필요할때마다 Locator에서 꺼내서 주입시켜주는 방식으로 DI를 할수있다.
getIt공식문서에 register, registerSingleton, registerLazySingleton, registerFactoryAsync, unregister등 여러 기능을 제공한다.
//register 종류
void registerFactory<T>(FactoryFunc<T> func) : call할때마다 new instance / 등록과 동시에 인스턴스 생성
void registerFactoryParam<T,P1,P2>(FactoryFuncParam<T,P1,P2> factoryfunc, {String instanceName}); : registerFactory와 동일하지만 최대 2개의 파라미터를 던질수있다는 차이점. / 등록과 동시에 인스턴스 생성
void registerSingleton<T>(T instance) : 싱글톤 객체 등록. call 할때마다 동일한 객체 반환. / 등록과 동시에 인스턴스 생성
void registerLazySingleton<T>(FactoryFunc<T> func) : registerSingleton과 동일하지만 / 호출됐을때 인스턴스 생성.
//인스턴스 등록시 비동기 동작 호출 하는 경우
void registerFactoryAsync<T>(FactoryFuncAsync<T> func, {String instanceName});
void registerSingletonAsync<T>(FactoryFuncAsync<T> factoryfunc,
{String instanceName,
Iterable<Type> dependsOn,
bool signalsReady = false});
//Unregister
void unregister<T>({Object instance,String instanceName, void Function(T) disposingFunction}) : 등록된 싱글톤이나 팩토리를 등록해제, disposingFunction은 등록을 리셋하기전에 리소스를 해제할경우 사용할 수 있다.
///isRegistererd
bool isRegistered<T>({Object instance, String instanceName}); : 이미 등록되어있는지 체크
//Resetting LazySingletons
void resetLazySingleton<T>({Object instance, String instanceName,void Function(T) disposingFunction}) : LazySingleton을 등록해제하는것은 원치않지만 인스턴스를 초기화시켜서 다음 호출때 새로 생성
//Resetting GetIt completely
Future<void> reset({bool dispose = true}); : 모든 등록된 타입과 인스턴스를 초기화. 만약에 등록할때 dispose 메서드를 제공했다면 dispose = true 옵션을 주었을때 해당 dispose 메서드를 실행
사용예)
final serviceLocator = GetIt.instance;
void initServiceLocator() {
//Register Api Instatnce
serviceLocator.registerLazySingleton<CommitApi>(() => CommitApi());
serviceLocator.registerLazySingleton<OrgApi>(() => OrgApi());
serviceLocator.registerLazySingleton<SearchUserApi>(() => SearchUserApi());
...
//Register RepositoryImpl instance
serviceLocator.registerLazySingleton<CommitApiRepository>(
() => CommitApiRepositoryImpl());
serviceLocator
.registerLazySingleton<OrgApiRepository>(() => OrgApiRepositoryImpl());
serviceLocator.registerLazySingleton<SearchUserApiRepository>(
() => SearchUserApiRepositoryImpl());
//Register UseCase instance
serviceLocator
.registerLazySingleton<GetCommitsUseCase>(() => GetCommitsUseCase());
serviceLocator.registerLazySingleton<GetOrgsUseCase>(() => GetOrgsUseCase());
serviceLocator
.registerLazySingleton<SearchUserUseCase>(() => SearchUserUseCase());
}
app start 시점에 lazy를 제외한 모든 인스턴스가 등록(생성)된다.
해당 인스턴스를 더이상 쓰지 않는시점에 수동으로 unregister로 해제필요.
ex)
class GetOrgsUseCase {
OrgApiRepository? repository;
//추후 mockito를 활용한 유닛테스트를 위해 주입할 객체인 repository를 optional parameter로 받게하고
repository가 null일경우 locator에서 객체를 주입받도록 함.
GetOrgsUseCase({OrgApiRepository? repository})
: repository = repository ?? serviceLocator<OrgApiRepository>();
Future<Result<List<Org>>> call() async {
final result = await repository!.fetch();
return result.when(success: (orgs) {
return Result.success(orgs);
}, error: (message) {
return Result.error(message);
});
}
}
class OrgApiRepositoryImpl implements OrgApiRepository {
OrgApi? api;
//추후 mockito를 활용한 유닛테스트를 위해 주입할 객체인 api optional parameter로 받게하고
api null일경우 locator에서 객체를 주입받도록 함.
OrgApiRepositoryImpl({OrgApi? api}) : api = api ?? serviceLocator<OrgApi>();
@override
Future<Result<List<Org>>> fetch() async {
final Result<Iterable> result = await api!.fetch();
return result.when(
success: (iterable) {
return Result.success(iterable.map((e) {
return OrgModel.fromJson(e);
}).toList());
},
error: (message) {
return Result.error(message);
},
);
}
}
플러터로 클린아키텍쳐 예제는 흔하지 않은데, sample 코드부터 폴더구조까지 설명이 디테일하네요!! 도움이 많이 될것같아요!