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 often are users experiencing longer loading times when accessing the camera verification feature? It's type 1 because is related to the performance of the app, related to the time expected to access the certain feature.
-
What are the closest recycling places?: It’s type 2 because the user is going to interact with the map displayed on the app. It's a type 2 question because it directly enhances the user experience by facilitating interaction with the app's map to find nearby recycling locations.
-
What is the average time it takes for a user to scan an object and get the right recommendation?: It's 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 much time does the user spend on the the camera section (taking the picture and uploading it in the app)?: This is type 3 because it focuses on the functionality of taking the picture while recycling and knowing if it must be improved
-
How frequently does someone consult the recycle map section to locate the recycling points?: This is a type 3 question as the feature might be upgraded to be more appealing for the user.
- What is the most visited location to recycle on campus?: This is type 4 because it focuses on knowing which are the strategic locations on campus that students use more. By knowing this, the university can improve the location’s characteristics.
- Data Source Layer
The data source layer is the starting point where raw data is generated and collected from different platforms and devices. This layer aggregates data from several origins, such as Firebase, which stores real-time user information and activity logs, and Auth0, which provides authentication data for tracking login patterns, errors, and access methods. Google Maps and Apple Maps APIs collect geolocation data, enhancing map-related features by logging the locations visited by users. Additionally, data from sensors or wearables can be integrated, tracking environmental data (like temperature) or user actions (like steps taken). These sources ensure that the pipeline captures diverse data in both structured and unstructured forms, creating a solid foundation for analysis.
- Ingestion and Integration Layer
Once the data is collected, it is moved to the next layer using a combination of APIs and transfer mechanisms. REST APIs or Firebase REST APIs transmit real-time data, ensuring smooth integration between sources and storage. For larger datasets, SFTP transfers files in batch processes, often for historical analysis. In cases where instant data ingestion is required, message queues such as MQTT can facilitate the continuous flow of telemetry data, such as the number of app users online at any given time. The ingestion layer formats the data into a usable structure, so the subsequent layers can efficiently process it. It ensures the smooth handling of diverse data formats, ranging from authentication logs to sensor outputs.
- Storage Layer
In this layer, the ingested data is saved in structured and unstructured formats to ensure future accessibility. Google Cloud Storage stores bulk datasets, including logs and raw data that might need future processing or analysis. Firebase serves as a dynamic repository for real-time data, such as user interactions within the app. SQL and NoSQL databases hold more structured data sets, allowing for efficient querying and analytics. This layer acts as the central data repository, ensuring scalability and data security as the data volume grows. It provides the backbone for the pipeline, ensuring the stored data is organized and readily available for analytics.
- Processing / Computation Layer
The processing layer takes the raw or semi-processed data and transforms it into a structured format, ready for analysis. Tools such as Google DataPrep automate the data cleaning process, ensuring the removal of inconsistencies and preparing the data for analysis. Custom Python or SQL scripts aggregate data from multiple sources, compute key metrics, and merge datasets as required. Crashlytics is used to monitor app crashes and errors, helping identify bugs or performance bottlenecks. This layer can also incorporate machine learning models to predict user behaviors or optimize app features. Whether operating in batch mode or real-time streaming, this layer ensures data is preprocessed for quick insights and efficient querying in the next stage.
- Analysis Layer
This layer focuses on generating meaningful insights by applying analytics to the processed data. With Google BigQuery, data analysts can run complex queries to calculate averages, find trends, and analyze user patterns efficiently. For instance, the system can identify which times of day see the most user activity, helping developers manage resource allocation. Machine learning algorithms applied in this stage can identify patterns such as the most frequently used recycling points or predict future user behavior. The analysis layer is where raw data transforms into actionable insights that can be used to improve the app and user experience.
- Presentation Layer
The final layer presents the insights and results in a way that is understandable and actionable for stakeholders. Dashboards built in Power BI allow users to interact with the data visually, helping them monitor key metrics like user activity or recycling statistics in real-time. Excel offers more granular reporting, providing detailed breakdowns for specific use cases. Real-time dashboards and notifications provide immediate alerts when critical events occur, such as spikes in authentication errors or increased recycling activity. This layer ensures that the results of the analytics are effectively communicated to both administrators and users, supporting informed decision-making and continuous improvement.
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?
- What are the most common authentication errors users encounter when logging into the app?
- What is the average time it takes for a user to scan an object and get the recommendation?
- How frequently does someone 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.
Builder Pattern: It is used for the UI components built and configured in Flutter (such as using Padding, Text, and Column widgets). This resembles the builder pattern, where various components such as the mentioned before are added step by step.
Column(
children: [
const Padding(
padding: EdgeInsets.all(8.0),
child: Column(
children: [
Icon(Icons.keyboard_arrow_up, color: Colors.green),
Text(
"Find collection points near you",
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black),
),
],
),
),
Factory Method: The _buildGridButton() method acts like a factory, encapsulating the creation of grid buttons. Each button is created with a different label and icon, but the process of creation is unified.
Widget _buildGridButton(String label, IconData icon, Color color) {
return Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: InkWell(
onTap: () {},
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(icon, color: color, size: 40),
SizedBox(height: 10),
Text(label, style: TextStyle(fontFamily: 'Montserrat')),
],
),
],
),
),
),
);
}
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
}
}
Future Handling: In the Map view, the use of Future 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.
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.
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() ?? ""
}
- 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.
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.