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

Noy Sengxayya
LaoITDev
Published in
3 min readApr 28, 2023
credits: https://riverpod.dev/

* ໃນ 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 ໃນພາຍຫຼັງ.

--

--