DEV Community

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

Posted 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 😎

Top comments (0)