Sprint 2 - ISIS3510-Team14/Wiki GitHub Wiki

First off, you will design your application's UI, going through everything you thought of during the design thinking phase. Afterwards, you will start working on your product backlog, developing features whilst applying the concepts shown in class about architecture, design and design patterns.
- How many users access the app by platform (Swift or Flutter) per hour of the day? This is a type 1 question because it analyzes the peaks of app usage by platform and hour, helping to manage service availability and resource allocation.
- What is the time the app takes to scan an object and show the recommendation? This is a type 2 question because it directly improves the user experience by measuring and optimizing the efficiency of the scanning process in the app.
- How many times do the users consult the recycle map section to locate the recycling points? This is a type 3 question because the feature might be upgraded to be more appealing or useful for the user.
-
What is the most visited location to recycle on campus? This is a type 4 question because it focuses on identifying the strategic recycling locations on campus that students use the most. This information can help improve these locations.
-
Which platform (iOS or Android) has been the most used overtime? This is is a type 4 question because it uses platform usage data (iOS vs Android) to inform strategic decisions about future resource allocation. By analyzing historical trends, the business can optimize development efforts, feature improvements, and support for the most-used platform, leading to more efficient resource utilization and potentially maximizing profit.
-
What are the most frequently recycled objects? This is a type 4 question because it focuses on understanding recycling patterns, which can help improve the placement of recycling bins and design educational initiatives based on recycling habits.
-
Data Source Layer The pipeline starts by collecting data from various sources, including Firebase for user data, Auth0 for authentication events, Google Maps and Apple Maps APIs for geolocation information, and potentially sensors or cameras to gather environmental or user-related data. These sources provide diverse types of structured and unstructured data, such as user actions, authentication errors, and location history, which are crucial for the app's sustainability-focused analytics.
-
Storage Layer After collection, the data is stored using Google Cloud, ensuring that both real-time and batch data are accessible for future analysis. The cloud storage solution can handle large-scale datasets, keeping logs and user data for later processing and reporting.
-
Processing Layer In this step, tools like n8n process the collected data. This layer applies transformations, cleaning, and aggregation to prepare the data for detailed analysis. Automated workflows ensure that data from different sources is uniformly structured, and any necessary calculations or processing take place before the analysis phase.
-
Analyze Layer Google BigQuery is used in this layer to perform deep analysis of the processed data. This includes tasks such as calculating averages, analyzing user behavior trends, and counting important events (like user engagement or map consultations). The analysis focuses on extracting actionable insights from the raw data collected, giving developers and stakeholders key metrics to work with.
-
Presentation Layer Finally, the insights derived from the analysis are presented in user-friendly formats using Power BI for interactive dashboards and Excel for detailed reports. This layer transforms the analyzed data into visuals and summaries, making it easier to understand the app's performance, user behavior, and recycling activities.
Each layer of this pipeline works together to ensure that data is efficiently collected, processed, analyzed, and presented, driving insights to improve both user experience and sustainability efforts.
In the end, we would have answered all these questions:
- How many users access the app by platform (Swift or Flutter) per hour of the day?
- Which platform (iOS or Android) has been the most used overtime?
- What is the average time the app takes to scan an object and show the recommendation?
- How many times do users consult the recycle map section to locate the recycling points?
- What is the most visited location to recycle on campus?
- What are the most frequently recycled objects?
What is clean architecture?
Clean Architecture is a widely adopted architectural pattern in software engineering, designed to organize code in a way that separates different concerns into distinct layers. It aims to structure software systems with a strong focus on maintainability, flexibility, and independence from external factors like frameworks or databases.
At the core of Clean Architecture is the concept of organizing code into concentric circles, with the innermost circles containing the business logic and domain objects, free from any dependencies on external systems. This separation allows the core logic to remain unaffected by changes in the outer layers, such as user interfaces, infrastructure, or databases. In other words, the business logic stands at the center, and any specific technologies or frameworks are kept on the outer circles, ensuring that the core of the application remains stable and adaptable.
One of the key benefits of Clean Architecture is the flexibility it provides in terms of technology. You can delay decisions about which frameworks, databases, or other tools to use until later in the development process, allowing you to focus on building the essential business rules and policies first. The outer layers, which handle the implementation details like the user interface or database access, are then built to support these core policies.
Clean Architecture divides a system into two major components: policies and details. The policies refer to the business rules and procedures that guide the system's behavior, while the details refer to the specific implementations needed to carry out these policies, such as database interactions or user interface code.
The structure is presented in the next diagram:
The SustainU app’s architecture is designed to be scalable, maintainable, and testable, with a clear separation of concerns. The MVVM pattern ensures that the UI logic is separated from business logic, while Clean Architecture guarantees modularity and ease of extension. The chosen technologies, such as Firebase and Google Maps API, enable efficient real-time data handling and location services, providing a seamless user experience in encouraging sustainable recycling habits on campus.
Architectural Layers
- Presentation Layer (UI/Widgets) This is the UI layer of the app, where the user interacts with Flutter widgets. The presentation layer includes both the visual components and the logic responsible for managing the UI state.
Views: These are the UI components that display information to the user and send user interactions to the ViewModel.
- sing_in_view: Handles user authentication using Auth0.
- map(view): Shows correct recycling practices.
- greenpoints(view): Displays nearby recycling points.
- verification_vew: Shows user profile information and recycling history.
- State Management (ViewModel): This layer is responsible for managing the state of each view. It listens to user input, performs business logic (via the domain layer), and updates the view accordingly. In MVVM, each ViewModel is connected to a specific View.
Each ViewModel performs specific tasks relevant to the corresponding view and interacts with the domain layer to fetch and manipulate data.
- Domain Layer The domain layer is responsible for implementing business logic and use cases for the app. This layer is independent of any specific framework, meaning it does not depend on the UI or data layers.
Use Cases: Each use case represents a specific action or function that the app performs. Use cases are typically implemented as classes that interact with repositories and manage core business rules.
- AuthenticateUser: Manages user login and authentication through Auth0.
- RecyclingPoints: Fetches location data from the repository using the Google Maps API.
- Data Layer This layer is responsible for accessing and managing the data. It includes repositories that abstract data operations and interact with APIs, local databases, or remote services.
Repositories: Repositories handle data retrieval and management, acting as an abstraction between the domain layer and the data sources. Each repository is responsible for interacting with a specific data source or API.
-
AuthRepository: Manages user authentication and session data via Auth0.
-
RecyclingRepository: Retrieves recycling data and classification from APIs.
-
MapRepository: Fetches locations of nearby recycling points using the Google Maps/Apple Maps API.
-
Data Sources: Each repository can interact with different data sources, such as:
-
- Firebase (Cloud Firestore): For user data and image storage.
-
- Google Cloud/Firebase Storage: For storing user data, logs, and images.
-
- Google Maps API: For fetching and displaying recycling points.
The repository pattern decouples the business logic from the data access logic, making the app easier to maintain and scale. Each repository abstracts data access and provides a single source of truth for the app, allowing for easier modification (e.g., replacing Firebase with a different backend).
This can be seen when it separates the UI (View) from the business logic (ViewModel). This can be seen in the way models, services, and UI components are structured in the lib directory.
Rationale for the organization:
-
Separation of Concerns: By structuring the project into different layers (core, data, domain, and presentation), each layer is responsible for a specific function (e.g., UI in the presentation layer, business logic in the domain layer). This makes the app easier to maintain, scale, and test.
-
MVVM and Clean Architecture: This structure aligns with the MVVM pattern, where each ViewModel is responsible for the logic of its corresponding view, and the domain layer handles the use cases that encapsulate business logic. Clean architecture principles are followed by separating the domain, data, and presentation layers.
-
Modularity and Reusability: Organizing reusable components such as widgets into their own folder encourages modularity. Additionally, separating services and repositories makes it easier to update and manage data sources or APIs in the future.
Command Pattern: Encapsulates a request as a independent object. This object contains all the necessary details to execute the request.
Instead of designing a unique view for each green location, a single execution method can be used across multiple locations. The execution method remains constant while information vary. This way, a single command can manage the construction of the view, and when adding new locations we do not require the developing of new views.
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => GreenPoints(
imagePath: point.img,
title: point.name,
description: point.description,
categories: const [
'Disposables',
'Non disposables',
'Organic',
],
),
),
Factory Method: To apply the Factory Method design pattern in the ProfileScreen, the creation of related objects (such as the profile creation logic) is encapsulated within a "Factory" class. This allows for creating different variations or types of profiles without changing the main logic of the component, improving code scalability and maintainability.
// ProfileFactory que crea el perfil
abstract class ProfileFactory {
Future<UserProfile> createProfile();
}
// Implementación concreta de ProfileFactory para crear un perfil desde StorageService
class StorageProfileFactory implements ProfileFactory {
final StorageService _storageService;
StorageProfileFactory(this._storageService);
@override
Future<UserProfile> createProfile() async {
Map<String, dynamic>? userProfile = await _storageService.getUserCredentials();
return UserProfile(
nickname: userProfile?['nickname'] ?? 'Unknown User',
email: userProfile?['email'] ?? 'Unknown Email',
profilePicture: userProfile?['picture'],
);
}
}
-
A ProfileFactory class is created with a method createProfile. This separates the logic for retrieving profile data from the UI code. As a result, if you decide to switch from StorageService to another service or data source in the future, you will only need to create a new implementation of ProfileFactory.
-
The ProfileScreen now doesn't know how the data is retrieved, it only knows that it can get a profile using the factory. This follows the Dependency Inversion Principle, enhancing code flexibility.
-
In the future, you need to support multiple types of profiles (e.g., profiles retrieved from an external API instead of StorageService), you can easily create.
Facade: The ChatService class acts as a facade to the underlying complexities of interacting with the chat API. It abstracts the details of how the requests are made, how responses are processed, and how different configurations or parameters may need to be set.
String trashString = trashTypes.map((e) => e.type).join(', ');
openaiAnswer = await ChatService().request("Answer for the image: Which of these types of trash is the user taking the picture holding?: $trashString. Answer only with the type", imageBase64);
appropriateBin = "Couldn't find an appropriate bin for this item";
for (TrashTypeIcon trash in trashTypes) {
if (openaiAnswer!.contains(trash.type)) {
foundTrashType = trash.type;
openaiAnswer = await ChatService().request("Answer for the image: What is the most appropriate bin to dispose of a $foundTrashType in?. Indicate if none of the present bins are appropriate", imageBase64);
appropriateBin = openaiAnswer!;
break; // Exit loop after the first match
}
}
Singleton Pattern: Ensures that a class has only one instance and provides a global point of access to it. This pattern is useful when only one instance of a class is needed throughout the lifecycle of the application.
In this case, the LoginViewModel
is designed using the Singleton Pattern to manage user authentication consistently across different views. By implementing the Singleton Pattern, we guarantee that there is a single source of truth for authentication, avoiding duplication and synchronization issues when multiple views need to access the same authentication state.
The code snippet below shows how the Singleton is implemented in the LoginViewModel
class:
class LoginViewModel: ObservableObject {
// Singleton instance
static let shared = LoginViewModel()
@Published var isAuthenticated: Bool = false
@Published var userProfile: Profile = .empty
// Private initializer to prevent creating other instances
private init() {}
func authenticate() {
Auth0
.webAuth()
.start { result in
switch result {
case .failure(let error):
print("Failed with: \(error)")
case .success(let credentials):
self.isAuthenticated = true
self.userProfile = Profile.from(credentials.idToken)
print("User profile: \(self.userProfile)")
}
}
}
}
### Tactics
**Future Handling:** In the Map view, the use of Future<void> for asynchronous operations such as fetching location updates, reflects a common tactic in Flutter for handling asynchronous tasks. Such as the making customMarkers in the map.
```dart
Future<void> customMarker() async {
BitmapDescriptor.asset(
const ImageConfiguration(),
"assets/ic_pick.png",
width: 40,
height: 40,
).then((icon) {
setState(() {
customIcon = icon;
setMarkers();
});
});
}
Separation of Concerns: This tactic divides different responsibilities into distinct modules. For example, the HomeScreen focuses on the UI, while the widgets like HeaderWidget and BottomNavBar are separated into their own files to handle specific tasks.
import '../widgets/head.dart';
import '../widgets/bottom_navbar.dart';
Data Validation: This tactic ensures that user inputs are correctly processed, improving system reliability. In the SignInScreen, validation for text input ensures data correctness.
TextFormField(
validator: (value) {
if (value.isEmpty) {
return 'Please enter your username';
}
},
)
Error Handling: The code includes error handling in the request method of ChatService, as well as in the FutureBuilder to manage API failures gracefully. This ensures that the application can respond appropriately to errors without crashing.
try {
Message message = Message(
role: "user",
content: [
{"type": "text", "text": prompt},
{
"type": "image_url",
"image_url": {"url": "data:image/png;base64,$photoBase64"},
},
],
);
ChatRequest request = ChatRequest(maxTokens: 50, messages: [message]);
if (prompt.isEmpty) {
return null;
}
http.Response response = await http.post(
chatUri,
headers: headers,
body: request.toJson(),
);
if (response.statusCode == 200) {
var responseBody = jsonDecode(response.body);
var result = responseBody['choices'][0]['message']['content'];
return result;
} else {
return null;
}
} catch (e) {
print("an error was found: $e");
}
In this diagram the different components that make up the application are listed and connected to understand the flow and relationships. In first place we have the Authentication interface that integrates with Auth0 for secure user login and management. The GreenPoints interface connects with Google Maps to provide users with real-time information about nearby recycling points. The Scan interface interacts with ChatGPT to assist users by offering educational guidance on the correct recycling bin. Additionally, the ScoreBoard interface handles app’s gamification features, tracking user progress. On the backend, the DataBase includes both GCP and Firebase, which store user data, recycling information, and other real-time data necessary for the app’s functioning.
This diagram provides a clear overview of the SustainU app's deployment architecture. It shows that the Android app communicates with the fastAPI backend, which, in turn, interacts with the PostgreSQL database. Each block has specific roles, ensuring the app runs efficiently for users interested in recycling, with core features such as user management, point tracking, recycling information, and location-based services.
What is the MVVM architecture in iOS?
Model-View-ViewModel (MVVM) is a design pattern widely used in iOS app development to create clean, maintainable, and testable code. MVVM separates an app’s user interface (View) from the underlying data (Model) and introduces an intermediary component called ViewModel to manage the presentation logic. Its main parts are:
-
View: It only performs things related to UI — Show/get information. Part of the view layer. It includes UI components such as buttons, labels, text fields, and views.
-
View Model: It receives information from VC, handles all this information, and sends it back to VC.
-
Model: This is only your model, nothing much here. It’s the same model as in MVC. It is used by VM and updates whenever VM sends new updates
The following components diagram shows an overview of the Swift architecture and its use of the MVVM architectural pattern. The top layer consists of various Views for different app functions, each paired with a corresponding ViewModel in the layer below. These ViewModels interact with a Services layer, which includes specialized services for authentication, camera operations, and location tracking, as well as integration with a ChatGPT API. The Services layer mediates between the ViewModels and the Model layer, which contains data structures for different app features. At the bottom, Firebase/Firestore serves as the data persistence layer, connected to all Models.
On the other hand, the deployment pattern shows provides a clear overview of the SustainU app's deployment architecture. It shows that the iOS app communicates with the fastAPI backend, which, in turn, interacts with the PostgreSQL database which will be in GCP. Each block has specific roles, ensuring the app runs efficiently for users interested in recycling, with core features such as user management, point tracking, recycling information, and location-based services.
Using MVVM is the best decision for SWIFT as it separates SustainU's app into 3 base layers responsible for models, business logic and views which are always part of each iOS app. And also to make them independent from each other and not tightly coupled but complementing mutually. Having them prepared using MVVM pattern make them also reusable in more than only one place and testable independently what affects on better code stability and quality.
Singleton is a creational design pattern that lets you ensure that a class has only one instance, while providing a global access point to this instance. We decided to apply it on The RequestService class in RequestService.swift as it is a common pattern for amanaging API requests.
class RequestService {
static let shared = RequestService()
let apiEndpoint = APIConfig.apiEndpoint
let apiKey = APIConfig.apiKey
private init() {}
}
Observer is a behavioral design pattern that lets you define a subscription mechanism to notify multiple objects about any events that happen to the object they’re observing. This is implemented through SwiftUI's @State and @Binding property wrappers, which allow views to react to changes in data.
The Facade pattern is a structural design pattern that provides a simplified interface to a library, a framework, or any other complex set of classes. We decided to use it in the Auth0.webAuth() method in LoginView.swift as it acts as a facade, simplifying the complex authentication process. For example, in LoginView.swift the use of Auth0.webAuth().start { result in ... }, acts as the Facade to the underlying authentication process that has many more steps (edirecting the user to a login page, handling OAuth protocols, token management)
func authenticate() {
Auth0
.webAuth()
.start { result in
switch result {
case .failure(let error):
print("Failed with: \(error)")
case .success(let credentials):
self.isAuthenticated = true
self.userProfile = Profile.from(credentials.idToken)
print("User profile: \(self.userProfile)")
}
}
The Factory Method is a creational design pattern that provides an interface for creating objects in a superclass, but allows subclasses to alter the type of objects that will be created. We decided to use it on the Profile as Profile.from(_ idToken: String) static method creating a Profile instance from a JWT token.
static func from(_ idToken: String) -> Self {
guard
let jwt = try? decode(jwt: idToken),
let id = jwt.subject,
let name = jwt.claim(name: "name").string,
let email = jwt.claim(name: "email").string,
let emailVerified = jwt.claim(name: "email_verified").boolean,
let picture = jwt.claim(name: "picture").string,
let updatedAt = jwt.claim(name: "updated_at").string
else {
return .empty
}
The Builder is a creational design pattern that constructs complex objects step by step. Naturally, in the case of Swift, its syntax for building UIs leverages the builder pattern. The body property within views constructs the UI by combining and configuring various view elements. For example, in the HomeView.swift:
var body: some View {
TabView {
// Home Tab
NavigationView {
VStack {
// Logo e imagen de perfil
HStack {
Image("logoBigger") // Logo personalizado
.resizable()
.frame(width: 50, height: 50)
Spacer()
AsyncImage(url: URL(string: userProfile.picture)) { image in
image
.resizable()
.frame(width: 40, height: 40)
.clipShape(Circle())
} placeholder: {
Image(systemName: "person.circle.fill")
.resizable()
.frame(width: 40, height: 40)
.clipShape(Circle())
}
}
}
}
}
}
- Optional handling: In Swift, we implemented optional handling to manage cases where data might be missing. In CameraView.swift, if let uiImage = info[.originalImage] as? UIImage checks if an image was actually captured before proceeding. This works similar to exception handling.
swift
if let uiImage = info[.originalImage] as? UIImage { parent.image = uiImage
// Procesar la imagen aquí (base64 y requests)
func convertImageToBase64String(img: UIImage) -> String {
return img.jpegData(compressionQuality: 1)?.base64EncodedString() ?? ""
}
2. Handling of asynchronous requests: On the camera view, we used DispatchGroup to manage multiple asynchronous API requests. dispatchGroup.enter(), dispatchGroup.leave(), and dispatchGroup.notify are used to track the completion of each request and execute code after all requests finish. So it completes correctly a request before doing another.
```swift
let dispatchGroup = DispatchGroup()
// Tipo de basura
let promptTrashType = "Answer for the image: Which of these types of trash is the user taking the picture holding?: \(trashTypesString). Answer only with the type."
// Primer request
dispatchGroup.enter()
RequestService().sendRequest(prompt: promptTrashType, photoBase64: photoBase64) { response in
DispatchQueue.main.async {
responseTextTrash = response ?? "No response"
dispatchGroup.leave()
}
}
This are the implemented features for both apps:
- An interactive map that highlights the locations of recycling bins and other waste disposal points across the campus.
- Users can find the nearest recycling bin, promoting convenience and accessibility.
- An interactive GUI that allows the user to log in and log out of the app using Auth0.
- Image recognition feature that uses the phone’s camera to verify that users are properly recycling.
- Users must scan their recycling actions, and points are only awarded after successful verification, ensuring accountability.