A native Android app in Java on Firebase Authentication and Cloud Firestore, built from the class example — completing the basketball cards, and making team cards something you can add without writing code.
Jason Muinde · Student No. 668504 · Facilitator: Paul Mwaniki
“Find the updated repository here — you just have to set up the firestore and the firebase on your end, and then improve the project and build another team card / complete the basketball cards.”
Live project, Email/Password auth, Firestore in nam5, hardened rules, 3 indexes, 16 seeded documents.
Two of the three cards were dead views. Both now open working screens.
Solved structurally: four sports today, a fifth needs no code.
activity_basketball_dashboard.xml declared three cards…
<!-- all three declared in the layout -->
<CardView android:id="@+id/cardApply" … />
<CardView android:id="@+id/cardPlayers" … />
<CardView android:id="@+id/cardResults" … />
…basketballDashboard.java wired one.
apply = findViewById(R.id.cardApply);
apply.setOnClickListener(v -> startActivity(…));
// cardPlayers — never looked up
// cardResults — never looked up
| cardBasketball | wired |
| cardHockey | dead |
Hockey was visible on the dashboard and unreachable. Tapping it did nothing — the third dead card.
INTERNET permission in the manifestreturn — accounts created with empty credentialsgetException().getMessage() with no null checkownerUid — anyone could edit anything"Player Name"| Card | Before | After |
|---|---|---|
| Apply to Team | worked | Works — and now for every sport |
| View Players | dead view | SquadActivity — live roster, RecyclerView, real-time |
| View Results | dead view | FixturesActivity — fixtures, scores, season W-D-L |
A Firestore snapshot listener pushes changes, so an application submitted on another handset appears in the squad list immediately. ListAdapter + DiffUtil rebind only the rows that changed.
W-D-L is derived in the client from the same fixture list shown beneath it — so the summary and the rows are computed from one source and can never disagree.
Ten minutes of work. Satisfies the words exactly.
And the next sport costs the same ten minutes again — another block of XML, another findViewById, another listener, another Activity.
One item_sport.xml. One SportAdapter. The list is whatever the Firestore sports collection contains.
Four sports ship today. A fifth is a database row — no code, no rebuild, no redeploy.
That is the difference between adding a card and being able to add cards.
From the class example — completed.
Added — no new Java.
The dead card, brought to life.
Added — no new Java.
SportAdapter.java — one adapter, N sports
public class SportAdapter extends
ListAdapter<Sport, SportAdapter.SportViewHolder> {
TrialApplicationActivity.java — the form adapts too
// basketball offers "Point Guard",
// football offers "Goalkeeper"
positions.addAll(sport.getPositions());
A fifth sport needs no code — it ships with a generic icon until artwork is added,
which resolveIcon() handles deliberately.
Firestore's own cache often beats Room. A cache result arriving after a network result is discarded — otherwise it overwrites fresher data and flips the offline banner on while you're online.
Room throws if queried on the main thread. All access goes through a single-threaded executor, which also serialises reads against writes.
Every observation returns a Subscription. Closing it detaches the listener and suppresses in-flight database results, so a destroyed Activity isn't retained.
assembleDebug
assembleRelease (R8)
Unit tests
Lint — “No issues found”
The launcher activity ran on a theme whose ancestry ends at a framework theme — not AppCompat:
Theme.VarsitySports.Splash
→ Theme.SplashScreen
→ … → android:Theme.DeviceDefault ← not AppCompat
AppCompatDelegate checks for windowActionBar inside setContentView and throws:
java.lang.IllegalStateException: You need to use a
Theme.AppCompat theme (or descendant) with this activity.
SplashScreen.installSplashScreen(this);
super.onCreate(savedInstanceState);
It's what reads postSplashScreenTheme and swaps the Activity onto the real theme.
Six Robolectric tests now launch the real Activity on the JVM — real AppCompatDelegate, real theme resolution, real inflation.
Delete that line again and 5 of the 6 fail. I verified that by actually deleting it, not by assuming.
Build-green is not run-green. None of those four signals ever executed an Activity.
createdAt unvalidated — queue position was forgeableRemoving the blanket ProGuard keeps alone took the release APK from 4.21 MB → 3.46 MB — proof they really were suppressing R8.
match /trialApplications/{id} {
allow read: if true; // so the website could show squads
}
Those documents carry studentId — a real six-digit USIU student number — beside full name, gender and school.
Anyone could take the project id out of the APK and dump the whole roster with one unauthenticated GET.
match /trialApplications/{id} {
allow read: if isSignedIn();
}
A roster of names is a design choice. Publishing identity numbers beside them is not — on a course whose outline names “user privacy” explicitly.
The website reads only sports and fixtures. No personal data.
Against the real rules engine in the Firestore emulator. A student cannot apply in another's name, self-select, forge createdAt, or delete someone else's application.
It's compiled into every APK and extractable in 30 seconds. Treating it as secret makes it no harder to get and breaks a fresh clone. The rules are the boundary.
Both backup rule files shipped as empty Android Studio templates — one still had a TODO — so email and student number were being uploaded to Google. Now closed.
| Suite | Tests | Runs on | Status |
|---|---|---|---|
| JVM unit — Validators, Converters, Fixture, TrialApplication | 33 | JVM | passing |
| Robolectric — LoginActivityLaunchTest | 6 | JVM | passing |
| Firestore security rules | 23 | Firebase emulator | passing |
| Room instrumented — AppDatabaseTest | 10 | device | not run |
| Espresso UI — LoginActivityTest | 5 | device | not run |
This machine has virtualisation disabled in firmware, so the Android emulator cannot start and connectedDebugAndroidTest has never run. There are no device screenshots.
I'd rather say that than imply a device run I didn't do — that gap is exactly what let the launch crash survive.
0x1F, not a comma — because a position may contain a comma. There's a test for exactly that.NumberFormatException if the parse were unguarded.| Week | Topic | Where |
|---|---|---|
| 3 | UI, buttons, click events, messages | 7 screens; Snackbar via BaseActivity |
| 4–5 | Views, ViewGroups, layout managers | Constraint / Linear / Frame / Scroll / Coordinator |
| 4–5 | Resource folder, menus | values-night/, 14 vectors, anim/, plurals, string array |
| 5–6 | Activity lifecycle | All 8 callbacks — incl. onRestart — logged under VarsityLifecycle |
| 5–6 | Intents & Bundles, navigation | EXTRA_SPORT_ID; parentActivityName on every screen below the hub |
| 5–6 | Configuration changes | Subscriptions closed in onStop, reattached in onStart |
| 8–9 | SharedPreferences and Room | SessionManager + AppDatabase, 3 DAOs |
| 8–9 | Reading/writing, local databases | @Transaction atomic replace, exported schema |
| 10 | RecyclerView | 3 adapters, all ListAdapter + DiffUtil |
| — | Firebase & libraries | Auth, Firestore, Material 3, ViewBinding |
| — | Animations & transitions | Activity push/pop + staggered list entry |
| — | Async / background processing | AppExecutors — serial disk executor |
| — | Security & user privacy | 23 rules tests, R8, SECURITY.md |
| 13 | Publishing | Valid reverse-DNS id; 3.46 MB minified release |
React 19 · TypeScript · Tailwind v4 · Vite · Cloudflare Pages
Reads the same public Firestore collections over REST — no Firebase JS SDK, which would have added ~200 KB gzipped for data that changes a few times a season.
The hero device is rendered from the same live documents. Add a sport and both the app and the page grow a card.
The sport cards rendered at opacity:0 — they mount after the fetch, so the observer had already run. And the hidden state was the CSS default, so any script failure blanked the page.
Bundle: 68 KB gzipped JS, 7 KB CSS.
| Activities | 7 |
| RecyclerView adapters | 3 |
| Room entities / DAOs | 3 / 3 |
| Layouts / vector drawables | 10 / 13 |
| Sports supported | 4, extensible without code |
| Debug APK | 12.59 MB |
| Release APK | 3.46 MB |
studentId into a private subcollection so signed-in students can't read each other's ID numbers.The first commit in the repository is the class example as provided, so every change is a reviewable diff.
REPORT.md · SECURITY.md · PROJECT.md in the repository root
Jason Muinde · 668504 · APT3060XA Mobile Application Development · USIU-Africa