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;
}
}
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);
}
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);
}
}
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()),
);
}
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();
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()));
}
}
}
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())),
],
...
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()]);
},
),
...
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)