Sprint 2 - ISIS3510-202620-Team14/Wiki GitHub Wiki
Mobile app (Kotlin and Flutter clients) that helps teachers in under-resourced rural schools track the learning progress of their students. This wiki contains the Sprint 2 deliverables: business questions, analytics pipeline, architectural design, implemented features, implementation links, ethics video, milestones and contributions.
- Micro Sprints
- 1. Business Questions
- 2. Analytics Pipeline
- 3. Architectural Design
- 4. Functionalities and Features
- 5. Implementation
- 6. Ethics Video
- 7. Milestones
- 8. Contributions
| Micro Sprint | Page | Dates |
|---|---|---|
| MS6 | MS6 | September 12th, 2026 |
| MS7 | MS7 | September 19th, 2026 |
Every member implemented at least one business question (BQ), and all of them come from the list submitted in the Sprint 1 deliverable. Each BQ states its type, who implemented it, a short rationale and the view of the app where its answer can be seen.
| BQ # | Business Question | Type | Rationale | View where it is implemented | Contributor |
|---|---|---|---|---|---|
| BQ #2 | Each week, what percentage of grouping sessions created on a device are not synced with the backend within 24 hours (or end in an error), by platform (Kotlin/Flutter), app version and connectivity at creation (online/offline)? | Type 1 | Teachers work in rural schools with intermittent connectivity, so grouping sessions are created offline and uploaded later. This metric shows the admin how reliable that offline-first sync is and whether failures concentrate on a platform, an app version or offline creation, so they can be fixed. | Teacher: create a group registers the session locally and uploads it in the background. Admin: Home, sync report (weekly share, with a breakdown by platform, version and connectivity). | Sebastian Rodriguez |
| BQ #4 | What percentage of teachers use the activity library versus creating their own activities? | Type 2 | Shows the share of a teacher's planned activities from the library and from their own creations. The current measure counts activities, not teachers. | Activities view, usage card | Samuel Charry |
| BQ #6 | How many times has the teacher opened the app this week? | Type 2 | Shows each teacher how many times they opened the app this week and reminds them to come back while they are below two opens, turning a habit metric into feedback they see in daily use. | Today tab of the teacher home screen (weekly usage card) | Santiago Quiroz |
| BQ #9 | How long does it take to classify 25 students? | Type 3 | It shows us the time that each teacher spends on average classifying students within an institution, and so we can see if the classification helps the process or slows it down, rather than replacing or improving it. | In the "Home" view of the app, the teacher will first see different cards, which are the student classification module. In each card, it shows a complete list of the institution's students and the process of classifying them. | Mateo Cante |
| BQ #11 | What is the most-used activity for kids? | Type 4 | Analyzes which activities children favor the most, so non-profits and institutions can use that information to focus their resources on the activities that work. | It can be seen through the admin report in the (TopActivitiesView) in flutter specifically. | Isabela Mantilla |
| BQ #14 | Based on a teacher's subject and class size, which grouping method (automatic or manual) is most likely to be adopted, so the app can pre-select the recommended option and increase feature adoption? | Type 5 | Tracks which grouping method teachers actually choose given their subject and class size, so the app can pre-select the most likely option and reduce friction in the group creation flow. | Group creation dialog in the Groups tab: the method selector shows a "Recomendado" badge on the predicted option with a confidence percentage. | Juan Triviño |
Short working summary of the types used in this wiki (replace with the course definitions if they differ):
- Type 1: internal telemetry (stability, performance, usage) meant for the development team.
- Type 2: direct user experience improvement. The answer is presented to the user inside the app, and the question can be asked for a single user in their own context.
- Type 3: product decisions about features (keep, improve, simplify or remove).
- Type 4: insights aimed at third parties.
- Type 5: smart feature driven by collected data. The app uses historical records to make a prediction or recommendation that changes its own behavior for the user, adapting the interface automatically instead of just displaying a metric.
Question: Each week, what percentage of grouping sessions created on a device are not synced with the backend within 24 hours (or end in an error), by platform (Kotlin/Flutter), app version and connectivity at creation (online/offline)?
Why this is a Type 1 question: The answer cannot be calculated on a single phone. A session is created on the teacher's device, and it only counts as late when the backend receives it more than 24 hours after it was created. So the metric needs the sessions of all teachers in one place, and it is read by the admin, not by the teacher who created them.
Expected usage of the answer: ENAd works in rural schools where connectivity is intermittent, so grouping sessions are often created offline and uploaded later. The admin and the team use the weekly percentage to see whether offline-first sync is reliable. They also use the breakdown to see whether late or failed sessions concentrate on one platform, one app version or sessions created offline, and fix that first.
The current report is available only to admins and starts from the first week with data. The current week is marked as provisional because its sessions still have time to sync. The week starts on Monday in Colombia time. A session counts as late when the time between its creation on the device and its arrival at the backend is more than 24 hours. A session the backend rejected counts as an error.
The implementation is in GruposViewModel, SesionAgrupacionRepository, SyncWorker, ReporteSincronizacionViewModel, ReporteSincronizacion, and the backend function agrupaciones.
Question: What percentage of teachers use the activity library versus creating their own activities?
This is a Type 2 question because the answer appears in the teacher's Activities view. The teacher can see which source they use more often.
The answer helps the teacher choose the next activity. It can also show the team whether the library is useful.
The current view counts planned activities for the signed-in teacher. It does not calculate the percentage of teachers across the app. The period is the full event history available to the app.
The implementation is in ActivityCreator, AnalyticsService, ActivitiesViewModel, and ActivitiesView.
Question: How many times has the teacher opened the app this week?
Why this is a Type 2 question:
- It focuses on one teacher and their own use of the app.
- The answer is shown to that teacher inside the app, in the weekly usage card of the Today tab, and not only to the team.
- It relies on measurable internal data: the app opens recorded since Monday of the current week, ignoring opens that happen less than five minutes after the previous one.
- The count is read from the local database, so it is available even without connectivity.
Expected usage of the answer: The teacher sees how close they are to the weekly goal of two opens. While they are below it, the app reminds them to come back and keep their students' progress up to date. The team-level count (how many teachers opened the app more than once in a week) is available as an admin report and complements this question, but it is not the question itself.
Question: How long does it take to classify 25 students?
**Why this is a Type 3 question:**It is about how teachers actually use a specific feature of the app, the student classification module, rather than about app performance (Type 1) or information shown back to the teacher to support a daily decision (Type 2). The answer comes from telemetry collected silently while teachers classify. It is used by the ENAd team to judge whether the feature is worth keeping as it is, improving, or redesigning.
Expected usage of the answer: For each institution, the admin report shows the average minutes each teacher needs to classify 25 students, plus the per-teacher breakdown. The foundation can compare this time with the paper-based process the app replaces. If it is shorter or similar, the module helps the process. If it is longer, the module slows teachers down and the team should simplify it (for example fewer taps per student, or bulk classification). Large differences between teachers or institutions point to where extra training or connectivity support is needed.
Question: What is the most-used activity for kids?
Why this is a Type 4 question: It is a type 4 question since finding out the most used activity for kids is a piece of data that can be collected through multiple users, in which it can benefit outsiders. In this case, this activity will indicate the most effective or liked activity by children, making non-profits, education institutions, etc. able to use their resources better and focus them on effective activities, or use it as a case study.
Expected usage of the answer: Through the admin report, any user with administrator access can see the top activity ranked across all teachers. Educational institutions can use this to focus their resources on the activities that truly work for students, so funding goes toward activities with a real impact instead of being spread evenly across all of them.
Question: Based on a teacher's subject and class size, which grouping method (automatic or manual) is most likely to be adopted, so the app can pre-select the recommended option and increase feature adoption?
Why this is a Type 5 question:
- The answer is not a number displayed to the user or a metric for the team: it is a prediction that changes the app's default behavior.
- Every time a teacher creates a group, the app records the chosen method, subject and class size in Firestore (
groupingEvents). When the next teacher opens the group creation dialog, the service fetches those historical events, filters by the same subject and a similar class size, computes the automatic-vs-manual ratio, and pre-selects the majority method. - The recommendation appears as a "Recomendado · 75 %" badge in the
GroupingMethodSelectorwidget, so the interface adapts to real usage patterns instead of always defaulting to the same option. - The teacher can still override the pre-selection; the choice is recorded and feeds future recommendations.
Expected usage of the answer: When a teacher opens the group creation dialog, GroupingAnalyticsService.recommend() is called with the current subject and class size. If there are at least three similar past events, the widget pre-selects the recommended method and shows the confidence badge. Over time, as more events accumulate, the recommendation becomes more accurate. This reduces the number of taps for teachers who follow the majority pattern and increases adoption of the grouping feature.
The pipeline follows the offline-first design of the app: data is written on the device first and travels to the cloud when there is connectivity. It is organized in six stages, and section 2.7 shows how each BQ goes through them.
Better visualization: Analytics Pipeline Diagram
Rationale: the target schools have intermittent 4G and Wi-Fi, so the pipeline never depends on a live connection: every record is stored in the local SQLite database with a pending status and a sync worker uploads it later. The backend is serverless (Firebase), which avoids running our own servers, and both clients (Kotlin and Flutter) use the same endpoints, so the data of both platforms ends up in the same collections and can be compared through a platform field.
Teacher interactions captured by the clients, each one tied to the verified teacher account:
- Grouping sessions (BQ #2): created by a teacher, with the subject, group name, students and teachers counted, and the connectivity at creation time.
- App opens (BQ #6): one event each time the teacher home screen starts, ignoring opens less than 5 minutes after the previous one.
- Classification sessions (BQ #9): one timed pass of the teacher through the student list of an institution, counting only foreground time.
- Activity planning events (BQ #4 and BQ #11): each time a teacher plans an activity, from the library or created by themselves, with its title and subject. BQ #11 reuses the events of BQ #4.
- Grouping method choices (BQ #14): the method (automatic or manual) chosen when a group is created, with the subject and the class size.
The two clients ingest in different ways:
-
Kotlin, with a local outbox (BQ #2, #6 and #14): each event is written first to Room with a
PENDINGsync status. When a row is saved, the repository enqueues a WorkManager job that only runs with network, andSyncWorkeruploads the pending rows of every repository (app opens in batches of 100). Transient errors (HTTP 5xx, 429, 408, 401 or no connection) leave the rowsPENDINGand WorkManager retries, up to 8 attempts per round. Errors that will not fix themselves on retry mark the rowFAILED. Nothing is lost when the connection drops. - Kotlin, with a one-off job (BQ #9): when the teacher finishes a pass, the app builds the session JSON and a WorkManager job sends it when there is network, with up to 8 retries. There is no Room table for this one.
- Activity events (BQ #4 and #11): both clients write the event straight to Firestore, with no local outbox.
- Flutter (BQ #14): the Flutter client has no local database or sync worker. The event is sent with a plain request and, if it fails, the error is ignored and the event is not retried.
BQ #2, #6, #9 and #14 send JSON over HTTPS to Cloud Functions endpoints (POST /groupings, POST /appOpens, POST /classificationSessions and POST /analytics/grouping-events), with the Firebase ID token of the user as bearer. The autenticar middleware verifies the token, the account status and the role, and then the function validates the payload and maps it to a Firestore document. The uid is taken from the verified token, not from the body. In the grouping sessions, app opens and classification sessions, the record carries an id generated on the device (clientId) that is used as the Firestore document id, so a retried upload does not create duplicates. The grouping-method events do not use it yet, so a retry could register the same event twice. Most records also carry platform (kotlin or flutter) and the app version. For grouping sessions the backend adds the week (Monday, Colombia time), the sync delay and whether the session arrived within 24 hours; for classification sessions it adds the seconds per 25 students.
The activity events (BQ #4 and #11) do not go through a Cloud Function: the clients write them directly with the Firestore SDK, and the Firestore rules only allow creating and reading one's own events.
Cloud Firestore is the central store, with one collection per type of data: groupingSessions (BQ #2), app_opens (BQ #6), classificationSessions (BQ #9), events (BQ #4 and #11) and groupingEvents (BQ #14). On the Kotlin device, SQLite (Room) works as a local buffer and as the source for reads that must work offline, such as the weekly count of app opens.
Computation happens in two places:
-
On the device, for answers about a single user or a recommendation for the screen the teacher is in. BQ #6 is a
COUNT(*)of the teacher's opens in the current week over Room, which works offline. BQ #4 counts the teacher's activities by source and calculates the percentage of library and own activities. BQ #14 downloads the last 200 grouping events of the subject, keeps the ones with a similar class size, takes the most chosen method as the recommendation and uses its share as confidence; with fewer than 3 events it is not shown. -
On the backend, for team-level answers restricted to administrators.
GET /groupings/reportreturns, per week, platform, app version and connectivity, the total of grouping sessions and how many arrived on time, late or with an error (BQ #2).GET /appOpens/reportreturns how many teachers opened the app and how many opened it more than once in a week (BQ #6).GET /classificationSessions/reportreturns the minutes needed to classify 25 students, weighted by the number of students, with each institution as the average of its teachers over the last 8 weeks (BQ #9).GET /analytics/top-activitiesgroups the planned activities by title and returns the count and the share of the total, from most to least used (BQ #11).
Type 2 answers are shown inside the app, as cards in the screen where the teacher already works: the weekly usage card of BQ #6 (opens out of the goal of two, with a reminder while the teacher is below it) and the usage card of the Activities view for BQ #4. BQ #14 shows a "Recomendado" badge with the confidence percentage on the predicted method in the create group screen. Aggregated answers for the team are shown in the admin Reports tab, which has a chip for each report: the synchronization of grouping sessions (BQ #2, one card per week with the total and the percentage that did not arrive within 24 hours) and the classification time (BQ #9, average per institution and per teacher). The ranking of most-used activities (BQ #11) is shown in a Flutter screen. The team report of app opens is available in the backend but does not have a screen yet.
| BQ # | Business Question | Type | Transformation / Queries | Implemented visualization | Contributor |
|---|---|---|---|---|---|
| BQ #2 | Each week, what percentage of grouping sessions created on a device are not synced with the backend within 24 hours (or end in an error), by platform (Kotlin/Flutter), app version and connectivity at creation (online/offline)? | Type 1 | Each session is stored in Room with a pending status and uploaded by the sync worker through "POST /groupings". The backend stores one document per session in "groupingSessions". The delay is the time the device took to send it (sent minus created, both read from the device clock), and the week starts on Monday in Colombia time. A session counts as late when the delay is over 24 h and as an error when the backend rejects it. "GET /groupings/report" (admin only) reads the documents of a range of weeks (the last 8 by default) and groups them by week, and by week, platform, app version and connectivity. For each group it computes (late + errors) / total. The current week is marked as provisional. | Admin Reports tab, "Sincronización de agrupaciones": one card per week with the percentage, "X of Y did not arrive on time" and the on-time / late / error split, plus a breakdown by platform, version and creation connectivity. | Sebastian Rodriguez |
| BQ #4 | What share of planned activities comes from the library or the teacher? | Type 2 | Count activity_selected events for the signed-in teacher by source (library or custom). Divide each count by the total. |
Activities view usage card with counts and percentages | Samuel Charry |
| BQ #6 | How many times has the teacher opened the app this week? | Type 2 | Each open is stored locally with the Monday date of its week and a sync status. The count of the current week is a local query. Pending opens are uploaded to the appOpens Cloud Function and stored in Firestore (app_opens). The team-level count of teachers with more than one open comes from the admin report endpoint of the same function. |
Weekly usage card in the Today tab: opens out of the goal of two, plus a reminder while the teacher is below it. | Santiago Quiroz |
| BQ #9 | How long does it take to clasify 25 students? | Type 3 | The classification screen times how long a teacher takes to classify students (foreground time only, until the last classification). Each session is queued with WorkManager and sent to "POST /classificationSessions", which validates the teacher's school and stores it in "classificationSessions" (keyed by clientId, so retries don't duplicate). "GET /classificationSessions/report" (admin only) computes, per teacher, active time / students classified × 25 in minutes, and averages the teachers of each institution. | Admin Reports tab, "Clasificación": one card per institution with the average minutes per 25 students, plus one row per teacher. Filter by subject. | Mateo Cante |
| BQ #11 | What is most used activity for kids? | Type 4 | Each activity that is selected by the teacher is stored directly on Firestore, on the events collection. Then it is computed in the backend (an admin-only endpoint, GET /analytics/top-activities), and the information is received and directly shown in the admin view. | The visualization is TopActivitiesView, a ranked list of activities with their count and percentage. | Isabela Mantilla |
| BQ #14 | Based on a teacher's subject and class size, which grouping method (automatic or manual) is most likely to be adopted, so the app can pre-select the recommended option and increase feature adoption? | Type 5 | Each group creation fires POST /analytics/grouping-events with subject, classSize and method. The service also keeps events in a local in-memory list. On the next group creation, GET /analytics/grouping-events?subject=X fetches up to 200 remote events; recommend() merges them with the local list, filters by subject and class-size tolerance (±3 if ≤10, ±5 if ≤25, ±8 if >25), counts the automatic-vs-manual ratio, and returns the majority method with its confidence. |
GroupingMethodSelector widget inside CreateGroupDialog: the recommended method gets a green "Recomendado · 75 %" badge when at least 3 similar events exist. The method is pre-selected; the teacher can override it. |
Juan Triviño |
When a teacher creates a group in the Groups tab, "GruposViewModel.agregarGrupo()" saves the group and calls "SesionAgrupacionRepository.registrar()". The repository writes the grouping session to Room with a "PENDING" status, the platform, the app version, the connectivity at that moment ("online" or "offline") and the creation time of the device. Then it asks "SyncScheduler" to enqueue a "SyncWorker" that runs when the network is available.
"SyncWorker" calls "SesionAgrupacionRepository.sincronizarPendientes()". For each pending session, "CloudFunctionsApi.enviarSesionAgrupacion()" sends a "POST /groupings" with the Firebase ID token, the session id as "clientId", the creation time, the sending time and the attempt number. If the request fails because of the network or a temporary server error, the session stays "PENDING" and the worker retries. If the backend rejects it, the session is marked "FAILED".
The Cloud Function validates the Bearer token through the "autenticar" middleware and the payload. It stores one document per session in the Firestore collection "groupingSessions", with the week (Monday, Colombia time), the sync delay (sending time minus creation time, both from the device clock) and "syncedWithin24h". A session that fails validation is also stored, with the status "error", so it counts as an error in the report. A retried upload with the same "clientId" does not create a duplicate.
When an admin opens the Reports tab, "ReporteSincronizacionViewModel" calls "CloudFunctionsApi.reporteSesionesAgrupacion()", which sends "GET /groupings/report". The function is admin-only. It reads the sessions of the last 8 weeks, groups them by week and by week, platform, app version and connectivity, and computes "(late + errors) / total" for each group. "ReporteSincronizacionSection" shows one card per week with the percentage and the breakdown. The current week is marked as provisional.
This Kotlin path works offline: the session is saved locally first and the upload can happen days later, which is exactly what the metric measures. The report needs a connection because it is read from the backend.
When the teacher plans an activity, ActivityCreator records an activity_selected event. Its source is library or custom. AnalyticsService saves the event in Firestore with the teacher ID and also sends it to Firebase Analytics.
ActivitiesViewModel asks Firestore for both counts for the signed-in teacher. It divides each count by the total and shows the rounded percentages in the Activities view. When the total is zero, both percentages are zero.
This Flutter path needs a connection to write and read Firestore events. It does not use a local outbox.
When a teacher selects an activity in the Activities view, the information is directly stored in the events collection in Firestore, through the AnalyticsService path. In this, we have both the data source and the data ingestion.
Then we follow the data computation through the GET /analytics/top-activities endpoint, which reads the stored activity events and aggregates them because this step only runs for admin users, since it needs data from every teacher, not just one. Finally, TopActivitiesView visualizes the result as a ranked list.
The analytics pipeline is implemented in the BQ #11 files: activities_view.dart, activity_creator.dart, and analytics_service.dart (source, ingestion, and storage in Firestore's events collection); analitica/index.js (GET /top-activities, computation, admin-only); and top_activities_service.dart, top_activities_viewmodel.dart, and top_activities_view.dart (visualization).
When a teacher creates a group in the Groups tab, GruposViewModel.createGroupOnBackend() calls analyticsService.trackGroupingEvent() with the subject, class size and chosen method. The service saves the event in a local in-memory list and sends a POST /analytics/grouping-events to the backend (fire-and-forget: if the request fails, the event stays in the local list).
The Cloud Function validates the Bearer token through the autenticar middleware, checks that subject, classSize and method are valid, and writes the event to the Firestore collection groupingEvents.
When the next teacher opens the group creation dialog, CreateGroupDialog calls GroupingAnalyticsService.recommend() with the current subject and class size. The service fetches up to 200 remote events with GET /analytics/grouping-events?subject=X, merges them with the local list, filters by class-size tolerance, computes the automatic-vs-manual ratio, and returns the majority method with its confidence.
The GroupingMethodSelector widget pre-selects the recommended method and shows a green "Recomendado · 75 %" badge when at least three similar events exist. If there are fewer, no badge is shown and the default is automatic.
This Flutter path keeps events in memory and in Firestore. It does not use a local database or background sync.
The architecture of the system is described with several diagrams. Each diagram comes with the rationale of the decisions it shows and why they were made.
| Layer | Kotlin client | Flutter client |
|---|---|---|
| User interface | Jetpack Compose screens | Flutter widgets |
| State and presentation logic | ViewModel with StateFlow | ViewModel with ChangeNotifier |
| Local storage | Room over SQLite | In-memory demonstration state; analytics events in Firestore |
| Background sync | WorkManager | Not implemented in Flutter yet |
Both clients share the same backend on Google Cloud (Firebase project enad-movil, region us-central1): Firebase Authentication (email and password), Cloud Firestore, and Cloud Functions (2nd generation, Node.js 22 with Express) exposing the REST endpoints used by the apps.
This is a Diagram that describes the technology in a graphic way
Better visualization: Overal Diagram
Better visualization: Deployment Diagram
Rationale: the system has two clients that share one Firebase backend. Both run the same MVVM structure and keep a local SQLite database, because the target schools have intermittent connectivity: every write lands in the device first and waits in the outbox until the sync worker can reach Cloud Functions. Authentication goes through Firebase Authentication and its ID token is sent as bearer to Cloud Functions, which validate it before touching Firestore. Using Firebase managed services keeps the operational load of the team low, and having two clients on the same endpoints avoids duplicating business logic on the server.
Better visualization: Domain Model
Rationale: a Region contains Schools, and each School has Students and Users (teachers and administrators). A User leads StudentGroups, each one for a subject (math or reading) with a target level, and a Student can belong to at most one group per subject. Evaluations (baseline, midline, endline or specific) link a Student, a User and a level taken from the math or reading level scales, and the current level of the student is derived from them. AttendanceRecord stores presence per student and day, and SessionDay stores the planned and actual hours logged by a User. Keeping groups, evaluations and attendance as separate entities lets each feature evolve without changing the others.
The app has two independent clients (Kotlin and Flutter) that share one Firebase backend, so there is no single general architecture. This section documents the architectural patterns and the component structure of each client. The design patterns and tactics are documented in section 3.4.
Better visualization: Architectural Patterns Diagram - Kotlin
Rationale: the Kotlin client follows MVVM as its structure: Jetpack Compose screens observe a UI state exposed by a ViewModel, and the ViewModel reaches the data through repositories. On top of it, the client is offline-first with an outbox: every write lands in Room first and the sync worker uploads it to the serverless Cloud Functions backend when there is connectivity. Both choices fit rural schools with intermittent 4G and Wi-Fi, because the teacher can keep working without a connection and nothing is lost when the network drops.
| Pattern | Where it appears | Responsible |
|---|---|---|
| MVVM | Composable screen --> ViewModel (StateFlow of a UI state) --> repository --> Room / Cloud Functions |
All team members |
| Offline-first with outbox | Room first (syncStatus = PENDING), SyncWorker uploads pending rows through the repositories |
Santiago Quiroz |
The diagrams below use the app opens feature (BQ #6) as the example.
The screen only observes a UI state exposed by its ViewModel, and the ViewModel asks the repository for data, so the UI never touches storage or the network. Every member applies it in the screens and ViewModels of their own features.
classDiagram
direction LR
class RegistrarAperturaEffect {
<<Composable>>
ON_START → registrar()
}
class TeacherHomeScreen {
<<Composable>>
viewModel() + collectAsStateWithLifecycle()
}
class AperturasSemanaCard {
<<Composable>>
+AperturasSemanaCard(state)
}
class AperturasViewModel {
<<AndroidViewModel>>
-repo: AperturasRepository
+uiState: StateFlow~AperturasUiState~
}
class AperturasUiState {
<<data class>>
+aperturas: Int
+meta: Int = 2
+mostrarRecordatorio: Boolean
}
class AperturasRepository {
<<Model>>
+registrar()
+observarSemana() Flow~Int~
+sincronizarPendientes() Boolean
}
TeacherHomeScreen --> AperturasViewModel : observes uiState
TeacherHomeScreen --> AperturasSemanaCard : passes state
AperturasViewModel --> AperturasUiState : exposes
AperturasViewModel --> AperturasRepository : observarSemana()
RegistrarAperturaEffect ..> AperturasRepository : registrar()
The Kotlin app follows MVVM. The View only draws the state and sends events. The ViewModel holds the state and decides what to do with each event. The Model (repositories, DAOs and entities) reads and writes the data. In the features below, a ViewModel never touches Room or the network directly, and a screen never calls a repository.
Context-aware suggestion (Today screen). Teacherhomescreen is the View. It asks for the location permission, reads ContextoHoyViewModel.uiState with collectAsStateWithLifecycle() and draws ContextoSesionCard, which only receives the state and the callbacks onAceptar, onCambiar and onElegir. ContextoHoyViewModel keeps the state in a MutableStateFlow<ContextoHoyUiState> and exposes it as a read-only StateFlow. It calls ContextoRepository to download the catalog, register the presence at the campus, suggest the group of the day and save the teacher's confirmation. The card has no logic: the choice between suggestion, manual list and confirmed group is made in the ViewModel.
Worked hours (Hours screen). HorasScreen and RegistrarHorasSheet are the View. HorasViewModel combines the days that come from Room with the day being edited into a single HorasUiState, and offers abrirDia, cambiarHoras, cambiarMotivo, guardar and cerrar. The rules that depend on the data live in the Model: HorasRepository decides if there is a suggestion (presence at the campus and attendance recorded, capped by the time at the campus). The rules that depend on what the teacher typed live in RegistroDiaUi: whether the reason is required and whether the Save button is enabled. When the teacher edits the value, the ViewModel marks it as manual.
Grouping sessions (BQ #2). Creating a group in the Groups tab goes through GruposViewModel.agregarGrupo(), which calls SesionAgrupacionRepository.registrar(). The admin report follows the same path: ReporteSincronizacionSection (View), ReporteSincronizacionViewModel (ViewModel), and CloudFunctionsApi.reporteSesionesAgrupacion() (Model).
Model. Room entities and DAOs (ContextoEntities, ContextoDao), repositories, the location data source and the domain models (ContextoSesion). Because the state always flows from the Model to the View, the screens keep working offline: the data comes from Room, and the network only refreshes it.
Every event is saved first in the local database as a pending row. The sync worker uploads the pending rows when there is connectivity and the backend confirms them.
classDiagram
direction LR
class AppOpenRepository {
+recordOpen()
}
class AppOpenEntity {
<<Outbox row>>
+id: String
+weekStart: String
+openedAt: Long
+syncStatus: String
}
class AppOpenDao {
+insert(open)
+getPending() List~AppOpenEntity~
+markSynced(id)
}
class SyncWorker {
<<WorkManager>>
+doWork() Result
}
class AppOpensApi {
<<Cloud Functions>>
+uploadOpens(list)
}
AppOpenRepository ..> AppOpenDao : 1. save as PENDING
SyncWorker ..> AppOpenDao : 2. read pending rows
SyncWorker ..> AppOpensApi : 3. upload over HTTPS
AppOpensApi ..> AppOpenDao : 4. mark SYNCED
AppOpenDao --> AppOpenEntity
Better visualization: Component Interaction Diagram - Kotlin
Rationale: the Kotlin client is organized in four layers: View, ViewModel, Model and Firebase. Each screen has its own ViewModel that exposes a UI state, and the ViewModels reach the data through repositories using interfaces, so screens never touch Room or the network directly. The repositories read and write the local data source (Room, DAOs and the outbox), call the Cloud Functions API over HTTPS, and use the Firebase module for sign-in. The sync worker is triggered when pending data appears and uploads it when there is connectivity. Dependencies only point downward, which makes each layer replaceable and testable on its own.
The Flutter client uses an MVVM-style separation: widgets display the interface, ChangeNotifier ViewModels hold screen state and handle user actions, and services or factories handle authentication, analytics, activity creation and on-device scanning. The authentication flow calls the shared register and login Cloud Functions and opens a Firebase Auth session. Activity planning records events in Firestore and Firebase Analytics. The scanner uses the phone camera and on-device ML Kit OCR.
Separating screen state from service calls keeps the interface simple. The scanner can read a photographed list on the phone without a network connection. Authentication and analytics need a connection. The Flutter client does not yet have a local database or a background sync process.
| Architectural pattern | Where it appears | Responsible |
|---|---|---|
| MVVM-style separation | Widgets --> ChangeNotifier ViewModels --> services, factories and models |
Flutter team |
Rationale: the diagram follows actual calls in the Flutter code. Views observe their ViewModels, which delegate work to services or factories. Authentication reaches Cloud Functions and Firebase Auth; activity analytics reaches Firestore and Firebase Analytics; the scanner reaches the camera and on-device OCR. The group, hours and student ViewModels keep their current demonstration data in memory, so the diagram does not imply that those screens persist or synchronize data yet.
This section documents the design patterns and architectural tactics currently implemented in each client. Section 3.3 shows MVVM in both clients and offline-first with an outbox in Kotlin. The tactics of the shared backend appear in 3.4.3 because they apply to both clients.
Rationale: Repository and DAO separate the business logic from the storage, so each layer can change or be tested on its own. The Singleton database avoids opening several Room instances, and the Observer (Flow / StateFlow) keeps the screens in sync with the local data without manual refreshes. As tactics, Local cache and retries keep the data safe when the network drops, Asynchronous I/O keeps the UI responsive on low-end devices, and Schema migrations preserve teachers' data when the database changes between versions.
| Name | Kind | Where it appears | Responsible |
|---|---|---|---|
| Repository | Design pattern | One concrete repository class per area of data: AsistenciaRepository, SesionAgrupacionRepository, AperturasRepository and AuthRepository, plus ContextoRepository and HorasRepository for the context-aware feature. GruposRepository, ClasificacionRepository and ReportesRepository complete the layer, so no ViewModel calls Room, Firebase or CloudFunctionsApi directly |
Sebastian Rodriguez - Mateo Cante |
| Facade | Design pattern |
AuthFacade.iniciarSesion(email, password) hides the three login steps behind one call: POST /login (Cloud Function) → signInWithCustomToken (Firebase Auth) → school from users/{uid} (Firestore). registrarse() does the same with POST /register, and AuthViewModel depends only on the Facade |
Mateo Cante |
| Singleton | Design pattern |
AppDatabase.obtener() (single Room instance with @Volatile and double-checked locking) and the Kotlin objects FirebaseModule, CloudFunctionsApi and SyncScheduler
|
Santiago Quiroz |
| Observer | Design pattern |
Flow from the DAO → StateFlow in the ViewModel → collectAsStateWithLifecycle() in the screen. In HorasViewModel, the DAO flow goes through combine and stateIn until it becomes a StateFlow
|
Sebastian Rodriguez |
| DAO (Data Access Object) | Design pattern | Room @Dao interfaces (EstudianteDao, AsistenciaDao, SesionAgrupacionDao, AperturaDao) that hide the SQL from the repositories. Also ContextoDao and SesionAgrupacionDao
|
Sebastian Rodriguez - Santiago Quiroz |
| Local cache and retries | Availability tactic | Room as buffer with PENDING / SYNCED / FAILED; SyncWorker retries with WorkManager backoff |
Kotlin Sub-group |
| Asynchronous I/O | Performance tactic | Room suspend functions and Flow, network calls wrapped in withContext(Dispatchers.IO), background work in WorkManager |
Kotlin Sub-group |
| Schema migrations | Maintainability tactic |
AppDatabase version 5 with MIGRACION_1_2, MIGRACION_2_3, MIGRACION_3_4 and MIGRACION_4_5
|
Kotlin Sub-group |
A single Room database instance is shared by all repositories, which avoids several open connections to the same file.
classDiagram
direction TB
class RoomDatabase {
<<abstract>>
}
class AppDatabase {
<<Singleton>>
-instancia: AppDatabase
+obtener(context) AppDatabase
+estudianteDao() EstudianteDao
+asistenciaDao() AsistenciaDao
+sesionAgrupacionDao() SesionAgrupacionDao
+aperturaDao() AperturaDao
}
class EnadDatabase {
<<Singleton>>
-instancia: EnadDatabase
+obtener(context) EnadDatabase
+gruposDao() GruposDao
}
class FirebaseModule {
<<Singleton object>>
+auth: FirebaseAuth
+firestore: FirebaseFirestore
}
class AsistenciaRepository
class SesionAgrupacionRepository
class AperturasRepository
class AuthRepository
class GruposViewModel
AppDatabase --|> RoomDatabase
EnadDatabase --|> RoomDatabase
AsistenciaRepository ..> AppDatabase : obtener()
SesionAgrupacionRepository ..> AppDatabase : obtener()
AperturasRepository ..> AppDatabase : obtener()
GruposViewModel ..> EnadDatabase : obtener()
AuthRepository ..> FirebaseModule : auth, firestore
classDiagram
class ContextoHoyViewModel
class HorasViewModel
class SyncWorker
class ContextoRepository {
<<Repository>>
-dao : ContextoDao
-ubicacion : LocationDataSource
-auth : AuthRepository
+tienePermisoUbicacion() Boolean
+descargarCatalogo() Boolean
+registrarPresencia() SedeEntity
+sugerir(sede) GrupoDelDia
+contextoDeHoy() GrupoDelDia
+gruposDelDocente() List~GrupoDelDia~
+confirmar(grupoId, origen)
}
class HorasRepository {
<<Repository>>
-dao : ContextoDao
-auth : AuthRepository
+observarDias() Flow~List~DiaSesion~~
+reporteDelDia(fecha) ReporteHorasEntity
+sugerencia(fecha, planeadas) SugerenciaHoras
+guardar(fecha, planeadas, horas, origen, motivo)
+sincronizarPendientes() Boolean
}
class AppDatabase {
<<Singleton>>
-instancia : AppDatabase
+obtener(context) AppDatabase
+contextoDao() ContextoDao
}
class ContextoDao {
<<DAO>>
+guardarSedes(sedes)
+guardarGrupos(grupos)
+guardarPresencia(presencia)
+guardarContexto(contexto)
+guardarReporte(reporte)
+observarReportes(uid, desde, hasta) Flow
+reportesPendientes(uid)
+marcarReporte(uid, fecha, guardadoEn, estado, error)
}
class LocationDataSource {
+tienePermiso() Boolean
+ubicacionActual() Location
}
class CloudFunctionsApi {
<<Singleton object>>
+listarEscuelas(token)
+listarGruposDelDocente(token, uid)
+enviarReporteHoras(token, reporte, schoolId, plataforma, version)
}
class AuthRepository {
<<Repository>>
+uidActual() String
+obtenerToken() String
+schoolIdActual() String
}
class SyncScheduler {
<<Singleton object>>
+programar(context)
}
class RoomDatabase {
<<abstract>>
}
ContextoHoyViewModel ..> ContextoRepository : sugerir y confirmar
HorasViewModel ..> HorasRepository : observarDias, sugerencia, guardar
HorasViewModel ..> ContextoRepository : registrarPresencia
SyncWorker ..> HorasRepository : sincronizarPendientes()
ContextoRepository ..> AppDatabase : obtener()
HorasRepository ..> AppDatabase : obtener()
AppDatabase ..> ContextoDao : contextoDao()
AppDatabase --|> RoomDatabase
ContextoRepository ..> LocationDataSource : ubicacionActual()
ContextoRepository ..> CloudFunctionsApi : descarga el catalogo
HorasRepository ..> CloudFunctionsApi : sube el reporte
HorasRepository ..> SyncScheduler : programar()
ContextoRepository ..> AuthRepository : uid y token
HorasRepository ..> AuthRepository : uid y token
classDiagram
class GruposViewModel
class SyncWorker
class ReporteSincronizacionViewModel
class SesionAgrupacionRepository {
<<Repository>>
-sesiones : SesionAgrupacionDao
-auth : AuthRepository
+registrar(materia, nombreGrupo, ninosContados, docentesContados, ninosAsignados)
+sincronizarPendientes() Boolean
}
class AppDatabase {
<<Singleton>>
-instancia : AppDatabase
+obtener(context) AppDatabase
+sesionAgrupacionDao() SesionAgrupacionDao
}
class SesionAgrupacionDao {
<<DAO>>
+insertar(sesion)
+pendientes(uid)
+sumarIntento(id)
+marcar(id, estado, cuando, error)
}
class SesionAgrupacionEntity {
+id
+creadaEn
+connectivity
+syncStatus : SyncStatus
}
class InfoDispositivo {
<<Singleton object>>
+conectividad(context) String
+versionApp(context) String
}
class CloudFunctionsApi {
<<Singleton object>>
+enviarSesionAgrupacion(token, sesion, intento)
+reporteSesionesAgrupacion(token)
}
class AuthRepository {
<<Repository>>
+uidActual() String
+obtenerToken() String
}
class SyncScheduler {
<<Singleton object>>
+programar(context)
}
class RoomDatabase {
<<abstract>>
}
GruposViewModel ..> SesionAgrupacionRepository : registrar()
SyncWorker ..> SesionAgrupacionRepository : sincronizarPendientes()
ReporteSincronizacionViewModel ..> CloudFunctionsApi : reporteSesionesAgrupacion()
SesionAgrupacionRepository ..> AppDatabase : obtener()
AppDatabase ..> SesionAgrupacionDao : sesionAgrupacionDao()
AppDatabase --|> RoomDatabase
SesionAgrupacionDao ..> SesionAgrupacionEntity : PENDING, SYNCED, FAILED
SesionAgrupacionRepository ..> InfoDispositivo : conectividad y version
SesionAgrupacionRepository ..> CloudFunctionsApi : POST groupings
SesionAgrupacionRepository ..> AuthRepository : uid y token
SesionAgrupacionRepository ..> SyncScheduler : programar()
The ViewModel depends on a repository interface. The implementation decides whether the data comes from the local database or from the remote API.
classDiagram
direction TB
class AperturasViewModel
class RegistrarAperturaEffect
class SyncWorker
class AperturasRepository {
-aperturas: AperturaDao
-auth: AuthRepository
+registrar()
+observarSemana() Flow~Int~
+sincronizarPendientes() Boolean
}
class AperturaDao {
<<Local data source>>
}
class CloudFunctionsApi {
<<Remote data source, object>>
+enviarAperturas(token, aperturas)
}
class AuthRepository {
+uidActual() String
+obtenerToken() String
}
AperturasViewModel --> AperturasRepository : observarSemana()
RegistrarAperturaEffect --> AperturasRepository : registrar()
SyncWorker --> AperturasRepository : sincronizarPendientes()
AperturasRepository --> AperturaDao
AperturasRepository --> CloudFunctionsApi
AperturasRepository --> AuthRepository
classDiagram
class ContextoDao {
<<DAO>>
+observarReportes(uid, desde, hasta) Flow~List~ReporteHorasEntity~~
}
class HorasRepository {
<<Repository>>
+observarDias() Flow~List~DiaSesion~~
}
class HorasViewModel {
<<ViewModel>>
-registro : MutableStateFlow~RegistroDiaUi~
+uiState : StateFlow~HorasUiState~
+abrirDia(dia)
+cambiarHoras(texto)
+guardar()
}
class HorasScreen {
<<Composable>>
+estado : collectAsStateWithLifecycle()
}
class HorasHeatmap {
<<Composable>>
}
class RegistrarHorasSheet {
<<Composable>>
}
class ContextoHoyViewModel {
<<ViewModel>>
-_uiState : MutableStateFlow~ContextoHoyUiState~
+uiState : StateFlow~ContextoHoyUiState~
+iniciar()
+aceptarSugerencia()
+elegir(grupo)
}
class Teacherhomescreen {
<<Composable>>
+contextoState : collectAsStateWithLifecycle()
}
class ContextoSesionCard {
<<Composable>>
}
ContextoDao ..> HorasRepository : Flow emite al cambiar reportes_horas
HorasRepository ..> HorasViewModel : map y combine
HorasViewModel ..> HorasScreen : StateFlow
HorasScreen ..> HorasHeatmap : dias
HorasScreen ..> RegistrarHorasSheet : registro
ContextoHoyViewModel ..> Teacherhomescreen : StateFlow
Teacherhomescreen ..> ContextoSesionCard : estado
The DAO emits a new value every time the table changes, the ViewModel republishes it as UI state, and the screen recomposes without any manual refresh.
classDiagram
direction LR
class AperturaDao {
<<Subject>>
+observarDeLaSemana(uid, semana) Flow~Int~
}
class AperturasRepository {
+observarSemana() Flow~Int~
}
class AperturasViewModel {
<<Observer>>
+uiState: StateFlow~AperturasUiState~
}
class TeacherHomeScreen {
<<Observer>>
+collectAsStateWithLifecycle()
}
class AperturasSemanaCard {
<<Composable>>
}
AperturaDao ..> AperturasRepository : emits on every change
AperturasRepository ..> AperturasViewModel : same Flow, map + stateIn
AperturasViewModel ..> TeacherHomeScreen : emits new UI state
TeacherHomeScreen ..> AperturasSemanaCard : recomposes with state
classDiagram
class ContextoRepository {
<<Repository>>
}
class HorasRepository {
<<Repository>>
}
class SesionAgrupacionRepository {
<<Repository>>
}
class AppDatabase {
<<Singleton>>
+contextoDao() ContextoDao
+sesionAgrupacionDao() SesionAgrupacionDao
}
class ContextoDao {
<<DAO>>
+guardarSedes(sedes)
+sedesUbicadas() List~SedeEntity~
+sede(id) SedeEntity
+guardarGrupos(grupos)
+grupos(uid) List~GrupoHorarioEntity~
+grupo(id) GrupoHorarioEntity
+presencia(uid, fecha, sedeId) PresenciaSedeEntity
+presenciasDelDia(uid, fecha) List~PresenciaSedeEntity~
+guardarPresencia(presencia)
+contexto(uid, fecha) ContextoDiaEntity
+guardarContexto(contexto)
+guardarReporte(reporte)
+observarReportes(uid, desde, hasta) Flow~List~ReporteHorasEntity~~
+reporte(uid, fecha) ReporteHorasEntity
+reportesPendientes(uid) List~ReporteHorasEntity~
+sumarIntentoReporte(uid, fecha, guardadoEn)
+marcarReporte(uid, fecha, guardadoEn, estado, error)
+asistenciasDelDia(uid, fecha) Int
}
class SesionAgrupacionDao {
<<DAO>>
+insertar(sesion)
+pendientes(uid) List~SesionAgrupacionEntity~
+sumarIntento(id)
+marcar(id, estado, cuando, error)
}
class SedeEntity {
<<Entity sedes>>
+id
+lat
+lng
}
class GrupoHorarioEntity {
<<Entity grupos_horario>>
+id
+campusId
+horario
}
class PresenciaSedeEntity {
<<Entity presencia_sede>>
+fecha
+sedeId
+primeraVez
+ultimaVez
}
class ContextoDiaEntity {
<<Entity contexto_dia>>
+fecha
+grupoId
+origen
}
class ReporteHorasEntity {
<<Entity reportes_horas>>
+fecha
+horasRealizadas
+syncStatus
}
class SesionAgrupacionEntity {
<<Entity sesiones_agrupacion>>
+id
+creadaEn
+syncStatus
}
class RoomDatabase {
<<abstract>>
}
ContextoRepository ..> ContextoDao : consulta y guarda
HorasRepository ..> ContextoDao : consulta y guarda
SesionAgrupacionRepository ..> SesionAgrupacionDao : consulta y guarda
AppDatabase ..> ContextoDao : contextoDao()
AppDatabase ..> SesionAgrupacionDao : sesionAgrupacionDao()
AppDatabase --|> RoomDatabase
ContextoDao ..> SedeEntity : Query y Upsert
ContextoDao ..> GrupoHorarioEntity : Query y Upsert
ContextoDao ..> PresenciaSedeEntity : Query y Upsert
ContextoDao ..> ContextoDiaEntity : Query y Upsert
ContextoDao ..> ReporteHorasEntity : Query y Upsert
SesionAgrupacionDao ..> SesionAgrupacionEntity : Query e Insert
Each DAO is an interface with annotated methods. Room generates the SQL code, so the repositories call plain methods and never write queries.
classDiagram
direction LR
class AperturasRepository
class AperturaDao {
<<Dao interface>>
+insertar(a: AperturaEntity)
+observarDeLaSemana(uid, semana) Flow~Int~
+ultimaApertura(uid) Long
+pendientes(uid) List~AperturaEntity~
+marcar(ids, estado: SyncStatus)
}
class AperturaEntity {
<<Entity: aperturas>>
+id: String
+docenteUid: String
+abiertaEn: Long
+semana: String
+platform: String
+appVersion: String
+syncStatus: SyncStatus
}
class AppDatabase {
<<Singleton>>
}
AperturasRepository --> AperturaDao : calls methods, no SQL
AperturaDao ..> AperturaEntity : reads and writes
AppDatabase --> AperturaDao : aperturaDao()
Local cache and retries (availability). The local database buffers every write, and the sync worker retries failed uploads until the backend confirms them.
Schema migrations (maintainability). Each schema change ships with a Room Migration, so existing teacher data survives app updates.
stateDiagram-v2
direction LR
[*] --> PENDING : insertar() in Room
PENDING --> SYNCED : upload accepted by the backend
PENDING --> PENDING : transient error (5xx, 429, 408, 401, no network), WorkManager retries
PENDING --> FAILED : non-transient error (e.g. 400)
SYNCED --> [*]
Asynchronous I/O (performance). Room queries are suspend functions or Flow, so they run off the main thread; CloudFunctionsApi wraps every HTTP call in withContext(Dispatchers.IO); RegistrarAperturaEffect launches the write on Dispatchers.IO; and the upload runs in a CoroutineWorker managed by WorkManager. The main thread only draws the UI.
classDiagram
direction LR
class AppDatabase {
<<Singleton>>
+version: Int = 3
}
class MIGRACION_1_2 {
<<Migration>>
+migrate(db)
}
class MIGRACION_2_3 {
<<Migration>>
+migrate(db)
}
class SchemaV1 {
<<Old schema>>
estudiantes
asistencias
}
class SchemaV2 {
<<Schema>>
+ sesiones_agrupacion
}
class SchemaV3 {
<<Current schema>>
+ aperturas
+ index docenteUid, semana
}
SchemaV1 --> MIGRACION_1_2 : existing data
MIGRACION_1_2 --> SchemaV2 : data preserved
SchemaV2 --> MIGRACION_2_3 : existing data
MIGRACION_2_3 --> SchemaV3 : data preserved
AppDatabase ..> MIGRACION_1_2 : addMigrations()
AppDatabase ..> MIGRACION_2_3 : addMigrations()
| Name | Kind | Where it appears | Responsible |
|---|---|---|---|
| Factory Method | Design pattern |
ActivityCreator creates library or custom activities through one planning flow |
Samuel Charry |
| Strategy | Design pattern |
GroupingAnalyticsService.recommend() selects between automatic and manual grouping based on historical event data, with configurable class-size tolerance thresholds |
Juan Triviño |
| Service Layer | Design pattern |
GroupingAnalyticsService and GroupService encapsulate all HTTP calls to Cloud Functions; ViewModels never do network I/O directly |
Juan Triviño |
| Singleton | Design pattern |
AuthTokenStore uses Dart's factory-constructor singleton to hold the session token and uid, shared by all services |
Juan Triviño |
| Observer | Design pattern |
ChangeNotifier and ListenableBuilder refresh screens when ViewModel state changes |
Flutter team |
| Asynchronous I/O | Performance tactic | Dart async / await in authentication, analytics and scanning |
Flutter team |
| Repository | Design pattern |
StudentsRepository hides the HTTP calls behind methods named for the domain (setLevel, withdraw, fetchStudents), so ClassificationViewModel never builds a request or sees raw JSON |
Isabela Mantilla |
| Adapter | Design pattern | StudentApiAdapter.toDomain() translates the backend's vocabulary into the Student format. | Isabela Mantilla |
Factory Method class diagram and rationale — Samuel Charry
Rationale: Factory Method keeps activity creation separate from the shared planning and analytics flow. Strategy lets the recommendation algorithm swap between grouping methods based on historical data without changing the dialog or the widget. Service Layer isolates every HTTP call behind dedicated service classes, so ViewModels never touch the network directly. Singleton ensures a single AuthTokenStore instance holds the session token for all services. Observer (ChangeNotifier + ListenableBuilder) keeps every screen in sync with its ViewModel state without manual refreshes. As a tactic, Asynchronous I/O (async / await) prevents service calls from blocking the interface. Local caching and background retries are not implemented in this Flutter client.
ActivityCreator decides whether to build a library activity or a custom activity through a single planning flow. The ViewModel calls the creator without knowing which concrete type will be returned.
GroupingAnalyticsService.recommend() collects historical grouping events and selects the method (automatic or manual) that teachers adopted most often for a given subject and class size. The tolerance threshold changes with the class size (±3 if ≤10, ±5 if ≤25, ±8 if >25), so the algorithm adapts without changing the widget or the dialog.
GroupService and GroupingAnalyticsService encapsulate every HTTP call to Cloud Functions. ViewModels delegate all network I/O to these services and never build requests or parse responses directly. Both services get their authorization headers from the shared AuthTokenStore singleton.
AuthTokenStore uses Dart's factory-constructor singleton pattern: a single private instance is created once and returned by every AuthTokenStore() call. It holds the session token and uid after login, and all services read authHeaders from it.
Repository — Isabela Mantilla
We use Repository in a similar way to Kotlin, to keep the architecture uniform across both clients, but ours is not as robust: it has no DAO, because there is no local database (Room) behind it yet. StudentsRepository only talks to the Cloud Function directly, if the backend changes, only this class changes, but unlike Kotlin's repositories, there is no local cache to fall back on if the request fails.
Better visualization: Repository Design Pattern Diagram - Flutter
Adapter — Isabela Mantilla
Unlike Kotlin, where the backend's data passes through a plain mapper function (GrupoMapper.kt), we use a dedicated Adapter class. Most of the student data from the backend cannot be used directly by the UI so StudentApiAdapter translates that vocabulary into the Student model the classification screens already expect.
Better visualization: Adapter Design Pattern Diagram - Flutter
Authenticate and authorize actors (security). Each request carries the Firebase ID token. The Cloud Function checks the token and then the role before touching Firestore. It protects student data and applies to both clients because they use the same endpoints.
Rationale: Middleware / Chain of Responsibility ensures that every request passes through authentication and role checking before reaching the business logic, and that every error is caught and formatted consistently. Module Pattern keeps each domain (students, groups, analytics, etc.) in its own Express app, so modules can evolve independently without affecting each other.
| Name | Kind | Where it appears | Responsible |
|---|---|---|---|
| Middleware / Chain of Responsibility | Design pattern | Each Express module chains autenticar → route handlers → manejarErrores; the analytics module (analitica/index.js) follows the same pattern |
Juan Triviño (analytics module), Santiago Quiroz (auth middleware) |
| Module Pattern | Design pattern | Each domain is an independent Express app exported from its own folder and registered as a separate Cloud Function in functions/index.js
|
Juan Triviño (analytics module), all backend contributors |
Each Express module chains three stages: autenticar verifies the token and enriches req.usuario, the route handler validates the payload and executes the business logic, and manejarErrores catches any ErrorApi or unexpected error and returns a consistent JSON response. The analytics module (analitica/index.js) follows the same chain as the other modules.
Each domain is an independent Express app in its own folder (estudiantes/, grupos/, profesores/, analitica/, etc.). Each one exports an app that is registered as a separate Cloud Function in functions/index.js with its own memory, timeout and instance limits. Adding a new domain means creating a new folder and one line in index.js, without touching existing modules.
| Feature # | Feature | Functionality # | Functionality | View | Requirement(s) met | Rationale | Contributor |
|---|---|---|---|---|---|---|---|
| F #1 | Grouping sessions sync and report (BQ #2) | F1.1 | Records each grouping session on the device, uploads it in the background, and shows the admin a weekly sync report | Groups tab; admin Reports tab | Local storage, eventual connectivity, asynchronous work | Shows the admin how reliable the offline-first sync is | Sebastian Rodriguez |
| F #1 | F1.2 | Creates groups on the device first, syncs them with the groups API, and imports the existing ones | Groups tab | Local storage, eventual connectivity, external backend | Teachers organize groups offline and keep them after changing phones | Sebastian Rodriguez | |
| F #2 | Weekly app opens card (BQ #6) | F2.1 | Shows how many times the teacher opened the app this week out of two, with a reminder | Today tab | Type 2 BQ, local storage, eventual connectivity, asynchronous work | Turns a habit metric into feedback seen in daily use | Santiago Quiroz |
| F #2 | F2.2 | Signs in with email and password and opens the screen of the teacher's role | Login screen | Authentication, security, local storage | Every request is tied to a verified user with a role | Santiago Quiroz | |
| F #3 | Activity library and library usage (BQ #4) | F3.1 | Plans activities from a shared library or creates your own, and shows what percentage comes from each | Flutter: Today, "Planear actividades" | Type 2 BQ, external services, Factory Method | Shows how much planning relies on the library versus the teacher's own ideas | Samuel Charry |
| F #4 | Subject screens and classification by level (Flutter) | F4.1 | Shows the students grouped by level for each subject, with a grade filter and the active count | Flutter: Matemáticas and Lectura screens | Shared ViewModel, subject-specific level scales | Shows each student's level at a glance, the base to form groups | Isabela Mantilla |
| F #4 | F4.2 | Ranks the most-used activities with their count and share of the total | Flutter: "Actividades más usadas" (admin) | Type 4 BQ, external backend | Tells third parties which activities teachers really use | Isabela Mantilla | |
| F #5 | Smart grouping recommendation (BQ #14) | F5.1 | Recommends automatic or manual grouping from what teachers with a similar class chose, with its confidence | Flutter: Groups, "Nuevo grupo" | Smart feature, Type 5 BQ, external backend | Helps unsure teachers choose and can increase the adoption of grouping | Juan Triviño |
| F #5 | F5.2 | Loads, creates and deletes groups through the groups API | Flutter: Groups tab | External backend | Groups live in the backend, so they survive a phone change | Juan Triviño | |
| F #6 | Classification time (BQ #9) | F6.1 | Times how long classifying students takes and shows the admin the average minutes for 25 students | Classification screen; admin Reports tab | Type 3 BQ, eventual connectivity, asynchronous work | Shows whether classifying with the app helps or slows the teacher | Mateo Cante |
When a teacher creates a group, the app records a grouping session on the device and uploads it in the background when there is a connection. An admin sees each week the percentage of sessions that did not reach the backend within 24 hours or ended in an error, by platform, app version and connectivity at creation.
- View: Teacher: Groups tab, create group. Admin: Reports tab, "Sincronización de agrupaciones".
- Requirements met: Local storage (Room as an outbox with PENDING / SYNCED / FAILED), eventual connectivity (WorkManager sync with retries, safe to retry without duplicates), asynchronous work (coroutines and background worker).
- Rationale: rural schools have intermittent connectivity, so sessions are created offline and uploaded days later. The report tells the admin how reliable that offline-first sync is and where it fails, so the team can fix it.
Groups are created on the device first and then synced with the backend: new groups are created through the groups API when there is a connection, deleted groups disappear at once and are removed from the backend later, and the groups the teacher already has in the backend are imported.
- View: Teacher: Groups tab (list, create, view and edit group).
- Requirements met: Local storage (Room, with a migration that adds the remote id, the sync state and a soft delete), eventual connectivity (the sync worker uploads pending creations and deletions), external backend (REST API of the groups function).
- Rationale: the teacher can organize groups in the classroom without a connection, and the groups are not lost when the phone is changed.
The Today screen shows how many times the teacher opened the app this week out of a goal of two, and reminds them to come back while they are below it. Each open is saved on the device and uploaded in the background, and opens less than 5 minutes apart count as one.
- View: Teacher: Today tab, "Uso de la app esta semana" card.
-
Requirements met: Answers a Type 2 question (BQ #6), local storage (Room, the count is a local query that also works offline), eventual connectivity (WorkManager uploads pending opens in batches), asynchronous work (the card updates from a Room
Flowwithout refreshing). - Rationale: it turns a habit metric into feedback the teacher sees in daily use, without needing a connection.
The teacher signs in with email and password, and the app opens the screen of their role (teacher or admin). The Firebase session is kept on the device and its ID token is sent to every backend request. Accounts with a role that has no screen yet are not let in, and the app says why.
- View: Login screen (login mode).
- Requirements met: Authentication (login through the backend and sign-in to Firebase Authentication with a custom token), security (the ID token is checked by the backend on every request), local storage (the Firebase Auth session is kept on the device).
- Rationale: student data is private, so every request is tied to a verified teacher. The role decides which screens the person can reach.
The teacher plans activities for Matemáticas or Lectura, either by choosing one of the ready-made activities of the shared library or by creating their own with a name and a duration. Each planned activity is saved as an activity_selected event that says whether it came from the library or was written by the teacher, and a card shows what percentage of the teacher's activities came from the library and how many they created themselves.
- View: Teacher (Flutter): Today tab, "Planear actividades", Actividades screen.
- Requirements met: Answers a Type 2 question (BQ #4), external services (each planned activity is stored as an event in Firestore and logged in Firebase Analytics, and the counts are read back from the teacher's own events), design pattern (Factory Method creates the library and the custom activities).
- Rationale: the teacher sees how much of their planning relies on the shared library versus their own ideas, which helps them decide whether to reuse more activities or keep creating their own.
For each subject (Matemáticas and Lectura) the teacher sees the students grouped by level, can filter by grade, and sees how many students of the class are active. Students without a level appear in "Sin evaluar" and withdrawn students in "Fuera del colegio".
- View: Teacher (Flutter): Matemáticas and Lectura screens.
-
Requirements met: Local state and ViewModel shared between both screens (
StudentsViewModel), subject-specific level scales (6 levels for math and 6 for reading). - Rationale: the teacher can see at a glance at which level each student is in each subject, which is the base to form the groups.
The admin sees the most-used activities ranked from most to least used, with how many times each one was planned and its share of the total (BQ #11).
- View: Flutter: "Actividades más usadas" screen (admin).
-
Requirements met: Answers a Type 4 question (BQ #11), external backend (
GET /analytics/top-activities, admin only). - Rationale: it tells third parties which activities the teachers really use with the children.
When the teacher creates a group, the app recommends the grouping method (automatic or manual) that teachers most often chose for the same subject and a similar class size, shows the recommendation with its confidence percentage, and pre-selects it. The recommendation only appears when there are at least 3 comparable past groupings, and until then the automatic method is the default. Each time a group is created, the chosen method, the subject and the class size are recorded as a grouping event.
- View: Teacher (Flutter): Groups tab, "Nuevo grupo" dialog, "Método de agrupación" selector.
-
Requirements met: Smart feature (the recommendation is computed from the grouping events of the teachers, filtered by subject and class size), answers the Type 5 question of BQ #14, external backend (
POSTandGET /analytics/grouping-eventsof the analytics function, stored in thegroupingEventscollection). - Rationale: teachers who are unsure which method to use get a suggestion backed by what teachers with a similar class actually chose, which can increase the adoption of the grouping feature.
The Groups tab loads the teacher's groups from the groups API, creates new groups and deletes existing ones through it, and shows an error message when the connection fails.
- View: Teacher (Flutter): Groups tab (list, create and delete groups).
- Requirements met: External backend (REST API of the groups function).
- Rationale: the groups live in the backend, so the teacher finds the same groups after changing phones and the data of the recommendation comes from real groupings.
The classification screen times how long the teacher takes to classify students (only while the app is in the foreground). When the teacher finishes, the session is uploaded in the background, and the admin sees the average minutes needed to classify 25 students for each teacher and institution.
- View: Teacher: Classification screen. Admin: Reports tab, "Clasificación".
- Requirements met: Answers a Type 3 question (BQ #9), eventual connectivity (WorkManager job with retries, no duplicates), asynchronous work.
- Rationale: it shows whether classifying students with the app helps or slows down the teacher's process.
| Feature # | Special Feature | Functionality # | Functionality | View | Requirement(s) met | Rationale | Contributor |
|---|---|---|---|---|---|---|---|
| SF #1 | Context-aware session suggestion by location and schedule | SF1.1 | When the teacher opens the app within 150 m of one of their campuses on a day with planned hours, the app suggests the campus and group of the day. The teacher confirms or changes it. The app also suggests the worked hours of the day only if the teacher was at the campus and recorded attendance, capped by the time spent at the campus. The teacher can edit the value and must give a reason if it is lower than planned. | Teacher: Today screen (suggestion card) and Hours screen (register hours). | Sensor (location with the fused provider, a single foreground reading, no background tracking), local storage (campuses, group schedules, presence, daily context and hours in Room), eventual connectivity (works offline from the downloaded catalog, hours are synced later), privacy (coordinates are never stored or sent, only the time at the campus). | It saves the teacher from setting up the session and reporting hours by hand, using context the app already has. The suggestion is only a proposal, the teacher always decides. | Sebastian Rodriguez |
| SF #2 | Welcome email through an external email service (Brevo) | SF2.1 | When a teacher registers, the backend builds a welcome email with their name and the institutions and campuses they chose, and sends it through the Brevo transactional email API. If the email fails, the account is still created. | Teacher: registration (Login screen in register mode). | External service (Brevo REST API over HTTPS), security (the API key lives in Secret Manager, never in the app or the code), resilience (a failed email never breaks the registration), input safety (user names are escaped before going into the HTML). | It confirms to new teachers that their account and institutions were set up correctly, without adding any step for them. | Mateo Cante |
| SF #2 | SF2.2 | The registration response tells the app whether the email was sent, and the app shows the teacher a notice to check their inbox only when it really was sent. | Teacher: Login screen, right after registering. | External service feedback to the user, eventual connectivity (registration needs a connection; the app never claims an email was sent if it was not). | The teacher knows what to expect, so a missing email is not confused with a failed registration. | Mateo Cante | |
| SF #3 | Attendance roster import with the camera and offline OCR (Kotlin) | SF3.1 | In the attendance screen the teacher takes a photo of the paper roster. The app reads it on the phone with ML Kit text recognition, rebuilds each row (name, code, grade and age) and shows the detected students so the teacher can review them before importing. | Teacher: Attendance screen (camera and "Lista escaneada" dialog). | Sensor (camera with CameraX, the permission is explained before it is requested and detected again when the teacher grants it from Settings), smart feature (on-device text recognition plus a parser that groups the text fragments into rows and ignores headers), eventual connectivity (the model is embedded, so it works without internet). | Many rural schools keep the roster on paper. Importing it by photo saves typing the whole class, and the photo never leaves the phone. | Santiago Quiroz |
| SF #4 | Subject classification progress (Type 2 BQ) | SF4.1 | Shows each subject's real classification percentage on the Today tab, computed live from the students the backend actually returns for that teacher. | Teacher: Today tab (subject progress cards) | Answers a Type 2 question | Turns raw classification data into an shortcut for the teacher, improving their experience. | Isabela Mantilla Mora |
"View" is the screen where the feature can be found. "Requirement(s) met" lists what the functionality satisfies (uses a sensor, uses an external service, answers a Type 2 question, is context-aware, is a smart feature, or handles authentication).
- BQ #2 · Type 1 — Sebastian Rodriguez
- BQ #6 · Type 2 — Santiago Quiroz
- BQ #4 · Type 2 — Samuel Charry
- BQ #11 · Type 4 — Isabela Mantilla
- BQ #14 · Type 5 — Juan Triviño
- BQ #9 · Type 3 — Mateo Cante
- Analytics pipeline · BQ #2 · Type 1 — Sebastian Rodriguez
- Analytics pipeline · BQ #6 · Type 2 — Santiago Quiroz
- Analytics pipeline · BQ #4 · Type 2 — Samuel Charry
- Analytics pipeline · BQ #11 · Type 4 — Isabela Mantilla
- Analytics pipeline · BQ #14 · Type 5 — Juan Triviño
- Analytics pipeline · BQ #9 · Type 3 — Mateo Cante
- Architecture Pattern — Sebastian Rodriguez
- Architecture Pattern — Santiago Quiroz
- Architecture Pattern — Samuel Charry
- Architecture Pattern — Isabela Mantilla
- Architecture Pattern — Juan Triviño
- Architecture Pattern — Mateo Cante
- Design Pattern — Sebastian Rodriguez
- Design Pattern — Santiago Quiroz
- Design Pattern — Samuel Charry
- Design Pattern — Isabela Mantilla
- Design Pattern — Juan Triviño
-
Design Pattern — Mateo Cante: Facade (
AuthFacade) and Repository (GruposRepository,ClasificacionRepository,ReportesRepository).
-
Features Sebastian Rodriguez
- Feature 1 — Sebastian Rodriguez: implementation of BQ #2. Grouping sessions are saved on the device and uploaded in the background, and the admin sees each week the percentage that did not sync within 24 hours.
- Feature 2 — Sebastian Rodriguez: groups created on the device first and synced with the backend (create, delete and import).
-
Features Santiago Quiroz
- Feature 1 — Santiago Quiroz: implementation of BQ #6. The Today screen shows the weekly app-opens card with the goal of two opens and a reminder, backed by Room and uploaded in the background.
- Feature 2 — Santiago Quiroz: login connected to the backend and Firebase Authentication, with the role-based navigation and the session kept on the device.
- Features Samuel Charry: Authentication and activity planning and usage.
-
Features Isabela Mantilla
- Feature 1 — Isabela Mantilla: Matemáticas and Lectura screens with the students grouped by level and filtered by grade.
- Feature 2 — Isabela Mantilla: implementation of BQ #11. Ranking of the most-used activities with count and percentage, read from the admin analytics endpoint.
- Features Juan Triviño
-
Features Mateo Cante
- Feature 1 — Mateo Cante: implementation of BQ #9. The classification screen times how long the teacher takes to classify students, the sessions are uploaded in the background, and the admin sees the average minutes per 25 students for each teacher and institution.
- Feature 2 — Mateo Cante: classification of students by level with real data. The teacher sees only the students of their own institutions, filtered by grade, with registration linked to several institutions and their campuses.
- Special Feature — Sebastian Rodriguez: context-aware suggestion of the campus, group and worked hours from the teacher's location and schedule.
- Special Feature — Santiago Quiroz: attendance roster import with the phone camera (CameraX) and on-device text recognition (ML Kit), with a parser that turns the text into students and a review dialog before importing.
- [Special Feature — Samuel Charry]:auth feature()
- Special Feature — Isabela Mantilla
- Special Feature — Juan Triviño
- Special Feature — Mateo Cante: welcome email through the external Brevo API: when a teacher registers, the backend sends a welcome email with their institutions and campuses, and the app shows whether it was sent.
- MS6: Typography and fonts.
-
MS7: Teacher screens of the Kotlin client: Groups, Hours and My data (branches
pantalla-grupos-profesor,pantalla-horas-profesorandpantalla-misdatos-profesor). -
Sprint 2:
-
Backend: students CRUD with progress by subject and level, institutions CRUD, BQ #2 (
groupingsfunction: session upload with the sync delay and the admin weekly report), campus coordinates and group schedules with their validation for the context-aware feature, and theworkedHoursfunction (daily worked-hours reports and admin summary) with its tests and deployment. - Front end (Kotlin): BQ #2 (grouping sessions saved in Room and uploaded by WorkManager, admin sync report), sync of the groups with the backend (create, delete and import, EnadDatabase migration to version 2), BQ #14 in the Kotlin client (grouping events in Room, recommendation badge) and the special feature of context-aware suggestion: location data source, campus and group catalog cached in Room for offline use, presence at the campus, suggestion card on the Today screen, suggestion of worked hours with a reason when they are below the plan, sync of the hours to the backend, and the Room migrations 3 to 4 and 4 to 5. Verified end to end on the emulator, including offline.
- Wiki: BQ #2 detail and analytics pipeline, deployment, domain model and component diagrams for the MVVM architecture, and the Repository, DAO, Observer and MVVM patterns.
-
Backend: students CRUD with progress by subject and level, institutions CRUD, BQ #2 (
- MS6: Navigation Patterns and Wiki organization
- MS7: Sub-group Flutter Subjects Screens and Prototype video edition
-
Sprint 2: BQ #11 (admin endpoint
GET /analytics/top-activities, the activity title added to the events, and the Flutter "Actividades más usadas" screen), Overall Architecture Diagram, Analytics pipeline Dashboard/Hoy, Matemáticas and Lectura screens with the students grouped by level, and editing the ethics video.
- MS6:
- MS7:
-
Sprint 2: BQ #14 Type 5 (smart grouping recommendation). Backend: analytics Cloud Function (
analitica/index.js) with POST and GET/grouping-eventsendpoints storing events in Firestore. Flutter:GroupingAnalyticsService(event tracking and recommendation algorithm),GroupingMethodSelectorwidget with recommendation badge,CreateGroupDialogintegration,GroupServiceandAuthTokenStore(singleton) services. Design patterns: Strategy, Service Layer, Singleton (Flutter), Middleware and Module Pattern (backend). Wiki: BQ #14 detail, analytics pipeline entry, design patterns documentation.
- MS6: The high-fidelity prototype is linked in the MS6 page.
- MS7: No individual contribution is recorded in the current wiki.
- Sprint 2: Flutter authentication, activity planning, the BQ #4 usage card, camera-based student list scanning, and the related diagrams and documentation.
- MS6: High fidelity prototype (Login screen), Value Proposition of the SolutioN
- MS7: Enhanced Value Proposition - Screen "Hoy" Teacher Menu - Screen reports foundations - Screen Login - Kotlin Video
-
Sprint 2:
- Front end (Kotlin): attendance roster import with the camera and offline OCR (CameraX, ML Kit and the list parser), BQ #6 Type 2 refinement and implementation through the weekly app-opens card in the "Hoy" screen with local persistence and synchronization, Login/Auth feature implementation and connection with the Kotlin repository and Firebase backend, BQ #4 implementation for activity usage analytics (library vs. teacher-created activities) with Firestore event tracking, persistent history and usage percentages, activities screen and integration with the teacher home, and integration and debugging of the Kotlin application with Firebase services.
- Backend / Firebase integration: integration and validation of the Kotlin application with the existing Firebase Cloud Functions and Firestore services, including authentication, app-opens analytics, grouping analytics and activity analytics data flows, as well as the Firestore security rules required for BQ #4 analytics events.
- Architecture / documentation: architectural patterns diagram for the Kotlin application, Repository design pattern diagram, analysis and documentation of the application's architecture, and Wiki development and maintenance, including documentation of implemented BQs, analytics flows, architectural patterns, and Kotlin implementation details.
- MS6: Icons and imagen
-
MS7: Classification screens of the Kotlin client: Reading and Math classification views, student lists with filter by group, unexpected student list, classification per student and navigation between the screens (branch
lect_mate_classification). -
Sprint 2:
- Backend: groups CRUD (create, list, detail, edit, delete, and add or remove students), teacher routes (list, create, detail and by school), registration with the teacher role, active field and several institutions with their campuses, schools with automatic id and school validation for students, welcome email on registration through the external Brevo API (email client and template), BQ #9 (
classificationSessionsfunction: session upload and the admin report of minutes per 25 students) with its tests, and a script to seed the Firebase emulators with test data. - Front end (Kotlin): registration with institutions and campuses and the welcome email notice, classification screen with real students filtered by the teacher's institutions and by grade, BQ #9 (classification timer, sessions sent with WorkManager and the admin "Clasificación" report), debug mode against the local Firebase emulators, and the Repository (
GruposRepository,ClasificacionRepository,ReportesRepository) and Facade (AuthFacade) design patterns. - Wiki: BQ #9 detail and analytics pipeline entry, Repository and Facade patterns, and the email API in the deployment diagram.
- Backend: groups CRUD (create, list, detail, edit, delete, and add or remove students), teacher routes (list, create, detail and by school), registration with the teacher role, active field and several institutions with their campuses, schools with automatic id and school validation for students, welcome email on registration through the external Brevo API (email client and template), BQ #9 (