Welcome to the exciting world of Flutter Web! In this codelab we'll learn how to tap into your browser's local storage to store non-critical data - in our case, we'll persist the last navigated page so we can restore it when the user reloads the page.

In this codelab, we'll accomplish the following:

We'll use the shared_preferences package for this.

The SharedPreferences package wraps platform-specific persistent storage for simple data. See in the table below what the SharedPreferences implementation defaults to for each individual platform underneath the hood:

Platform

Location

Android

SharedPreferences

iOS

NSUserDefaults

Linux

In the XDG_DATA_HOME directory

macOS

NSUserDefaults

Web

LocalStorage

Windows

In the roaming AppData directory

Let's proceed!

Let's go ahead and install the required packages.

In the command line at the root of the project, run this command:

flutter pub add shared_preferences

This will install shared_preferences and add the required entries to the pubspec.yaml.

In order to consume the SharedPreferences around a strategy such as Riverpod, we'll create a few providers that wrap its implementation in order to support dependency injection, testability and mocking.

Create a SharedPreferences Instance provider

In the lib/shared/providers/shared_providers.dart file, add the following provider (with the required imports):

//... rest of the code omitted

final sharedPrefsInstanceProvider = Provider((ref) {
  return SharedPreferences.getInstance();
});

The call to SharedPreferences.getInstance() returns a Future of SharedPreferences, but since we won't be consuming this from the UI, we can settle for a simple Riverpod Provider, that way we can read it from inside another provider or repository.

Now, let's go to the lib/services folder and add a new file called localstorage.service.dart, and add the following code (plus imports):

class WebLocalStorage {
  
  final Ref ref;
  WebLocalStorage(this.ref);

  static const String menuSelection = 'menuSelection';
  late SharedPreferences prefs;

  Future<bool> initLocalStorage() {

    Completer<bool> localStorageCompleter = Completer();

    ref.read(sharedPrefsInstanceProvider).then((sp) {
      prefs = sp;
      localStorageCompleter.complete(true);
    });

    return localStorageCompleter.future;
  }

  void storeSelectedNav(String route) {
    prefs.setString(WebLocalStorage.menuSelection, route);
  }

  String getSelectedNav() {
    return prefs.getString(WebLocalStorage.menuSelection) ?? '';
  }
}

Let's dissect this code.

We created a class called WebLocalStorage, which receives a reference of type Ref via its constructor.

We created a static variable called menuSelection with a value of the same name, since that will be the key that we'll be maintaining inside local storage, and instead of hardcoding it everywhere we created this property so we can refer to it later. This is the key that will hold the value in local storage corresponding to the selected page route in the navigation. We'll get to that in a minute.

A method called initLocalStorage() that returns a Future<bool> to denote whether the preferences were loaded correctly, and holds a reference of the SharedPreferences in a property called prefs.

A getter (getSelectedNav()) fetchs the associated value from local storageof the menuSelection key; if null, it will return an empty string, otherwise it brings the stored value, and a setter (storeSelectedNav) which takes corresponding route selected by the user and stores it in local storage.

Back in the shared_providers file, add a provider wrapping our newly created WebLocalStorage service, as such:

//... rest of the code omitted

final webLocalStorageProvider = Provider((ref) {
  return WebLocalStorage(ref);
});

Now, we need to determine who will call the initLocalStorage() method. I'd say let the navigation feature handle this as well, as is the one at the moment who depends on the preferences being initialized.

Let's do some refactoring, shall we?

Refactor the LeftNavigationViewModel

In the LeftNavigationViewModel class (file features/navigation/presentation/viewmodels/leftnavigation.viewmodel.dart), first, remove the logic that always selects the first item in the list of navigation items from the constructor (we'll add it someplace else):

//... remove this code from the constructor:

var item = state.first;
selectNavItem(item);


Your updated constructor should look as such:

LeftNavigationViewModel(List<LeftNavigationItem> items, this.ref)
    : super([]) {
    state = items;
}

Let's proceed and add a method called init() with the following content:

//... in the leftnavigation.viewmodel.dart

void init() {
    var selectedNavRoute = ref.read(webLocalStorageProvider).getSelectedNav();

    if (selectedNavRoute.isNotEmpty) {
        var item = state.where((i) => i.route == selectedNavRoute).first;
        selectNavItem(item);
    }
    else {
      selectNavItem(state.first);
    }
}


The init() method will be called upon the shared preferences being loaded (we'll call it in a minute - let's assume it is loaded and this method gets called).

In this method, we read the webLocalStorageProvider and invoking its getSelectedNav() method, which returns the currently selected navigation route (if any), and hold it in a local variable called selectedNavRoute.

If the value is not empty, we proceed to auto-select the corresponding navigation item by leveraging the selectNavItem method, passing the LeftNavigationItem instance that matches the stored route, otherwise we'll select the first one by default - when it's the first time loading the app, or if the user manually deletes the local storage entry.

Proceed now to the selectNavItem() method, which is where we will trigger the storing of the corresponding page route upon the user selecting a navigation item.

Anywhere before resetting the state, add the following line:

ref.read(webLocalStorageProvider).storeSelectedNav(item.route);

This is a straightforward call - we store the corresponding route from the selected item navigation item by reading the webLocalStorageProvider provider, calling on the storeSelectedNav() method, passing the currently selected route - simple as that.

Ok, let's get back to who's gonna call the initialization method.

Let's go to the features/navigation/presentation/widgets/left_navigation.dart widget, and convert it to a ConsumerStatefulWidget, since we want to leverage the initState() lifecycle method - that's where we'll initialize the shared preferences.

Do the refactoring and make sure your updated left_navigation.dart file looks like this after the refactoring:

class LeftNavigation extends ConsumerStatefulWidget {
  const LeftNavigation({super.key});

  @override
  ConsumerState<LeftNavigation> createState() => _LeftNavigationState();
}

class _LeftNavigationState extends ConsumerState<LeftNavigation> {

  @override
  Widget build(BuildContext context) {
    //... rest of the code here
  } 
}

In the _LeftNavigationState state class, override the initState() lifecycle method and add the following code:

//... add the initState()

@override 
void initState() {
    super.initState();

    ref.read(webLocalStorageProvider).initLocalStorage().then((value) {
        ref.read(navigationItemsViewModelProvider.notifier).init();
    });
}

Let me explain what we did. In the initState() of the navigation widget, we call the initLocalStorage() on the WebLocalStorage service by reading its provider (webLocalStorageProvider), followed by the init() method of the LeftNavigationViewModel, which allows us to load the shared preferences upfront, then after ensuring a successful load, we can start pulling data from it (in the init() method).

Let's take this for a spin for a minute.

Load the Flutter web app and navigate to any page; do it a couple of times to other pages.

Assuming you're using Chrome as your previewing device, open the Chrome Developer tools, navigate to the Aplication tab; on the left hand side, find Storage > LocalStorage. Click on the localhost entry of your currently running web app.

LocalStorage

Notice then on the right hand side an entry for the currently selected route (a key named flutter.menuSelection with a value named /welcome). Click a few times and notice this value change accordingly.

LocalStorage

Reload the Flutter web app on any page other than the welcome page, just to prove the app will restore the last selected route.

LocalStorage

And voilá! That's shared preferences for you! We are able to load from local storage the persisted route, and just as it is expected by our users, we bring them back to the last page they visited.

Go ahead - test by deleting the local storage entry - it should behave as expected: redirect you to the welcome page. Neat!

LocalStorage

And with that, we're done with this codelab for this workshop, where we accomplished the following:

Please don't forget to follow me on social media:

The complete code is available on Github by following this link.

Happy Coding!!!