Flutter riverpod + Go Router [part 1] — ການໃຊ້ riverpod ສຳລັບເຮັດ go router notifier


* ໃນ part 1 ນີ້ຈະເປັນການ setup ໃນແບບຂອງຜູ້ຂຽນເອງ

  • ໃຊ້ pocketbase ເປັນ backend api (firebase alternative ຈະຂຽນ blog ເພີ່ມຕ່າງຫາກກ່ຽວກັບການໃຊ້ງານກັບ pocketbase)
  • hive ສຳລັບ persist data (login credentials, theme, language, etc)
  • go router
  • get_it ສຳລັບ dependency injection

*project setup ໂດຍອີງຕາກ TDD architecture

structure ຄ່າວໆ ສຳລັບ project ທົດລອງ

main_dev.dart

void main() async {
  runZonedGuarded(
    () async {
      WidgetsFlutterBinding.ensureInitialized();

      // init hive
      final appDir = await getApplicationDocumentsDirectory();
      await Hive.initFlutter(appDir.path);
      await Hive.openBox('prefs-${Env.envName}');

      // detect platform type
      final platformType = detectPlatformType();

      // init dependencies injection
      dpInit();

      runApp(
        ProviderScope(
          overrides: [platformTypeProvider.overrideWithValue(platformType)],
          observers: [StateLogger()],
          child: App(
            key: Key('app-${Env.envName}'),
          ),
        ),
      );
    },
    (error, stack) {
      ...
    },
  );
}

app.dart

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

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final router = ref.watch(routerProvider);
    // final currentLocale =
    // final currentThemeMode =
    return MaterialApp.router(
      routerConfig: router,
      builder: (context, child) {
        child = ResponsiveBreakpoints.builder(
          child: BouncingScrollWrapper.builder(context, child!),
          breakpoints: [
            const Breakpoint(start: 0, end: 450, name: MOBILE),
            const Breakpoint(start: 451, end: 800, name: TABLET),
            const Breakpoint(start: 801, end: 1920, name: DESKTOP),
            const Breakpoint(start: 1921, end: double.infinity, name: '4K'),
          ],
        );

        return child;
      },
    );
  }
}

ເຮົາຈະໃຊ້ເປັນ MaterialApp.router ແທນ Material ທຳມະດາ. ໂດຍຈະມີ routerConfig ແມ່ນ ມາຈາກ routerProvider (riverpod ref).

ສຳລັບ routerProvider ແມ່ນຜູ້ຂຽນວາງໄວ້ໃນ lib/app/core/routes

ໂດຍຈະມີຢູ່ 2 ໄຟລ໌ຫຼັກໆຄື: router_notifier.dart ແລະ router_provider.dart

router_provider.dart

final key = GlobalKey<NavigatorState>(debugLabel: '${Env.envName}-router-key');

final routerProvider = Provider.autoDispose<GoRouter>((ref) {
  // router notifier
  final notifier = ref.watch(routerNotifier.notifier);

  return GoRouter(
    navigatorKey: key,
    refreshListenable: notifier,
    debugLogDiagnostics: kDebugMode,
    initialLocation: SplashScreen.path,
    routes: notifier.routes,
    redirect: notifier.redirect,
    errorBuilder: (context, state) => const ErrorRouterWidget(),
  );
});

router_notifier.dart

class RouterNotifier extends AutoDisposeAsyncNotifier<void>
    implements Listenable {
  VoidCallback? routerListener;
  bool isAuth = false;

  @override
  FutureOr<void> build() async {
    /// mock set default initial auth state to false
    /// UNAUTHENTICATED
    isAuth = false;

    ref.listenSelf((_, __) {
      // One could write more conditional logic for when to call redirection
      if (state.isLoading) return;
      routerListener?.call();
    });
  }

  /// Redirects the user when our authentication changes
  String? redirect(BuildContext context, GoRouterState state) {
    /// redirect none if state == null
    if (this.state.isLoading || this.state.hasError) return null;

    // login location
    final loginLocation = state.location == LoginScreen.path;

    // splash location
    final splashLocation = state.location == SplashScreen.path;

    // redirect from splash location
    if (splashLocation) {
      return isAuth ? HomeScreen.path : LoginScreen.path;
    }

    // redirect from login location
    if (loginLocation) {
      return isAuth ? HomeScreen.path : LoginScreen.path;
    }

    return null;
  }

  /// all available app routes
  ///
  /// `<GoRoute>[]`
  List<GoRoute> get routes => [
        GoRoute(
          path: SplashScreen.path,
          builder: (context, state) => const SplashScreen(),
        ),
        GoRoute(
          path: LoginScreen.path,
          builder: (context, state) => const LoginScreen(),
        ),
        GoRoute(
          path: RegisterScreen.path,
          builder: (context, state) => const RegisterScreen(),
        ),
        GoRoute(
          path: HomeScreen.path,
          builder: (context, state) => const HomeScreen(),
        ),
      ];

  /// Adds [GoRouter]'s listener as specified by its [Listenable]
  /// [GoRouteInformationProvider] uses this method on creation to handle its
  /// internal [ChangeNotifier].
  /// Check out the internal implementation of [GoRouter] and
  /// [GoRouteInformationProvider] to see this in action.
  @override
  void addListener(VoidCallback listener) {
    routerListener = listener;
  }

  /// Removes [GoRouter]'s listener as specified by its [Listenable].
  /// [GoRouteInformationProvider] uses this method when disposing,
  /// so that it removes its callback when destroyed.
  /// Check out the internal implementation of [GoRouter] and
  /// [GoRouteInformationProvider] to see this in action.
  @override
  void removeListener(VoidCallback listener) {
    routerListener = null;
  }
}

final routerNotifier = AutoDisposeAsyncNotifierProvider<RouterNotifier, void>(
  () => RouterNotifier(),
);

ຫຼັກໆໃນ router_notifier.dart ນີ້ແມ່ນຈະໄວ້ listen ກໍລະນີມີ router redirect ເຊັ່ນ: ເວລາ user logged in, user token expired, user logged out, ແລະ ອື່ນໆ. ແລະ ຜູ້ຂຽນເອງກໍໄດ້ປະກາດ List<GoRoute> get routes ໄວ້ນຳ ຫຼື ກໍຄື routes ທັງໝົດໃນ app ເຮົາໄວ້ທີ່ນີ້.

ຫຼັງຈາກ run

ຜົນຫຼັງຈາກ run ກໍຈະສະແດງ GoRouter ຕາມໃນຮູບດັ່ງນີ້:

debug console

ເຊິ່ງຈະເຫັນວ່າມີທັງໝົດຢູ່ 4 routes ຕາມໃນ router_notifier.dart

init location ແມ່ນ /splash ຕາມໃນ router_provider.dart

initialLocation: SplashScreen.path,

redirecting ແມ່ນອີງຕາມ redirect ໃນ router_notifier.dart ທີ່ build() ໃນ redirect ຈະ return route path ອີງຕາມທີ່ເຮົາໄດ້ check condition ໄວ້.

part 1 — ການໃຊ້ riverpod ສຳລັບເຮັດ go router notifier

ຂໍຈົບໄວ້ພຽງເທົ່ານີ້. ສຳລັບ part ຕໍ່ໄປແມ່ນເຮົາຈະເລີ່ມເຮັດ authentication ແລະ navigation ພ້ອມກັບການ pass argument.

ໝາຍເຫດ: ສຳລັບ project source code ຜູ້ຂຽນຈະ open public ໄວ້ທາງ github ໃນພາຍຫຼັງ.

Mobile Development

ຂຽນໂດຍ: Noy Sengxayya