Photo by fabio on Unsplash

Flashlist PART 1: Serverpod + Riverpod = ServerRiverPodPod

Leverage the power of Serverpod and Riverpod

Ben Auer
6 min readMar 12, 2024

--

Intro

In this post, I’ll share how I integrate Serverpod into my frontend, leveraging Riverpods functionality for a reactive layout and explore caching on both sides.

Also before we get into this whole thing, the irony is by far not lost on me, that two of the core technologies I am basing my app on both end in “pod”. Rather than repeatedly mentioning ‘Serverpod and Riverpod’, I’ve decided to dub this combined integration ‘ServerRiverPodPod' as it is way more satisfying to say and write.

If you’re not a fan of ‘ServerRiverPodPod’ well, I’m sorry, but you should have come up with a “better” name before me then. (Unless you happen to be Remy or Viktor, in which case, I’m all ears if you have any suggestions).

Prerequisites

I won’t be covering the setup of Serverpod authentication as it is well-documented on their docs or here. Ensure you have the setup for ServerpodAuth and an Authentication-Provider (Email, Google, Apple, Firebase). I am using SignInWithEmailButton as it provides (almost) everything I need for now.

All set? Awesome, let’s continue :)

In the Serverpod documentation, you probably already saw an option on how to declare the Client and initialize the SessionManager in a StatefulWidget. However, I want something a little different as I am going to use Riverpod. I aim to encapsulate both Client and SessionManager into a Provider for Riverpod to cache.

Caching

Additionally, it’s worth noting that Serverpod features its own caching mechanism, complementing Riverpods caching capabilities. Below, I’ve outlined some points showcasing how powerful the product of this “fusion” is:

To summarise, with ServerRiverPodPod we leverage two caching systems to optimise performance, effectively reducing the strain on the internet connection, which serves as the bridge between them. All we have to do is handle the life cycle of our Client and SessionManager instance within a Provider and follow the riverpod-pattern.

Let’s provide ourselves with a Client instance

All we need to do for that, is declaring an instance of the Client inside a provider, which looks in my case like:

// ...flutter/lib/src/utils/serverpod/serverpod_helper.dart

@riverpod
Client client(ClientRef ref) => Client(
"10.0.2.2:8080/",
authenticationKeyManager: FlutterAuthenticationKeyManager(),
)..connectivityMonitor = FlutterConnectivityMonitor();

If you’re not familiar with or don’t use riverpod_annotation and build_runner you would just do something like this:

final clientProvider = Provider<Client>(
(ref) => Client(
"10.0.2.2:8080/",
authenticationKeyManager: FlutterAuthenticationKeyManager(),
)..connectivityMonitor = FlutterConnectivityMonitor(),
);

SessionManager

With our Client now ready, it’s time to tend to the SessionManager, which we can declare like so:

// ...flutter/lib/src/utils/serverpod/serverpod_helper.dart

@riverpod
SessionManager sessionManager(SessionManagerRef ref) =>
SessionManager(caller: ref.watch(clientProvider).modules.auth,
);

Just like this we can declare our SessionManager and connect it to the auth module associated our clientProvider.

ServerpodHelper

It is important to note, that the SessionManager needs to be initialised which I do in a ServerpodHelper class and set up a provider to manage the life cycle and make the properties accessible like so:

// ...flutter/lib/src/utils/serverpod/serverpod_helper.dart

class ServerpodHelper {
ServerpodHelper(this.client, this.sessionManager) {
sessionManager.initialize();
}


final Client client;
final SessionManager sessionManager;
}


@riverpod
ServerpodHelper serverpodHelper(ServerpodHelperRef ref) =>
ServerpodHelper(
ref.watch(clientProvider),
ref.watch(sessionManagerProvider),
);

This way we declare ServerpodHelper, which expects Client and SessionManager as parameters. And initialize the SessionManager within the constructor of the ServerpodHelper.

Note: While it’s possible to achieve a similar result by passing the Ref to the ServerpodHelper and declare Client and SessionManager as instance variables with their own getters, I chose the current approach for mainly two reasons:

  1. Modularity — It’s possible to access the properties individually, thereby separating concerns and promoting a modular design. This enhances code maintainability and readability.
  2. Testing — In scenarios where we need to set up tests that require mocking the behaviour of either the Client or SessionManager, this approach allows for comfortable overrides.

Now that the initial setup is done, it’s time to take a deep breath (maybe also grab a coffee) before we reflect on what we’ve accomplished. We can now effortlessly call upon the Client and SessionManager individually or combined within our providers, be it a Consumer, ConsumerWidget, or ConsumerStatefulWidget.

isAuthenticatedProvider

But before we dive into building Widgets and Screens, let’s leverage our SessionManager to construct a StreamProvider. This StreamProvider will gracefully handle the authentication state, seamlessly transitioning between the auth-screen and home-screen based on the user’s authentication status.

