DEV Community

Cover image for Flutter App, Shared preferences repository
Saad Alkentar
Saad Alkentar

Posted on • Edited on

Flutter App, Shared preferences repository

What to expect from this article?

We initiated our Flutter project with the main libraries, and started working on the login screen in the previous articles.

This article will cover the implementation of shared preferences repository, same steps can be followed to build any other repository, edit profile screen and other.

Before going on, I want to point out this amazing article series by AbdulMuaz Aqeel, his articles detail the project structure, clean architecture concepts, VS code extensions, and many other great details. feel free to use it as a more detailed reference for this article 🤓.

I'll try to cover as many details as possible without boring you, but I still expect you to be familiar with some aspects of Dart and Flutter.

the final version of the source code can be found at https://github.com/saad4software/alive-diary-app

Shared preferences repository implementation

So, every feature in this app starts with the Domain layer, then we implement the domain interfaces in the Data layer and finally connect the presentation layer with the domain repositories.

Instead of storing preferences as simple key-value pairs, I recommend storing them as a JSON object. This allows a structure to the preferences, simplifying storing, recovering, and updating preferences value, yet some would argue its time efficiency 🤔. What is your opinion on that?

Domain Layer

Let's start by creating a model for preferences. We need to keep both access and refresh tokens, user model, and app locale.

import 'user_model.dart';
class SettingsModel {
  SettingsModel({
      this.token, 
      this.refresh, 
      this.user, 
      this.locale,});

  SettingsModel.fromJson(dynamic json) {
    token = json['token'];
    refresh = json['refresh'];
    user = json['user'] != null ? UserModel.fromJson(json['user']) : null;
    locale = json['locale'];
  }
  String? token;
  String? refresh;
  UserModel? user;
  String? locale;
SettingsModel copyWith({  String? token,
  String? refresh,
  UserModel? user,
  String? locale,
}) => SettingsModel(  token: token ?? this.token,
  refresh: refresh ?? this.refresh,
  user: user ?? this.user,
  locale: locale ?? this.locale,
);
  Map<String, dynamic> toJson() {
    final map = <String, dynamic>{};
    map['token'] = token;
    map['refresh'] = refresh;
    if (user != null) {
      map['user'] = user?.toJson();
    }
    map['locale'] = locale;
    return map;
  }
}
Enter fullscreen mode Exit fullscreen mode

lib/domain/models/entities/settings_model.dart

great, now for the preferences repository interface at the domain layer


import '../models/entities/login_model.dart';

abstract class PreferencesRepository {

  void saveLoginModel(LoginModel? loginModel);
  bool invalidToken();
  void logout();
  String getToken();
  String getLocale();
  void setLocale(String locale);

}
Enter fullscreen mode Exit fullscreen mode

lib/domain/repositories/preferences_repository.dart

With our preferences repository interface ready, it is time to work on the data layer

Data Layer

let's implement the repository in the data layer

import 'dart:convert';

import 'package:shared_preferences/shared_preferences.dart';
import 'package:jwt_decoder/jwt_decoder.dart';

import '../../config/app_constants.dart';
import '../../domain/models/entities/login_model.dart';
import '../../domain/models/entities/settings_model.dart';
import '../../domain/repositories/preferences_repository.dart';

class PreferencesRepositoryImpl extends PreferencesRepository {

  final SharedPreferences preferences;
  PreferencesRepositoryImpl(this.preferences);

  SettingsModel getSettingsModel() {
    final str = preferences.getString(AppConstants.keySettings) ?? "{}";
    final model = SettingsModel.fromJson(jsonDecode(str));
    return model;
  }

  Future<bool> updateSettingModel(SettingsModel settings) async {
    return preferences.setString(
      AppConstants.keySettings,
      jsonEncode(settings),
    );
  }

  @override
  String getLocale() {
    final model = getSettingsModel();
    return model.locale ?? "en";
  }

  @override
  String getToken() {
    final model = getSettingsModel();
    return model.token ?? "";
  }

  @override
  void logout() {
    final settings = getSettingsModel();
    settings.refresh = "";
    settings.token = "";
    updateSettingModel(settings);
  }

  @override
  Future<void> saveLoginModel(LoginModel? loginModel) async {
    final settings = getSettingsModel();
    settings.token = loginModel?.access;
    settings.refresh = loginModel?.refresh;
    settings.user = loginModel?.user;
    await updateSettingModel(settings);
  }

  @override
  void setLocale(String locale) async {
    final settings = getSettingsModel();
    settings.locale = locale;
    await updateSettingModel(settings);
  }

  @override
  bool invalidToken() {
    final token = getToken();
    return token.isEmpty || JwtDecoder.isExpired(token);
  }
}
Enter fullscreen mode Exit fullscreen mode

