Sprint 4 - EQUIPO-11-CONSTR-APLIC-MOVILES/Team-11-Wiki GitHub Wiki
- Creation of the MenuItemsRepository, MenuItemsRepositoryImpl, MenuItem, GetRestaurantMenu, MenuItemsUseCases and modification of the AppModule for DI.
- Implementation of the menu list in the MenuItemsScreen along with the supporting elements: MenuItemCard, MenuItemsEvent, MenuItemsState, MenuItemsViewmodel.
- Modification of the NavGraph, RestaurantScreen and Route for navigation.
- Modification of the RestaurantsRepositoryImpl and the UserRepositoryImpl to improve loading times when needing cached information.
- Modification of the NavigatorViewModel.kt to load restaurants during the splashcreen.
- Creation of the MapSearchTimesRepository.kt, MapSearchTimesRepositoryImpl.kt, SendMapSearchTimes.kt for sending the event for recording the usage of the pin button in the restaurant detail screen.
- Modification of the AnalyticsUseCases.kt to include the SendMapSearchTimes use case.
- Modification of the RestaurantViewModel to call the new use case. Modification of the AnalyticsUseCases file to include the use case.
- Corresponding modifications to the AppModule.kt.
- Modification of MapViewModel, MapScreen and MapState to use LRUCache to optimize the image cache in the map.
- Documentation and analysis of one microoptimization.
- Implementation and inclusion in the wiki of the eventual connectivity strategy of the menu item list.
- Implementation and inclusion in the wiki of the caching strategy of the menu list.
- Implementation of the favorite categories business question.
- Participation in the ethics video.
- Updating shared preferences to include the likes in the local storage and fix the likes feature.
- Including error icon on menu item images for when image network request fails.
- Implemented the flutter profile screen
- Implemented the local storage strategy flutter
- Contribution to ethic's video
- Implemented detail screen map button BQ endpoint
- Documented local storage strategy
- Documented eventual connectivity strategy for profile screen
- Editted and uploaded ethic's video
- Implemented menu item detail
- Implemented BQ Presentation: What is the match percentage between a restaurant and the user preferences?
- Implemented BQ: How far is a restaurant from the user?
- Participation in the ethics video.
- Implemented the Kotlin profile screen.
- Implemented the local storage strategy kotlin with SharedPreferences.
- Contribution to ethic's video.
- Implemented reviews left per week BQ endpoint.
- Documented local storage strategy in kotlin.
- Logout implementation.
- Eventual connectivity strategies documentation profile screen.
- Update user information implementation across the app.
- Menu Detail re design
- Menu Detail Implementation
- Implemented BQ: What is the match percentage between a restaurant and the user preferences?
- Implemented BQ Presentation: How far is a restaurant from the user?
- Documented Multithreading strategy
- Listed Delivered Features
- Eventual connectivity strategies documentation for Menu Detail
- Participation in the ethics video.
- Implemented menu screen
- Implemented BQ: What is average rating per week?
- Participation in the ethics video.
Depending on the location of the user and the established search radius, this context aware feature will automatically adapt and show the restaurants near the user. This will improve the user's experience by narrowing down the restaurant search space by showing only the restaurants the user is willing to reach/transport to. This will allow the user to find a restaurant within distances he/she feels comfortable with.
A dedicated section in the app highlights only the restaurants currently open based on real-time business hours. This feature ensures that users can instantly find operational dining options, saving time and avoiding inconvenience.
For the smart feature there is a section in the home screen that recommends the user restaurants they might like and improve their experience through the highlighting of restaurants that match their registered likings in the app. They will see this information by just pressing the "For you" button in the home screen.
This section allows users to easily access and manage their list of favorite restaurants. By keeping track of liked places, users can quickly revisit or recommend them to friends and family, enhancing their overall dining experience.
Login and register screens for users to enjoy all the personalization the app offers. This feature ensures secure access and allows users to save their preferences and interactions across devices.
A menu where users can specify their favorite cuisines, dietary needs, and other preferences. This information helps tailor restaurant recommendations, menus, and listings, making the app feel uniquely personalized.
Functionality that uses the phone's microphone to listen to the user's voice to fill the search field with their words when searching for restaurants in the search screen. This feature enables hands-free searching, improving accessibility and convenience.
A visually dynamic map feature that shows restaurants as interactive cards. These cards display essential details like the name, distance, ratings, and current status of each restaurant, offering a seamless exploration experience for users.
Users can share their dining experiences by writing reviews for restaurants they have visited. This feature includes a user-friendly interface where users can rate restaurants and provide comments. This helps create a reliable database of feedback that enhances the community-driven aspect of the app.
A comprehensive list of all reviews submitted by users for a specific restaurant. This feature organizes reviews in an intuitive format.
A detailed view for each restaurant that aggregates all relevant information in one place. This screen includes the restaurant's name, description, menu, location, business hours, contact information, user reviews, and personalized match percentage based on user preferences. The goal is to offer users a complete picture of the restaurant to streamline decision-making.
A fun feature designed to help indecisive users quickly choose a dining spot. With a simple tap on the "Random Restaurant" button, the app selects a restaurant from the user’s liked restaurants. The result is presented with key details, such as name, cuisine type, and match percentage. This feature encourages users to explore new places while saving time in decision-making.
Displays the full menu of a selected restaurant in an organized and visually appealing layout. Users can browse through appetizers, main courses, desserts, and drinks.
Provides an in-depth look at individual menu items. Each item includes a name, description and an accompanying photo.
A dedicated section where users can update their personal information, preferences, and settings. Users can change their profile picture and modify their name.
RestaU aims to revolutionize how students and staff around the Uniandes area discover and access restaurants. By centralizing information, the app addresses the pain points of disorganization, lack of reliable reviews, and inefficiency in finding dining options that match dietary preferences and budgets. Unlike generic alternatives, RestaU offers tailored solutions designed specifically for the unique needs of this community.
Our core features empower users to make informed dining decisions. With comprehensive restaurant details such as menus, reviews, average pricing, and operating hours, users can quickly evaluate their options. Advanced filters allow users to search by criteria like budget, type of cuisine, seating arrangements, and user ratings. Additionally, the randomizer feature removes decision fatigue, offering users an exciting way to explore their favorite or nearby restaurants. Integration with maps ensures seamless navigation, eliminating frustration from finding closed or inconveniently located eateries. These features enhance user satisfaction by providing convenience and minimizing the time spent choosing a restaurant.
RestaU’s implemented business questions demonstrate our commitment to data-driven decisions. By analyzing metrics such as the most popular categories, user preferences, and interaction patterns, we ensure that the platform evolves to meet user expectations. For example, identifying frequently clicked categories or popular times for Google Maps redirection helps us fine-tune recommendations and provide valuable insights to restaurant partners. Tracking reviews over time encourages community interaction, creating a reliable and dynamic ecosystem of feedback.
The revenue model of RestaU ensures sustainability and growth. By offering advertising opportunities to local restaurants, such as promoting their listings in search results or highlighting them on the map, we provide them with a cost-effective way to reach their target audience. This strategy not only generates revenue but also supports new and small businesses by increasing their visibility, fostering a more vibrant and diverse gastronomic community around the campus.
Our technical optimizations enhance user experience and ensure efficient performance. For example, caching strategies, such as storing menu items locally or optimizing image handling with an LRU cache, reduce latency and improve app responsiveness. By minimizing memory usage and network dependency, these micro-optimizations deliver a smoother experience, even on resource-constrained devices. Additionally, features like local storage in the profile screen enhance personalization and reliability, ensuring a consistent user experience even when offline.
RestaU stands out for its commitment to community-building and inclusivity. By enabling users to share reviews and display their favorite restaurants, we foster a sense of connection. Highlighting new restaurants ensures continuous discovery while supporting the growth of emerging businesses. These elements differentiate RestaU from generic platforms, making it a unique and indispensable tool for the Uniandes community.
When looking for micro-optimization opportunities we came across our custom cache for the images of the restaurants portrayed in the map. In the map, when pressing a pin, a picture of the restaurant, it’s name and review are shown. The restaurants’ pictures aren’t saved in the coil library’s cache due to it’s incompatibility with the Google Maps library, the images are saved in a list with bitmaps in the view model. This can be optimized because every single image visited is saved rather than the most recent ones. This can be improved by changing the list with an LRU cache structure that keeps the 3 most recent images saved with strong references and the others can be collected by the garbage collector.
To evaluate the effectiveness of the micro-optimization we profiled memory before implementing the change, more specifically we defined a scenario where the user would enter the search page, then the map in the Uniandes area, trigger a garbage collection event and click on about 6 pins to load the images in memory, return to the search page and then trigger another garbage collection event. With this sequence of events, we could evaluate how much memory was used under the original and optimized case.
In the MapState
data class it is seen how the images are saved in a list (line 6) that then it is initialized with the size of the restaurants list. In the downloadImage
method from the MapViewModel
, it can be seen how it is checked if the image is already saved (line 2), if it’s not, the image is retrieved via the corresponding use case (line 4), the list is copied into a mutable list to make the modification (lines 5 and 6) and then the state is updated (line 7-9). This implementation has a lot of problems, first, the images are never dropped within the session and accumulate in the list, additionally, a new list is created whenever a new image is retrieved.
data class MapState(
val restaurants: List<Restaurant> = emptyList(),
val startLocation: LatLng = LatLng(4.603096177609384, -74.06584744436493),
val isLoading: Boolean = true,
val permission: Boolean = true,
val images: List<ImageBitmap?> = emptyList(),
val idIndexMap: Map<String, Int> = emptyMap(),
)
private fun downloadImage(restaurants: List<Restaurant>, index: Int, onGather: suspend () -> Unit) {
if (state.images[index] == null) {
viewModelScope.launch(Dispatchers.IO) {
val bitmap = imageDownloadUseCases.downloadSingleImage(restaurants[index].imageUrl)
val images = state.images.toMutableList()
images[index] = bitmap
state = state.copy(
images = images,
)
withContext(Dispatchers.Main) {
onGather()
}
}
}
}
When testing the first version, we analyzed the final amount of native memory (where bitmaps are stored) after executing the scenario. We see how it has an amount of 160 MB.
We analyzed the Shallow Size of the byte[]
objects which are related to the Bitmap object. We see we get a shallow size of 3,884,271 bytes.
Now, the implementation of the micro optimization can be seen. It is highlighted that instead of the list of ImageBitmap
we see a LruCache
with integers as keys and ImageBitmap
as the value (line 6). The downloadImage
method’s implementation changes slightly, we see how the implementation is drastically shortened because, instead of having to copy the array, save the item and update the state, we just simply insert the image in the LruCache (lines 5-7).
data class MapState(
val restaurants: List<Restaurant> = emptyList(),
val startLocation: LatLng = LatLng(4.603096177609384, -74.06584744436493),
val isLoading: Boolean = true,
val permission: Boolean = true,
val images2: LruCache<Int, ImageBitmap> = LruCache(3),
val idIndexMap: Map<String, Int> = emptyMap(),
)
private fun downloadImage(restaurants: List<Restaurant>, index: Int, onGather: suspend () -> Unit) {
if (state.images2[index] == null) {
viewModelScope.launch(Dispatchers.IO) {
val bitmap = imageDownloadUseCases.downloadSingleImage(restaurants[index].imageUrl)
if (bitmap != null) {
state.images2.put(index, bitmap)
}
withContext(Dispatchers.Main) {
onGather()
}
}
}
}
When studying the used native memory, we see that at the end of the scenario, using the micro optimization, we see that 153.1 MB of memory was used. We see that the difference was 6.9 MB less than the previous case.
We see the byte[]
objects occupying 3,768,243 bytes which is 116,028 bytes (116 K bytes) less than the test without the implementation. This makes sense because the image files used are most of them below 50K bytes.
In conclusion, we can say that this micro-optimization improves the use of memory avoiding the constant copying of lists and saving a lot of images. Now, only the 3 most recently seen images are saved into our LruCache
structure to allow the user to check these images without having to wait for another network request to be completed.
In this micro-optimization the goal was to identify the performance impact between two implementations: iterating directly over the list objects (items(restaurants)
) and using indices to access the elements (items(restaurants.size)
) in the search screen. This analysis aims to document the findings and justify the final decision based on measurable data.
In the original implementation, which directly iterated over the list objects, we observed a higher number of memory allocations, particularly for structures like float[]
, int[]
, and Object[]
. This is because Jetpack Compose creates additional references to the objects during iteration, which can introduce minor overhead in terms of allocations. However, this implementation optimizes object management at the recomposition level, as Compose can more efficiently track changes to the rendered elements.
Before micro-optmization:
items(restaurants) { restaurant ->
RestaurantCard(
isNew = false,
isFavorite = true,
restaurantId = restaurant.documentId,
name = restaurant.name,
imageUrl = restaurant.imageUrl,
placeName = restaurant.placeName,
averageRating = restaurant.averageRating.toFloat(),
onFavorite = {},
onClick = { onRestaurantClick(restaurant) },
showLikeButton = false
)
Spacer(modifier = modifier.height(29.dp))
}
After micro-optmization:
items(restaurants.size) { index ->
RestaurantCard(
isNew = false,
isFavorite = false,
restaurantId = restaurants[index].documentId,
name = restaurants[index].name,
imageUrl = restaurants[index].imageUrl,
placeName = restaurants[index].placeName,
averageRating = restaurants[index].averageRating.toFloat(),
onFavorite = {},
onClick = { onRestaurantClick(restaurants[index]) },
showLikeButton = false
)
Spacer(modifier = modifier.height(29.dp))
}
Before micro-optmization: After micro-optmization:
Switching to the index-based implementation resulted in a significant reduction in memory allocations, especially for structures like float[]
. This is because accessing the list elements directly via their indices avoids creating extra references during iteration. This optimization is beneficial in scenarios where frequent allocations are a bottleneck, such as rendering large lists or running on resource-constrained devices.
Before micro-optmization: After micro-optmization:
When comparing the total memory usage between both implementations, we found only a minor difference. This suggests that there was no substantial improvement in this area, and the variations observed could be attributed to external factors, such as the system’s internal memory handling during tests. Since the difference is negligible, the results can fluctuate slightly between runs, making it difficult to claim a definitive advantage in terms of total memory usage for either approach.
After analyzing the metrics and the main use cases of our application, we decided to retain the original implementation that directly iterates over the objects. This decision aligns with Jetpack Compose best practices, ensuring more efficient recomposition handling and a more consistent experience on low-resource devices. Additionally, we prioritized code clarity to facilitate long-term maintenance.
https://developer.android.com/studio/profile/memory-profiler https://developer.android.com/topic/performance/graphics/manage-memory https://www.kodeco.com/4557771-android-memory-profiler-getting-started/page/2
Type 1: What features are causing the app to crash unexpectedly?
Type 2: Is there a new restaurant nearby that matches the user's 'liked' cuisine types?
Type 3: What are the screens where the users in average spend the least time weekly?
Type 3: What are the most common navigation paths users take within the app?
Type 4: What are the most searched cuisines or types of food by the users?
Type 5: Which of the app's features are the most used?
Type 1: What is the number of users per device model?
Type 2: What restaurant(s) have been the most liked or positively reviewed this week?
Type 2: What percentage of the restaurants has the user left a review in?
Type 3: How often does a user leave a review after using the "randomize restaurant" feature?
Type 4: What common qualities are shared by the restaurants most frequently added to users' favorites?
Type 5: In what area are the restaurants that are the most liked?
Type 1: What are the most frequently occurring events within the app?
Type 2: What is the match percentage between a restaurant and the user preferences?
Type 2: How far is a restaurant from the user?
Type 3: What is the average rating left by the users per week?
Type 3: How many reviews are left per week?
Type 3: How many times was the Google Maps redirection button cliked in the detail screen in the last week?
Type 4: What are the most popular categories according to users?
Event Description | The user wants to see the menu list of a restaurant but has no internet connection. |
---|---|
System Response | The system will still show the menu list but using old locally saved data. |
Possible Antipatterns | #4 Lost content, #2 Stuck progress bar, #3 Non-informative message |
Caching + Retrieving Strategy | #3 Network falling back to cache, #4 Cached on network response |
Storage Type | 1.e. Firestore cache, Coil library's memory and disk cache. |
Stored Data Type | Menu Items Documents, User Document, Image files |
Rationale | The menu list isn't going to change and expire frequently, so this public data will be shown from the saved information when there is no connectivity. |
Event Description | The user wants to see a menu list for the first time but has no internet connection. |
---|---|
System Response | The system will show a message indicating that the user should connect to the internet to initiate their experience with the app. |
Possible Antipatterns | #2 Stuck progress bar, #4 Lost content, #3 Non-informative message |
Caching + Retrieving Strategy | #6 Generic fallback |
Storage Type | 1.d. App's stored files with texts and icons. |
Stored Data Type | Strings and XML |
Rationale | A message will be shown to the user telling them that there is no connectivity because there is no saved information and the user needs to connect to fetch the desired data. |
Event Description | The user wants to see the detail of a menu item but has no internet connection. |
---|---|
System Response | The system will still show the menu item detail but using old locally saved data. |
Possible Antipatterns | #4 Lost content, #2 Stuck progress bar, #3 Non-informative message |
Caching + Retrieving Strategy | #3 Network falling back to cache, #4 Cached on network response |
Storage Type | 1.e. Firestore cache, Coil library's memory and disk cache. |
Stored Data Type | Menu Items Documents, User Document, Image files |
Rationale | The menu item isn't going to change and expire frequently, so this public data will be shown from the saved information when there is no connectivity. |
Event Description | The user wants to see a menu item for the first time but has no internet connection. |
---|---|
System Response | The system will show a message indicating that the user should connect to the internet to initiate their experience with the app. |
Possible Antipatterns | #2 Stuck progress bar, #4 Lost content, #3 Non-informative message |
Caching + Retrieving Strategy | #6 Generic fallback |
Storage Type | 1.d. App's stored files with texts and icons. |
Stored Data Type | Strings and XML |
Rationale | A message will be shown to the user telling them that there is no connectivity because there is no saved information and the user needs to connect to fetch the desired data. |
Event Description | The user wants to see his profile information but has no internet connection. |
---|---|
System Response | The system will still show the user infomation saved in the SharedPreferences. |
Possible Antipatterns | #4 Lost content, #2 Stuck progress bar, #3 Non-informative message |
Caching + Retrieving Strategy | #3 Network falling back to cache, #4 Cached on network response |
Storage Type | 1.e. Firestore cache, SharedPreferences, Coil library's memory and disk cache. |
Stored Data Type | User Document, Image files |
Rationale | The menu list isn't going to change and expire frequently, so this public data will be shown from the saved information of the user when there is no connectivity. |
Event Description | The user wants to see his profile information for the first time but has no internet connection. |
---|---|
System Response | The system will show a message indicating that the user should connect to the internet to initiate their experience with the app. |
Possible Antipatterns | #2 Stuck progress bar, #4 Lost content, #3 Non-informative message |
Caching + Retrieving Strategy | #6 Generic fallback |
Storage Type | 1.d. App's stored files with texts and icons. |
Stored Data Type | Strings and XML |
Rationale | A message will be shown to the user telling them that there is no connectivity because there is no saved information of the user and the user needs to connect to fetch the desired data. |
Event Description | The user wants to update his data but has no internet connection. |
---|---|
System Response | The system will show an explicit message indicating that the user should connect to the internet to update his information |
Possible Antipatterns | #4 Lost content, #2 Stuck progress bar, #3 Non-informative message |
Caching + Retrieving Strategy | #6 Generic fallback |
Storage Type | User Document, Image files |
Stored Data Type | Strings and XML |
Rationale | A message will be shown to the user telling them that there is no connectivity indicating the user that need internet to update his data so the user know it |
Event Description | The user wants once on the restaurant detail screen wants to see the menu but lost connection. |
---|---|
System Response | The system will still show the menu list but using the last locally saved data. |
Possible Antipatterns | #4 Lost content, #2 Stuck progress bar, #3 Non-informative message |
Caching + Retrieving Strategy | #3 Network falling back to cache, #4 Cached on network response |
Storage Type | 1.e. Firestore cache. |
Stored Data Type | Menu Items Documents, User Document, Image files |
Rationale | The menu is quite static information that does not change very often. |
Event Description | The user wants to see a menu list for the first time but lost internet connection. |
---|---|
System Response | The system will show a message suggesting the user to connect its device to internet. |
Possible Antipatterns | #2 Stuck progress bar, #4 Lost content, #3 Non-informative message |
Caching + Retrieving Strategy | #6 Generic fallback |
Storage Type | 1.d. App's stored files with texts and icons. |
Stored Data Type | Strings and XML |
Rationale | A message will be shown to the user telling them that the internet connection is compulsory to display the menu this time. |
Event Description | The user wants to update his username but has no internet connection. |
---|---|
System Response | The system will save the new name in cache and send it to the database when connection is regained. |
Possible Antipatterns | #8 Unclear behavior, #7 Unavailable Functionality after Connection Recovery |
Caching + Retrieving Strategy | Cache falling back on network |
Storage Type | 1.d. App's stored files with texts and icons. |
Stored Data Type | String |
Rationale | The name will be stored in shared preferences and will update the database when connection is regained, so the app continues to work normally |
Event Description | The user wants to log out but has no internet connection. |
---|---|
System Response | The system will allow the user to log out. |
Possible Antipatterns | #8 Unclear behavior, #6 Redirection without connectivity check |
Caching + Retrieving Strategy | Cache falling back on network |
Storage Type | N/A |
Stored Data Type | N/A |
Rationale | The user will be able to perform this task as if they had connection |
The local storage strategy in Kotlin uses SharedPreferences as a persistent cache to optimize access to user data. This approach reduces the need for frequent queries to Firebase and enhances performance by enabling quick, local data retrieval.
When the getUser function is called, it first checks SharedPreferences for the requested user’s data. If found, the data is deserialized using Gson and returned immediately, avoiding a Firebase query. If the data is not present locally, the function queries Firebase and, upon successfully fetching the data, stores it in SharedPreferences for future use. This caching mechanism minimizes redundant network requests.
override suspend fun getUser(email: String): User {
val sharedPreferences = context.getSharedPreferences("AppPrefs", Context.MODE_PRIVATE)
val userJson = sharedPreferences.getString(email, null)
var user: User
if (userJson != null) {
user = Gson().fromJson(userJson, User::class.java)
} else {
try {
val snapshot = db
.collection("users")
.whereEqualTo("email", email)
.get()
.await()
user = User()
for (document in snapshot.documents) {
document.toObject<User>()?.let {
user = it.copy(documentId = document.id)
}
}
val editor = sharedPreferences.edit()
editor.putString(email, Gson().toJson(user))
editor.apply()
} catch (e: Exception) {
Log.w(TAG, e.toString())
user = User()
}
}
return user
}
Updates to user data, such as with the updateUserInfo function, are synchronized with both Firebase and SharedPreferences. Once the updates are successfully sent to Firebase, the new data is stored locally, ensuring that SharedPreferences always reflect the latest user information. Each user is stored independently in SharedPreferences, with their email address serving as the unique key. This design supports applications with multiple users, keeping their data separate and easily accessible.
override suspend fun updateUserInfo(user: User): Boolean {
return try {
val info = mapOf(
"name" to user.name,
"profilePic" to user.profilePic
)
db.collection("users").document(user.documentId).set(info, SetOptions.merge()).await()
val sharedPreferences = context.getSharedPreferences("AppPrefs", Context.MODE_PRIVATE)
val editor = sharedPreferences.edit()
editor.putString(user.email, Gson().toJson(user))
editor.apply()
true
} catch (e: Exception) {
Log.e("UsersRepository", "updateUserInfo: failure", e)
false
}
}
By combining SharedPreferences for local storage and Firebase as the primary data source, this strategy achieves an efficient balance between performance and data consistency, offering a seamless experience for managing user information.
The local storage strategy in the Flutter app was implemented in the profile screen, where the user can update their username and checkout their profile picture. To implement this, whenever the username is updated, the new value is stored in the app's shared_preferences. Additionally, when these values are required, they are also fetched from shared_preferences.
Future<String?> getUserName() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
String? name = prefs.getString('user_name');
return name;
}
The previous code shows the function to get the username in the user_viewmodel.dart file, which is also a singleton, accessed throughout the app.
Future<void> updateUserName(String newName) async {
try {
// Get the current user's email
String? email = FirebaseAuth.instance.currentUser?.email;
if (email == null) {
throw Exception("User is not logged in.");
}
// Query the user's document in Firestore
QuerySnapshot querySnapshot = await _db.collection('users')
.where('email', isEqualTo: email)
.get();
if (querySnapshot.docs.isNotEmpty) {
// Get the document ID of the user
String docId = querySnapshot.docs.first.id;
// Update the name in Firestore
await _db.collection('users').doc(docId).update({'name': newName});
// Optionally update SharedPreferences if needed
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString('user_name', newName);
print("User name updated successfully.");
} else {
print('No user found with email: $email');
}
} catch (e) {
print("Error updating user name: $e");
}
}
On the other hand, the method that updates the username, updates the current value stored in shared preferences, as well as sending a petition to firebase, which will be stored by the firebase cache and sent whenever a connection can be made.
MenuDetailViewModel employs multithreading through Coroutines, using viewModelScope to perform background tasks without blocking the main thread. When the MenuDetail Screen is launched it initiates a coroutine using viewModelScope.launch, which runs in the background to retrieve a menu item from the MenuItemsUseCases asynchronously. This design ensures that even when the operation involves network delays, the main thread remains free to handle user interactions.
private fun onLaunch(itemID: String){
viewModelScope.launch {
try {
val item = menuItemsUseCases.getMenuItem(itemID)
state = state.copy(item = item)
showFallback = !isConnected.value && item.documentId.isEmpty()
} catch (e: Exception) {
Log.e("MenuDetailViewModel", "Error during onLaunch", e)
}
}
}
The getMenuItem function in MenuItemsUseCases interacts with Firestore to fetch a document associated with a menu item. This operation leverages the await() function to suspend execution until the asynchronous Firestore call completes, without blocking the thread.
override suspend fun getMenuItem(itemID: String): MenuItem {
try{
val snapshot = db
.collection("menu_items")
.document(itemID)
.get()
.await()
return snapshot.toObject<MenuItem>()?.copy(documentId = snapshot.id) ?: MenuItem()
} catch (e: Exception) {
Log.w(TAG, e.toString())
return MenuItem()
}
}
This use of multithreading provides UI responsiveness and lifecycle-aware resource management. Coroutines launched with viewModelScope are automatically canceled when the ViewModel is cleared, preventing memory leaks.
The MenuScreen class utilizes asynchronous programming via Future to fetch data without blocking the UI. This is evident in the _fetchMenuItems method, which determines whether to fetch menu items from Firestore (online) or from a local cache (offline) based on the device's connectivity status. The method uses the async/await keywords to handle asynchronous operations, such as checking connectivity with Connectivity().checkConnectivity() and retrieving data using repository methods like fetchMenuItemsByRestaurantId or getCachedMenuItems. This ensures smooth and efficient data fetching while keeping the UI responsive.
The class also employs a FutureBuilder to dynamically update the UI based on the state of the Future returned by _fetchMenuItems. While waiting for the data, a loading spinner (CircularProgressIndicator) is displayed. In the case of an error or no data, appropriate messages are shown. Once the data is successfully fetched, a list of menu items is displayed using a ListView.builder. The repository methods themselves, such as fetchMenuItemsByRestaurantId and getCachedMenuItems, handle Firestore and local cache operations asynchronously, making the data layer efficient and non-blocking.
Future<List<MenuItem>> _fetchMenuItems() async {
var connectivityResult = await Connectivity().checkConnectivity();
if (connectivityResult == ConnectivityResult.none) {
return MenuItemsRepository().getCachedMenuItems(restaurantID);
} else {
return MenuItemsRepository().fetchMenuItemsByRestaurantId(restaurantID);
}
}
For the caching strategy of this sprint we included the caching of the menu items that the user has seen. We see that in the MenuItemsRepositoryImpl
, when a restaurant's menu items are retrieved in the getMenuItems
method using the get
method in line 13 (implementation in the first image after the following code), firebase calls a get
method that takes a Source
type parameter which is an enumeration (second image after the implementation) that defines where the information will be retrieved from (DEFAULT
, SERVER
, CACHE
). Since we are not specifying a source in line 13, firebase uses DEFAULT
in which firebase tries to retrieve the information from the server but fallback on cache. When server returns information successfully, data is saved in cache.
class MenuItemsRepositoryImpl(
private val db: FirebaseFirestore
): MenuItemsRepository {
private val TAG = "FIREBASE_MENUITEMS"
override suspend fun getMenuItems(restaurantId: String): List<MenuItem> {
val menuItems = mutableListOf<MenuItem>()
try {
val snapshot = db
.collection("menu_items")
.whereEqualTo("restaurantId", restaurantId)
.get()
.await()
for (document in snapshot.documents) {
document.toObject<MenuItem>()?.let {
menuItems.add(
it.copy(documentId = document.id)
)
}
}
} catch (e: Exception) {
Log.w(TAG, e.toString())
}
return menuItems
}
}
The caching strategy in the Flutter app was implemented in the menu detail screen, where the menu item is displayed. When the user navigates to the menu detail screen, the app first checks if the menu item is already stored in the cache. If the item is found, it is displayed immediately, enhancing the user experience by reducing loading times. If the item is not cached, the app fetches the data from the server and stores it in the cache for future use.
This can be seen in the following code:
CachedNetworkImage(
imageUrl: menuItem.imageUrl,
fit: BoxFit.fill,
placeholder: (context, url) => const CircularProgressIndicator(),
errorWidget: (context, url, error) => const Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
Icons.error,
size: 50,
),
SizedBox(height: 8),
Text(
'Unable to load image (connect to internet)',
textAlign: TextAlign.center,
),
SizedBox(height: 16.0),
],
),
)
Which shows a Circular progress Indicator when the image is being loaded, and an error message when the image cannot be loaded.