@riverpod
Stream<bool> isAuthenticated(IsAuthenticatedRef ref) async* {
// access the SessionManager
final SessionManager sessionManager = ref.watch(sessionManagerProvider);

// Add a listener to the SessionManager that invalidates the IsAuthenticatedRef
// whenever the session state changes, ensuring that the isAuthenticated stream
// reflects the latest authentication status.
sessionManager.addListener(() {
ref.invalidateSelf();
});

// Yield a boolean value reflecting the authentication state.
// If the signed-in user is not null, the user is authenticated.
// Otherwise, the user is not authenticated.
yield sessionManager.signedInUser != null;
}

If you haven’t already it’s now time to navigate to your flutter directory in your terminal and run:

dart run build_runner build

Handling Authentication State with isAuthenticatedProvider

Now that we have all our providers set up, it’s time to build something tangible. With the isAuthenticatedProvider returning true or false, we can seamlessly integrate it into our widget tree. This can be achieved within the main or home widget, provided that the widget extends ConsumerWidget or ConsumerStatefulWidget.

In my case, I opt for using go_router. Coming from a background in JavaScript frameworks, I appreciate having a centralised SOT when it comes to possible routes and how they relate to each other.

So within my app_router.dart file I:

// ...flutter/lib/src/features/routing/app_router.dart

...
GoRoute(
path: '/',
name: AppRoute.home.name,
builder: (context, state) {
return Consumer(
builder: (context, ref, child) {
// Uses the ref provided by Consumer to get isAuthenticatedProvider
final isAuthenticated = ref.watch(isAuthenticatedProvider);


return isAuthenticated.when(
data: (user) {
if (!user) {
return const AuthScreen();
} else {
return const HomeScreen();
}
},
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stackTrace) => Center(
child: Text('Error: $error'),
),
);
},
);
...

Note: In this implementation, the isAuthenticatedProvider resides higher up in the WidgetTree. As a reminder, the SessionManager needs to be initialized to function properly. Therefore, it’s essential to ensure that the SessionManager is initialized by calling ServerpodHelperProvider either in the AuthScreen or HomeScreen to enable the authentication process. However, once initialized, we can seamlessly use the Client and SessionManager individually throughout the application without any concerns.

AuthScreen

Our current AuthScreen, is straightforward yet effective. At present, it only contains the SignInWithEmailButton. This button initiates a modal that handles user authentication.

// ..._flutter/lib/src/features/authentication/presentation/auth_screen.dart

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


@override
Widget build(BuildContext context, WidgetRef ref) {
// calling the client via the serverpodHelperProvider here
// ensures that the sessionManager will be instantiated at the right time
final client = ref.watch(serverpodHelperProvider).client;


return Scaffold(
body: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SignInWithEmailButton(
caller: client.modules.auth,
)
],
)),
);
}
}

HomeScreen

Our HomeScreen is as simple, it only features a Text saying welcome! And also holding a sign-out button to let us try out the functionality that we just built.

// ..._flutter/lib/src/features/home/presentation/home_screen.dart

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


@override
Widget build(BuildContext context, WidgetRef ref) {
final sessionManager = ref.read(sessionManagerProvider);


return Scaffold(
appBar: AppBar(
title: const Center(
child: Text('Home'),
),
),
body: Column(
children: [
const Center(
child: Text('Welcome!'),
),
ElevatedButton(
child: const Text('Sign Out'),
onPressed: () {
sessionManager.signOut();
},
),
],
),
);
}
}

Recap and Props

Nice Job making it this far, let’s sum up what we achieved here.

  1. Created Client and ClientProvider.
  2. Established SessionManager and SessionManagerProvider.
  3. Implemented ServerpodHelper and ServerpodHelperProvider to manage the life cycle of our Client and SessionManager.
  4. Built ourselves an isAuthenticatedProvider, which utilizes SessionManager to listen for changes in authentication state and trigger rerenders when necessary.
  5. Set up Screens (HomeScreen and AuthScreen) to facilitate user authentication and navigation.

Conclusion

With these foundational elements in place, you’re now all set to explore ServerRiverPodPod. Once an Endpoint has been established, it can be called using the following pattern:

ref.read(clientProvider).endpoint-name.method()

or

ref.watch(clientProvider).endpoint-name.method()

So when my Endpoint is:

class FlashlistEndpoint extends Endpoint {
...

Future<List<Flashlist>> getFlashlistsForUser(Session session) {
// code that retrieves the Flashlists
// asociated with the current User
}

...
}

I would call it like:

final List<Flashlist> =  ref.watch(clientProvider).flashlist.getFlashlistsForUser();

It’s important to recognize that the UserInfo object provided by the serverpod_auth package contains sensitive information and should not be shared with other users.

In the next post, I’ll introduce a separate Database entity called AppUser. Subscribe if you want to tag along on the journey ahead, if you couldn’t be bothered, that’s also fine :)

Thanks for reading my first technical blog post and “May the pods have mercy on your code”.

Next: Flashlist PART 2: Let there be users

--

--