[Riverpod] Riverpod 예제 프로젝트 분석 - (1)

Yellowtoast·2023년 2월 10일
1

Flutter Riverpod

목록 보기
2/2
post-thumbnail

해당 글은 Riverpod 공식 문서에 링크되어있는 starter_architecture_flutter_firebase 프로젝트의 구조를 분석하며 작성된 글입니다.

Router 및 첫 화면 (Go Router 사용)

앱을 실행한 뒤 다음 화면을 결정하는 것은 Go Router의 routerConfig기능을 사용하였습니다.

class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final goRouter = ref.watch(goRouterProvider);
    return MaterialApp.router(
      routerConfig: goRouter,
      theme: ThemeData(
        primarySwatch: Colors.indigo,
        unselectedWidgetColor: Colors.grey,
        appBarTheme: const AppBarTheme(
          elevation: 2.0,
          centerTitle: true,
        ),
        scaffoldBackgroundColor: Colors.grey[200],
      ),
      debugShowCheckedModeBanner: false,
    );
  }
}

맨 처음 시작되는 MyApp에서는, goRouterProvider를 바라보도록 설계되어 있습니다. 그리고 해당 goRouter은 MaterialApp.router의 속성 중 하나인 routerConfig에 할당됩니다.

그렇다면 goRouterProvider내부는 어떻게 설계되어 있을까요?

final goRouterProvider = Provider<GoRouter>((ref) {
  final authRepository = ref.watch(authRepositoryProvider);
  final onboardingRepository = ref.watch(onboardingRepositoryProvider);
  return GoRouter(
    initialLocation: '/signIn',
    navigatorKey: _rootNavigatorKey,
    debugLogDiagnostics: true,
    redirect: (context, state) {
      final didCompleteOnboarding = onboardingRepository.isOnboardingComplete();
      if (!didCompleteOnboarding) {
        // Always check state.subloc before returning a non-null route
        // https://github.com/flutter/packages/blob/main/packages/go_router/example/lib/redirection.dart#L78
        if (state.subloc != '/onboarding') {
          return '/onboarding';
        }
      }
      final isLoggedIn = authRepository.currentUser != null;
      if (isLoggedIn) {
        if (state.subloc.startsWith('/signIn')) {
          return '/jobs';
        }
      } else {
        if (state.subloc.startsWith('/jobs') ||
            state.subloc.startsWith('/entries') ||
            state.subloc.startsWith('/account')) {
          return '/signIn';
        }
      }

위의 코드과 같이, goRouteerProvider는 authRepositoryProvider와, onboardingRepositoryProvider 를 바라보고 있습니다.

authRepositoryProvider와, onboardingRepositoryProvider의 구조는 아래와 같습니다.

final authRepositoryProvider = Provider<AuthRepository>((ref) {
  return AuthRepository(ref.watch(firebaseAuthProvider));
});

class OnboardingRepository {
  OnboardingRepository(this.sharedPreferences);
  final SharedPreferences sharedPreferences;

  static const onboardingCompleteKey = 'onboardingComplete';

  Future<void> setOnboardingComplete() async {
    await sharedPreferences.setBool(onboardingCompleteKey, true);
  }

  bool isOnboardingComplete() =>
      sharedPreferences.getBool(onboardingCompleteKey) ?? false;
}

final onboardingRepositoryProvider =
    Provider<OnboardingRepository>((ref) => throw UnimplementedError());

위의 코드에서 onboardingRepositoryProvider에 이상한 점이 하나 있습니다. onboardingRepositoryProvider를 바로 가져다 쓰게 되면 UnimplementedError를 뿜게 되는데요, 이 코드는 왜 있고, 어떻게 에러를 뿜지 않도록 사용할 수 있을까요?
그 비밀은 바로 main 함수에 숨겨져 있습니다.

...
  final container = ProviderContainer(
    overrides: [
      onboardingRepositoryProvider.overrideWithValue(
        OnboardingRepository(sharedPreferences),
      ),
    ],
  );
  // await until auth state is determined
  // this will prevent unnecessary redirects inside GoRouter when the app starts
  await container.read(authStateChangesProvider.future);
  runApp(UncontrolledProviderScope(
    container: container,
    child: const MyApp(),
  ));

main 함수를 보면, ProviderContainer를 따로 만든 뒤, overrideWithValue라는 함수를 사용하여 오버라이딩 하고 있습니다. 이는 OnboardingRepository의 프로바이더가 불렸을 경우, 필요한 오버라이딩이 되었다면 에러를 뿜지 않도록 합니다.

그리고, container.read로 authStateChangesProvider를 읽어와, 현재 authState를 기다려서 가져온 뒤에 GoRouter내부에 있는 redirect 로직을 수행하도록 합니다.

OnboardingScreen

OnboardingScreen은 앱을 처음 시작했을 때 뜨는 페이지입니다. 앱의 사용설명서와 같은 화면입니다. 해당 앱을 처음 사용한 유저에게는 띄워주겠지만, 사용경험이 있는 유저에게는 더이상 해당 화면을 띄우지 않아야 하기에, OnboardingRepository는 SharedPreference에 해당 화면이 실행 되었는지 저장하는 로직을 포함하고 있습니다.

class OnboardingRepository {
  OnboardingRepository(this.sharedPreferences);
  final SharedPreferences sharedPreferences;

  static const onboardingCompleteKey = 'onboardingComplete';

  Future<void> setOnboardingComplete() async {
    await sharedPreferences.setBool(onboardingCompleteKey, true);
  }

  bool isOnboardingComplete() =>
      sharedPreferences.getBool(onboardingCompleteKey) ?? false;
}

Sign In Page

OnboardingScreen을 나온 유저는 SignInScreen을 보게 됩니다. 이때, 만약 익명로그인(Go anonymous) 버튼을 클릭하면 어떤 일이 일어나며, 그 다음 화면은 어떻게 동작하게 될까요?

Go anonymous

익명 로그인 버튼을 클릭하면, signInScreenControllerProvider 에 있는, signInAnonymously함수가 실행되며, 이후에는 authState가 변경되게 됩니다.

                PrimaryButton(
                  key: anonymousButtonKey,
                  text: Strings.goAnonymous,
                  onPressed: state.isLoading
                      ? null
                      : () => ref
                          .read(signInScreenControllerProvider.notifier)
                          .signInAnonymously(),
                ),

하지만, 버튼을 누르면 바로 홈 화면으로 넘어가게 되는데 이 로직이 어디에서 실행되는지 찾을 수 없었습니다. 해당 로직은 goRouterProvider에서 찾을 수 있었습니다. GoRouter에서 refreshListenable이라는 함수내부에, Stream을 반환하는 함수를 달아놓게 되면, 해당 Stream의 값이 올 때 마다 redirect 로직이 수행되며, 알맞은 페이지로 redirect되게 됩니다.

    refreshListenable: GoRouterRefreshStream(authRepository.authStateChanges()),
profile
Flutter App Developer

0개의 댓글