Skip to content

Commit

Permalink
Add "Offline-first" architecture cookbook recipe (#11425)
Browse files Browse the repository at this point in the history
_Description of what this PR is changing or adding, and why:_

Following PRs #11410 and #11394 this PR adds the "Offline-first"
cookbook recipe for the Architecture design patterns.

_Issues fixed by this PR (if any):_

Part of #11374

_PRs or commits this PR depends on (if any):_

## Presubmit checklist

- [ ] This PR is marked as draft with an explanation if not meant to
land until a future stable release.
- [x] This PR doesn’t contain automatically generated corrections
(Grammarly or similar).
- [x] This PR follows the [Google Developer Documentation Style
Guidelines](https://developers.google.com/style) — for example, it
doesn’t use _i.e._ or _e.g._, and it avoids _I_ and _we_ (first person).
- [x] This PR uses [semantic line
breaks](https://github.com/dart-lang/site-shared/blob/main/doc/writing-for-dart-and-flutter-websites.md#semantic-line-breaks)
of 80 characters or fewer.

---------

Co-authored-by: Shams Zakhour (ignore Sfshaza) <[email protected]>
  • Loading branch information
miquelbeltran and sfshaza2 authored Nov 22, 2024
1 parent 4e9f9ba commit d2583ca
Show file tree
Hide file tree
Showing 12 changed files with 1,070 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Take our settings from the example_utils analysis_options.yaml file.
# If necessary for a particular example, this file can also include
# overrides for individual lints.

include: package:example_utils/analysis.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import 'dart:async';

import '../../domain/model/user_profile.dart';
import '../services/api_client_service.dart';
import '../services/database_service.dart';

class UserProfileRepository {
UserProfileRepository({
required ApiClientService apiClientService,
required DatabaseService databaseService,
}) : _apiClientService = apiClientService,
_databaseService = databaseService {
// #docregion Timer
Timer.periodic(
const Duration(minutes: 5),
(timer) => sync(),
);
// #enddocregion Timer
}

final ApiClientService _apiClientService;
final DatabaseService _databaseService;

// #docregion getUserProfile
Stream<UserProfile> getUserProfile() async* {
// Fetch the user profile from the database
final userProfile = await _databaseService.fetchUserProfile();
// Returns the database result if it exists
if (userProfile != null) {
yield userProfile;
}

// Fetch the user profile from the API
try {
final apiUserProfile = await _apiClientService.getUserProfile();
//Update the database with the API result
await _databaseService.updateUserProfile(apiUserProfile);
// Return the API result
yield apiUserProfile;
} catch (e) {
// Handle the error
}
}
// #enddocregion getUserProfile

// #docregion getUserProfileFallback
Future<UserProfile> getUserProfileFallback() async {
try {
// Fetch the user profile from the API
final apiUserProfile = await _apiClientService.getUserProfile();
//Update the database with the API result
await _databaseService.updateUserProfile(apiUserProfile);

return apiUserProfile;
} catch (e) {
// If the network call failed,
// fetch the user profile from the database
final databaseUserProfile = await _databaseService.fetchUserProfile();

// If the user profile was never fetched from the API
// it will be null, so throw an error
if (databaseUserProfile != null) {
return databaseUserProfile;
} else {
// Handle the error
throw Exception('User profile not found');
}
}
}
// #enddocregion getUserProfileFallback

// #docregion getUserProfileLocal
Future<UserProfile> getUserProfileLocal() async {
// Fetch the user profile from the database
final userProfile = await _databaseService.fetchUserProfile();

// Return the database result if it exists
if (userProfile == null) {
throw Exception('Data not found');
}

return userProfile;
}

Future<void> syncRead() async {
try {
// Fetch the user profile from the API
final userProfile = await _apiClientService.getUserProfile();

// Update the database with the API result
await _databaseService.updateUserProfile(userProfile);
} catch (e) {
// Try again later
}
}
// #enddocregion getUserProfileLocal

// #docregion updateUserProfileOnline
Future<void> updateUserProfileOnline(UserProfile userProfile) async {
try {
// Update the API with the user profile
await _apiClientService.putUserProfile(userProfile);

// Only if the API call was successful
// update the database with the user profile
await _databaseService.updateUserProfile(userProfile);
} catch (e) {
// Handle the error
}
}
// #enddocregion updateUserProfileOnline

// #docregion updateUserProfileOffline
Future<void> updateUserProfileOffline(UserProfile userProfile) async {
// Update the database with the user profile
await _databaseService.updateUserProfile(userProfile);

try {
// Update the API with the user profile
await _apiClientService.putUserProfile(userProfile);
} catch (e) {
// Handle the error
}
}
// #enddocregion updateUserProfileOffline

// #docregion sync
Future<void> sync() async {
try {
// Fetch the user profile from the database
final userProfile = await _databaseService.fetchUserProfile();

// Check if the user profile requires synchronization
if (userProfile == null || userProfile.synchronized) {
return;
}

// Update the API with the user profile
await _apiClientService.putUserProfile(userProfile);

// Set the user profile as synchronized
await _databaseService
.updateUserProfile(userProfile.copyWith(synchronized: true));
} catch (e) {
// Try again later
}
}
// #enddocregion sync
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import '../../domain/model/user_profile.dart';

// #docregion ApiClientService
class ApiClientService {
/// performs GET network request to obtain a UserProfile
Future<UserProfile> getUserProfile() async {
// #enddocregion ApiClientService
// Simulate a network GET request
await Future.delayed(const Duration(seconds: 2));
// Return a dummy user profile
return const UserProfile(
name: 'John Doe (from API)',
photoUrl: 'https://example.com/john_doe.jpg',
);
// #docregion ApiClientService
}

/// performs PUT network request to update a UserProfile
Future<void> putUserProfile(UserProfile userProfile) async {
// #enddocregion ApiClientService
// Simulate a network PUT request
await Future.delayed(const Duration(seconds: 2));
// #docregion ApiClientService
}
}
// #enddocregion ApiClientService
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import '../../domain/model/user_profile.dart';

// #docregion DatabaseService
class DatabaseService {
/// Fetches the UserProfile from the database.
/// Returns null if the user profile is not found.
Future<UserProfile?> fetchUserProfile() async {
// #enddocregion DatabaseService
// Simulate a database select query
await Future.delayed(const Duration(milliseconds: 100));
// Return a dummy user profile
return const UserProfile(
name: 'John Doe (from Database)',
photoUrl: 'https://example.com/john_doe.jpg',
);
// #docregion DatabaseService
}

/// Update UserProfile in the database.
Future<void> updateUserProfile(UserProfile userProfile) async {
// #enddocregion DatabaseService
// Simulate a database update query
await Future.delayed(const Duration(milliseconds: 100));
// #docregion DatabaseService
}
}
// #enddocregion DatabaseService
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'package:freezed_annotation/freezed_annotation.dart';

part 'user_profile.freezed.dart';

// #docregion UserProfile
@freezed
class UserProfile with _$UserProfile {
const factory UserProfile({
required String name,
required String photoUrl,
@Default(false) bool synchronized,
}) = _UserProfile;
}
// #enddocregion UserProfile
Loading

0 comments on commit d2583ca

Please sign in to comment.