Flutter 아키텍처(Get_it + Riverpod + hooks 조합)

고랭지참치·2025년 4월 9일
0

Flutter

목록 보기
17/24

깃헙

https://github.com/MinwooRowan/get_it_study

Flutter Architectire 핵심

  • 레이어 아키텍처를 통한 관심사 분리
    • reusable, lean widgets that hold as little logic as possible.
    • 비즈니스 로직과 UI의 분리
    • 모듈의 운용
  • SSOT (Single Source of Truth)를 통한 데이터 일치성 유지
    • "앱의 모든 데이터는 단일 출처에서만 관리되어야 한다”
    • Repository 사용을 통해 데이터 소스가 하나의 원천에서 제공될 수 있도록 통제해야 한다.

레이어 운용

UI Layer (View + ViewModel)

  • View
    • 역할
      • 앱 데이터를 사용자에게 표시
    • 책임
      • 사용자 입력 이벤트를 처리하고 ViewModel로 전달.
      • 비즈니스 로직을 포함하지 않는다.
      • 필요한 데이터를 ViewModel에서 받아온다.
  • ViewModel
    • 역할
      • 데이터를 UI에서 사용할 수 있도록 변환하고 관리.
    • 책임
      • Repository에서 데이터를 가져오고, UI에 적합한 형식으로 변환.
    • *Command라는 개념!

Data Layer (Repository + Service)

  • Repository
    • 역할
      • 데이터를 가져오고 가공하여 도메인 모델(domain model)로 변환.
      • 각 데이터 타입마다 하나의 레포지토리 클래스 생성.
        • ex) UserModel → UserRepository / Post → PostRepository
    • 책임
      • 서비스 호출: 데이터를 API나 로컬 소스에서 가져옴.
      • 로직처리 - 캐싱, 오류처리, 데이터 갱신
      • 도메인 모델 생성 - ViewModel(or Usecase)에서 사용가능한 형태로 변환
      • ViewModel은 여러 레포지토리에서 데이터를 가져올 수 있음.
      • 레포지토리는 서로 참조하지 않는다.
  • Service
    • 역할
      • 데이터 로드
      • API, LocalData 등
    • 특징
      • 레포지토리는 여러 서비스를 호출 가능.

Domain Layer

  • ViewModel에서 지나치게 복잡한 로직을 분리하여 관리하는 선택적 계층.
  • Use Case의 역할:
    • 레포지토리 데이터를 가공하여 UI 계층에서 사용하기 쉽게 변환.
    • 로직을 재사용 가능하고 단순화.
    • 다음과 같은 경우에 유용:
      • 여러 레포지토리의 데이터 병합 필요.
      • 복잡한 로직 처리.
      • 다른 ViewModel에서도 로직이 반복적으로 사용.
  • Domain Layer는 필요할때만 추가하는 전략을 취하여 복잡성을 낮출 수 있으나, 반대로 항상 추가하여 모듈화와 테스트 가능성을 높힐 수 있다.

역할의 분리

  • Riverpod → App State 상태 관리
  • Get_it → 의존성 주입
  • Hooks → Local State 상태 관리
  • viewmodel → view 비즈니스 로직 관리

Viewmodel 도입

🗒️

viewmodel은 UI상태관리를 직접적으로 하지 않도록 한다.

UI상태관리는 provider, hooks를 통해서 진행

Router를 통해서 Ref를 주입받고, initializer 제공

Viewmodel Interface

abstract class CommonViewmodel {
  CommonViewmodel(this.ref) {
    initializer();
  }
  
  final Ref ref;
  
  Future<Result> initializer();
  void dispose();
}

ViewModel 구현 예시


class CombViewmodel extends CommonViewmodel {
  CombViewmodel(
     super.ref,
    UserUsecase userUsecase,
  ) : _userUsecase = userUsecase {
  
	  // Viewmodel 초기화시 진행 함수 작성
    getUserList = Command0(_getUserList);
    getUserList.execute();
  }

  final UserUsecase _userUsecase;
  
	// 함수 은닉화를 위한 Command함수 (from Flutter Architecture)
  late final Command0<List<UserEntity>> getUserList;

  
  Future<void> initializer() {
    return getUserList.execute();
  }
  
  
  void dispose() {
	  //
  }

  Future<Result<List<UserEntity>>> _getUserList() async {
	  //
  }
}

Command

  • 함수 은닉화 및 중복요청 방지
  • 리턴타입을 통한 처리 통일
abstract class Command<T> extends ChangeNotifier {
  Command();
  bool running = false;
  Result<T>? _result;

  /// true if action completed with error
  bool get error => _result is Error;

  /// true if action completed successfully
  bool get completed => _result is Ok;

  /// Internal execute implementation
  Future<void> _execute(action) async {
    if (_running) return;

    // Emit running state - e.g. button shows loading state
    _running = true;
    _result = null;
    notifyListeners();

    try {
      _result = await action();
    } finally {
      _running = false;
      notifyListeners();
    }
  }
}

Recommendation

관심사분리

Strongly Recommend

  • 데이터와 UI를 분리하기 → 노력중
  • Data Layer에서 Repository패턴 사용하기 → 적용
  • Viewmodel과 View를 사용한 UI Layer운용 → 적용전
  • 위젯에 로직 넣지 않기 → 노력중
    • View에 들어갈 수 있는 로직은 다음과 같다
      • 간단한 조건부 로직 (if문):
        • 위젯 표시 및 숨기기:
          • ViewModel의 플래그(boolean) 또는 nullable 필드 값을 기반으로 위젯의 가시성을 제어.

            
            if (viewModel.isLoading) {
              return CircularProgressIndicator();
            } else {
              return Text("Content Loaded");
            }
            
      • 애니메이션 로직:
        • 위젯 자체에서 계산해야 하는 애니메이션.
        • Flutter의 AnimationController 또는 AnimatedWidget 관련 로직.
          
          AnimatedOpacity(
            opacity: viewModel.isVisible ? 1.0 : 0.0,
            duration: Duration(milliseconds: 300),
            child: Text("Fade In Text"),
          );
          
      • 레이아웃 로직:
        • 디바이스 정보(화면 크기, 방향 등)에 따라 레이아웃을 조정.
        • 예: 화면 크기에 따라 다른 위젯을 표시.
          
          if (MediaQuery.of(context).size.width > 600) {
            return LargeScreenWidget();
          } else {
            return SmallScreenWidget();
          }
          
      • 간단한 라우팅 로직:
        • 버튼 클릭 등에서 경로 이동을 처리.
          
          onPressed: () {
            Navigator.pushNamed(context, '/details');
          }
          

Conditional

  • ChangeNotifier, Listenables를 통한 UI State 업데이트 → 적용
  • Domain Layer사용해서 ViewModel 무게 줄이기 → 일부 진행중

데이터 관리

Strongly Recommend

  • 일방향 데이터 플로우 → 적용
  • 불변성 데이터 모델 사용하기 → 적용

Recommend

  • Commands를 통해서 이벤트 처리하기 → 적용전
    • 은닉화 및 중복처리 방지
  • freezed or built_value를 통해서 데이터 모델 자동화 생성 → 일부 진행중

Conditional

  • Api Model과 Domain Model 분리하기 → 적용

앱 구조

Strongly Recommend

  • DI 사용하기 → 적용
  • Repository Interface 운영하기 → 적용

Recommend

  • goRouter를 통해서 내비게이션 사용하기 → 적용
  • 작명 컨벤션 적용하기 → 적용
profile
소프트웨어 엔지니어 / Flutter 개발자

0개의 댓글