lib/data/repositories/preferences_repository_impl.dart

Dependency Injection

Great, now with the repository implementation ready, it is time to inject it where needed, moving to the dependency injection config file

...

Future<void> initializeDependencies() async {
  final dio = Dio();

  locator.registerSingleton<Dio>(dio);

  locator.registerSingleton<SharedPreferences> (
    await SharedPreferences.getInstance(),
  );

  locator.registerSingleton<RemoteDatasource>(
    RemoteDatasource(locator()),
  );

  locator.registerSingleton<RemoteRepository>(
    RemoteRepositoryImpl(locator()),
  );

// new
  locator.registerSingleton<PreferencesRepository>( 
    PreferencesRepositoryImpl(locator()),
  );
}
Enter fullscreen mode Exit fullscreen mode

lib/config/dependencies.dart

Nice! We can get the preferences repository from the dependency injection provider wherever it is needed 🤓 basically in the presentation layer.

App router config

With the preferences repository ready, we need to find a way to select initial app screen based on the token status, if the token is invalid or expired, we will use login screen, if we have a valid token, we want to start with the home screen

...
@AutoRouterConfig()
class AppRouter extends _$AppRouter {

  bool invalidToken() => locator<PreferencesRepository>().invalidToken();

  @override
  List<AutoRoute> get routes => [
    AutoRoute(page: LoginRoute.page, initial: invalidToken()),
    AutoRoute(page: HomeRoute.page, initial: !invalidToken()),
  ];

}

final appRouter = AppRouter();
Enter fullscreen mode Exit fullscreen mode

lib/config/router/app_router.dart

we are getting the PreferencesRepository from our locator (dependency injection) and using invalidToken to check token status.

Presentation layer

We have already designed the login screen, the main modification is to save token data in preferences. in login_bloc

...
class LoginBloc extends Bloc<LoginEvent, LoginState> {

  final RemoteRepository repository;
  final PreferencesRepository preferencesRepository; // new

  LoginBloc(this.repository, this.preferencesRepository) : super(LoginInitial()) { // new
    on<LoginPressedEvent>(handleLoginEvent);
  }

  FutureOr<void> handleLoginEvent(
      LoginPressedEvent event,
      Emitter<LoginState> emit,
  ) async {
    emit(LoginLoadingState());

    final response = await repository.login(
      username: event.username,
      password: event.password,
    );

    if (response is DataSuccess) {
      final loginModel = response.data?.data;
      preferencesRepository.saveLoginModel(loginModel); // new

      emit(LoginSuccessState());
    } else if (response is DataFailed) {

      emit(LoginErrorState(message: response.error?.getErrorMessage()));
    }

  }
}
Enter fullscreen mode Exit fullscreen mode

lib/presentation/screens/login/login_bloc.dart

after adding the preferences repository to the login bloc, we used it to update the tokens using saveLoginModel.
we still need to update the main app file to provide the new repository to loginBloc

...
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
        useMaterial3: true,
      ),
      home: MultiBlocProvider(
        providers: [
          BlocProvider(create: (context)=>HomeBloc(locator())),
          BlocProvider(create: (context)=>LoginBloc(locator(), locator())),
        ],
...
Enter fullscreen mode Exit fullscreen mode

lib/main.dart

That is it! after logging in and restarting the app, it should start with the home screen, after cleaning the app caches (logging out) the app should start at the login screen.

Let's also add a logout button to make sure it works as expected, in the home screen

...
            ElevatedButton(
              child: const Text("logout"),
              onPressed: () async {
                locator<PreferencesRepository>().logout();
                appRouter.replaceAll([const LoginRoute()]);
              },
            ),
...
Enter fullscreen mode Exit fullscreen mode

lib/presentation/screen/home/home_screen.dart

the current home screen design have a column with two buttons, ar and en buttons, simply add another button for logout. pressing the button will call the logout function from our preferences repository, then clear the app routing tree to show the logout page.

That is it! we will work on text to speech and speech to text next so

Stay tuned 😎

Heroku

This site is built on Heroku

Join the ranks of developers at Salesforce, Airbase, DEV, and more who deploy their mission critical applications on Heroku. Sign up today and launch your first app!

Get Started

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Explore a sea of insights with this enlightening post, highly esteemed within the nurturing DEV Community. Coders of all stripes are invited to participate and contribute to our shared knowledge.

Expressing gratitude with a simple "thank you" can make a big impact. Leave your thanks in the comments!

On DEV, exchanging ideas smooths our way and strengthens our community bonds. Found this useful? A quick note of thanks to the author can mean a lot.

Okay