Sprint 3 - mobile-dev-16/S1-G16-planning GitHub Wiki
Implemented Business Questions
Sprint 2
- What is the average loading time of the app from launch to the main page? (Type 1) ( Raul )
- Is there a surplus of your favorite cuisine available today near your location? (Type 2) ( Abel )
- Is the user currently located far from their saved address? (Type 2) ( Raul )
Sprint 3
- How does the app's performance (loading times, crash rates) correlate with user retention rates? (Type 5) ( Raul )
- Are there any new partner businesses offering surplus deals near the user's saved address today? (Type 2) ( Mario )
- Are there new surplus food deals available that match the user’s dietary preferences today? (Type 2) ( Abel )
List of implemented Functionalities
Sprint 2
- User Authentication (Google and Email Sign-In, Sign-Up).
- Address Management with GPS and Google Maps API.
- Calculate loading time of the app from launch to the main page and loading with splash.
- Add and delete cart items.
- Scrollable order items.
Sprint 3
- Profile information registration.
- Modifiable diet and food type for better user experience
- "For you" uses user favorite cuisineType
- Log out option
- User information in cache
- Flutter toast to manage Eventual Connectivity
- Complete cart items management.
- calculate total order and delete it completely
- List of available restaurants
Eventual Connectivity Solutions and Scenarios
Our connectivity solution is designed to keep users constantly aware of their internet status, minimizing confusion and enhancing user experience by avoiding common anti-patterns. The solution relies on two main components: InternetConnectionBloc
and NetworkInfo
.
The InternetConnectionBloc
listens for changes in network connectivity using the connectivity_plus
library. When there’s a change, it emits specific events: ConnectedEvent
when internet is available and DisconnectedEvent
when it’s lost. These events trigger state updates in the bloc (ConnectedInternet
or DisconnectedInternet
), which are then communicated to the user via toast notifications, keeping them informed in real-time.
Meanwhile, the NetworkInfo
class constantly checks the device's connectivity status and collaborates with the InternetConnectionBloc
to ensure a responsive connection update mechanism. This setup allows widgets within the app to react dynamically to connectivity changes, ensuring users are consistently informed and can make decisions accordingly. For example, in the UI, a SnackBar
or toast message notifies users whenever there is a change in connection, as illustrated in the example screenshots. This proactive approach to connectivity management helps maintain user trust and smooth app operation.
Offline Data Access and Updates
- Scenario: The user is offline and tries to access or update their profile information.
- Strategy: Implement local caching using SharedPreferences to store user data and updates locally. When the user is offline, any updates to the profile are saved in the local cache. Once the connectivity is restored, the cached updates are synchronized with the server.
- Behavior: When the user updates their profile offline, the app saves the changes locally and informs the user that the data will be synchronized once the connection is available. Upon reconnection, the app automatically syncs the cached data with the server.
Data Synchronization on Reconnection
- Scenario: The user was offline and made several updates to their profile. The app needs to synchronize these updates once the connection is restored.
- Strategy: Implement a mechanism to detect reconnection and trigger the synchronization of cached data with the server. Use a background service or a periodic check to ensure that the data is synchronized as soon as the connection is available.
- Behavior: When the app detects that the connection is restored, it automatically synchronizes the cached profile updates with the server. The user is notified that their data has been successfully synchronized.
User Authentication and Session Management
- Scenario: The user is offline and tries to log in or access restricted features.
- Strategy: Implement offline authentication using cached credentials. Allow the user to access certain features based on the last known authentication state. Once the connection is restored, validate the session with the server.
- Behavior: When the user tries to log in offline, the app checks the cached credentials and allows access to restricted features if the credentials are valid. Upon reconnection, the app validates the session with the server and updates the authentication state accordingly.
How It Works
-
onnectivity Detection: The InternetConnectionBloc uses the connectivity_plus library to detect changes in internet connectivity. When there is a change, it emits an event (ConnectedEvent or DisconnectedEvent).
-
Event Handling: The InternetConnectionBloc handles these events and updates its state (ConnectedInternet or DisconnectedInternet). It also shows a toast message to notify the user about the change in state.
-
UI Reaction: Widgets that listen to the InternetConnectionBloc can react to these state changes. In the example above, the InternetStatusWidget shows a SnackBar when the internet connection state changes.
InternetConnectionBloc
The InternetConnectionBloc listens for connectivity changes and emits corresponding events
NetworkInfo
The NetworkInfo class checks the connectivity status and emits events to the InternetConnectionBloc.
Visualization in the Application
When the application detects a change in internet connectivity, a SnackBar is displayed at the bottom of the screen:
- Internet Connection: A green SnackBar with the message "Connected to the internet" is shown.
- No Internet Connection: A red SnackBar with the message "No internet connection. Some information may not be up to date." is shown. This provides immediate visual feedback to the user about the internet connection status.
The eventual connectivity solution is implemented and the widgets in the application can listen to these changes and react accordingly, providing a smooth and responsive user experience.
Implemented Local Storage Strategies
How It Works
The application uses local storage strategies to persistently save data on the user's device, allowing it to access this information even after being closed and reopened. SharedPreferences
is a Flutter library that provides a simple way to store key-value pairs of primitive data types (such as strings, integers, and booleans) in persistent storage. SharedPreferences
works by saving the data in an XML file in the device's internal storage. When a value is saved, it is associated with a unique key, making it easy to retrieve later. For example, to save a user ID, the application uses setString('userId', userId)
, and to retrieve it, it uses getString('userId')
. This technique is useful for storing user settings, session data, and other information that needs to be persistent across application sessions.
Implementation
Snipppet 1
The _getUserId function retrieves the unique ID of the currently authenticated user from Firebase Authentication. It first accesses the current user through FirebaseAuth.instance.currentUser. If a user is logged in, it returns their unique identifier (uid). If no user is logged in, it simply returns null. This function is useful for identifying the user in order to personalize the app experience, retrieve user-specific data, or manage user-specific storage operations, ensuring that actions within the app are tied to the correct user.
Snipppet 2
This code attempts to fetch the userId stored in SharedPreferences, which is a local storage tool on mobile devices. If the userId is available, it logs the value and checks if it’s not empty before returning it. If an error occurs during retrieval, the code catches and logs the error, returning null to indicate that the user ID could not be obtained. This error-handling process is essential for ensuring the app’s stability and reliability.
Snipppet 3
local data source is implemented that uses SharedPreferences to store and retrieve user data in JSON format. The getCachedUser function tries to retrieve the user data from SharedPreferences in JSON format, converting it to a UserModel object if found. The cacheUser function saves the user data in JSON format to SharedPreferences for quick access in the future. This implementation is useful for keeping user data accessible even when the device has no internet connection.
Snipppet 4
part of the SplashBloc class, which executes when the app starts. The _onAppStarted function tries to retrieve the userId from SharedPreferences, using this data to check the user’s authentication status on app startup. Based on the retrieved userId, the SplashBloc emits states indicating whether the user is authenticated. This helps decide which screen to display to the user, facilitating initial navigation and the login experience within the app.
Implemented Multi-Threading Strategies
Our application implements multi-threading strategies primarily through the use of asynchronous programming with async
and await
keywords in Dart. This allows the application to perform non-blocking operations, such as network requests and database queries, without freezing the user interface. Here are some examples:
- Fetching Data from Remote Sources:
- In
FoodBusinessRemoteDataSourceImpl
(e.g.,fetchNearbySurplusFoodBusinesses
method), asynchronous operations are used to query Firestore and fetch data without blocking the main thread.
In the fetchNearbySurplusFoodBusinesses
method of the FoodBusinessRemoteDataSourceImpl
class, asynchronous programming is used to query Firestore without blocking the main thread. First, a query is built on the foodBusiness collection in Firestore, applying filters based on cuisine type and category if they are present. Then, a QuerySnapshot
of the matching documents is obtained. For each document, another query is made to the offers subcollection to get available and valid offers. If there are available offers, the distance between the user's location and the food business is calculated. If the distance is within the specified range, the business is added to the result list. All of this is done asynchronously to avoid blocking the user interface.
- Saving and Retrieving Data Locally:
- In
UserLocalDataSourceImpl
(e.g.,getCachedUser
,cacheUser
methods), asynchronous operations are used to interact withSharedPreferences
to save and retrieve data.
In the UserLocalDataSourceImpl
class, the getCachedUser
and cacheUser
methods use SharedPreferences
to persistently store and retrieve user data. The getCachedUser
method retrieves a JSON string stored under the StorageKeys.user
key, decodes it, and converts it into a UserModel
object. If the string is not found, it returns null. The cacheUser method takes a UserModel object, converts it to a JSON string, and stores it in SharedPreferences
under the StorageKeys.user
key. Both methods are asynchronous, allowing these read and write operations to be performed in the background without blocking the main thread.
- Network Connectivity Checks:
- In
NetworkInfo
(e.g.,isConnected
method), asynchronous operations are used to check the network connectivity status.
In the NetworkInfo
class, the isConnected method checks the network connectivity status asynchronously. It uses the connectivity_plus
library to get a list of connectivity results. If there are results and none of them is ConnectivityResult.none, the device is considered to be connected to the Internet. Depending on the connection status, a corresponding event (ConnectedEvent
or DisconnectedEvent
) is emitted to the InternetConnectionBloc
. This approach allows the application to handle network connectivity changes without blocking the main thread.
- User Authentication:
- In
UserRemoteDataSourceImpl
(e.g.,signInWithEmailAndPassword
,signUpWithEmailAndPassword
methods), asynchronous operations are used to interact with Firebase Authentication.
In the UserRemoteDataSourceImpl
class, the signInWithEmailAndPassword
method uses Firebase Authentication to authenticate a user with their email and password. This method is asynchronous and uses await to wait for Firebase to complete the sign-in operation. If the authentication is successful and a valid UserCredential
is obtained, it is converted into a UserModel
object and returned. If the authentication fails, an AuthException
with an error message is thrown. This approach allows the sign-in operation to be performed in the background, keeping the user interface responsive.
These strategies ensure that the application remains responsive by offloading time-consuming tasks to separate threads, allowing the main thread to handle user interactions smoothly.
Implemented Caching Strategies
The UserLocalDataSource
class is responsible for caching user data locally using SharedPreferences.
It provides methods to store, retrieve, and clear user information and user IDs. The getCachedUser
method retrieves a JSON string stored under the StorageKeys.user
key, decodes it, and converts it into a UserModel
object. If the string is not found, it returns null. The cacheUser
method takes a UserModel
object, converts it to a JSON string, and stores it in SharedPreferences
under the StorageKeys.user
key. Similarly, getCachedUserId
and cacheUserId
methods handle the storage and retrieval of the user ID. The clearUserCache
and clearUserId
methods remove the stored user data and user ID, respectively. These operations are asynchronous, allowing them to be performed in the background without blocking the main thread, ensuring a responsive user interface. This caching strategy ensures that user data is persistently available across app sessions and can be accessed quickly without requiring a network call.
This function is used to check and save the cached profile data when the Internet connection is re-established. First, it retrieves the cached profile data from SharedPreferences under the cachedProfile key. If there is cached data, it converts it to a UserProfile object and calls the updateUserProfileUseCase use case to update the profile on the server. Then, it removes the cached data from SharedPreferences. If the update is successful, it emits a ProfileLoaded state with the updated profile. If not, it emits a ProfileError state with an error message.
This function handles the LoadProfileEvent event, which is fired when the user's profile is loaded. First, the function emits a ProfileLoading status to indicate that loading is in progress. Then, it attempts to get the user's ID. If the user's ID cannot be obtained, it emits a ProfileError status with an error message. If the user's ID is obtained, the function calls the fetchUserProfileUseCase use case to retrieve the user's profile from the server. If it is retrieved successfully, it emits a ProfileLoaded status with the user's profile. If not, it emits a ProfileError status with an error message.