MyFamilyTracker
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
This post dismantles the myth that QA equals mere test execution, shows how solo Android developers can embed quality throughout the lifecycle, and provides a 2026‑ready toolchain with concrete Kotlin/Compose examples.
On this page
QA is not a checkbox you tick after coding. Treating QA as merely test execution blinds you to defects that slip into design, architecture, and user experience. In 2026, solo Android developers who conflate QA with testing ship slower, incur higher rework, and lose user trust.
When you work alone, every hour spent fixing a bug that could have been caught earlier is an hour stolen from feature development or personal time. The myth that QA = testing leads to reactive firefighting, missed performance regressions, and UI inconsistencies that only surface after Play Store release. To ship reliably, you need a proactive quality system that spans requirements, code, build, and release.
Testing validates that the software behaves as expected under specific conditions. Quality Assurance, however, is the set of practices that prevent defects from being introduced in the first place. When you reduce QA to test execution, you ignore three critical dimensions:
| Dimension | What Testing Covers | What QA Covers |
|---|---|---|
| Prevention | None | Design reviews, static analysis, coding standards |
| Detection | Unit, integration, UI tests | Testability, observability, fault injection |
| Feedback | Pass/fail after change | Continuous improvement loops, metrics, retrospectives |
Consider a typical solo workflow: you write a feature, run a few unit tests, push to GitHub, and let GitHub Actions run your instrumented tests. If the tests pass, you assume the release is ready. Yet a subtle layout bug that only appears on foldable devices, a memory leak triggered by rapid navigation, or a security misconfiguration in your Firebase rules never surfaces because no test exercised those paths. The result? A one‑star review, a rollback, and wasted hours debugging in production.
[!WARNING]
Assuming that passing tests equal zero risk is the most costly mistake a solo dev can make. It creates a false sense of security and delays the discovery of high‑impact defects.
A healthy QA loop treats quality as a feedback signal that flows from left to right (idea → code → build → release) and back again. Implementing this loop does not require a team; it requires disciplined automation and lightweight practices you can adopt today.
Start every PR with tools that fail fast without launching an emulator. Use Detekt for Kotlin linting, Ktlint for formatting, and the Android Lint baseline for resource issues. Add contract tests for your ViewModels using Kotlin Flow and Turbine to guarantee that state emissions obey business rules.
// src/test/java/com/example/app/ui/home/HomeViewModelTest.kt
class HomeViewModelTest {
private val dispatcher = UnconfinedTestDispatcher()
private lateinit var viewModel: HomeViewModel
@BeforeEach
fun setUp() {
Dispatchers.setMain(dispatcher)
viewModel = HomeViewModel(repository = FakeRepository())
}
@AfterEach
fun tearDown() {
Dispatchers.resetMain()
}
@Test
fun `fetching shows loading then data`() = runTest {
// Assert initial loading state
viewModel.uiState.collect { assertTrue(it.isLoading) }
// Trigger load
viewModel.load()
// Expect loading then success
val states = viewModel.uiState.take(2).toList()
assertEquals(listOf(true, false), states.map { it.isLoading })
assertTrue(states.last().data.isNotEmpty())
}
}[!TIP]
Keep your test suites under two minutes. If they grow longer, split them by module or feature so you get rapid feedback on every commit.
Quality isn’t only about correctness; it’s also about performance, stability, and usability. Integrate Firebase Performance Monitoring and Crashlytics into your debug builds, and export the metrics to a personal dashboard (e.g., Google Data Studio) so you can spot regressions before they reach users.
Add the following to your
app/build.gradledependencies {
implementation platform('com.google.firebase:firebase-bom:33.2.0')
implementation 'com.google.firebase:firebase-analytics-ktx'
implementation 'com.google.firebase:firebase-crashlytics-ktx'
implementation 'com.google.firebase:firebase-perf-ktx'
}Initialize Crashlytics in your
Applicationclass MyApp : Application() {
override fun onCreate() {
super.onCreate()
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(true)
FirebasePerformance.getInstance().isPerformanceCollectionEnabled = true
}
}[!NOTE]
Even a solo developer benefits from treating production data as a quality signal. Set up alerts for crash‑free‑users dropping below 99.5% or frame‑rate dropping under 60 fps on the 90th percentile.
Your CI pipeline should enforce more than just test pass/fail. Use Gradle’s
testconnectedAndroidTest# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
- name: Cache Gradle
uses: actions/cache@v4
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
restore-keys: ${{ runner.os }}-gradle-
- name: Build & Test
run: ./gradlew clean test connectedAndroidTest --no-daemon
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: |
**/build/reports/tests/
**/build/outputs/androidTest-results/If you want to enforce a minimum coverage of 80 % for unit tests, add the Jacoco plugin and a quality gate:
jacoco {
toolVersion = "0.8.12"
}
tasks.withType(Test) {
finalizedBy jacocoTestReport
}
jacocoTestReport {
dependsOn test // tests are required before running the report
}
jacocoTestCoverageVerification {
violationRules {
rule {
limit {
minimum = 0.80 // 80 %
}
}
}
}When coverage falls below 80 %, the build fails, prompting you to add missing tests before merging.
Modern solo development benefits from AI‑augmented tooling that reduces boilerplate while keeping quality front‑and‑center. Claude Code (the MCP‑enabled assistant) can generate test skeletons, suggest edge cases, and even refactor legacy Java to Kotlin with safety guarantees.
Instead of writing each
@TestPrompt:
“Create a Jetpack Compose UI test for a
that displays a user’s name, avatar, and a logout button. Verify that clicking the logout button navigates to the login screen using NavHostController.”codeProfileScreen
Claude Code responds with:
// src/androidTest/java/com/example/app/ui/profile/ProfileScreenTest.kt
class ProfileScreenTest {
private lateinit var navController: TestNavHostController
@BeforeEach
fun setUp() {
navController = TestNavHostController(ApplicationProvider.getApplicationContext())
navController.setGraph(R.navigation.profile_nav_graph)
}
@Test
fun profileScreen_displaysUserAndHandlesLogout() = createComposeTestRule(navController).run {
// Given a fake user
val testUser = User(
id = "1",
name = "Ada Lovelace",
avatarUrl = "https://example.com/ada.png"
)
// Provide the ViewModel via Hilt test binding (omitted for brevity)
// When
composeTestRule.setContent {
ProfileScreen(viewModel = hiltViewModel(), navController = navController)
}
// Then
assertTrue(hasText(testUser.name))
assertTrue(existsWithContentDescription("Avatar for ${testUser.name}"))
assertTrue(existsWithTooltip("Logout"))
// When logout clicked
composeTestRule.onNodeWithTooltip("Logout").performClick()
// Then navigate to login
assertTrue(navController.currentDestination?.route == "login")
}
}You can paste this directly into your source tree, then replace the fake data with real implementations from your Hilt test modules. The AI‑generated test gives you a solid baseline, reducing the time spent on boilerplate and letting you focus on asserting business‑specific behavior.
When testing asynchronous UI logic, use
runTestkotlinx-coroutines-testUnconfinedTestDispatcher@Test
fun `search debounces and shows results`() = runTest {
val viewModel = SearchViewModel(repository = FakeSearchRepository())
viewModel.query.emit("kotlin")
advanceTimeBy(300) // debounce duration
assertTrue(viewModel.results.value.isNotEmpty())
}[!IMPORTANT]
Always pair AI‑generated code with a manual review. Treat the output as a starting point, not a final product. This keeps you accountable for correctness while still gaining velocity.
Set up a Claude Code hook that reads your Firebase Crashlytics weekly digest and summarizes trends in natural language. For example:
> [!NOTE]
> Claude Code summary (2026-08-02): Crash rate rose 0.3 % after v3.4 release, primarily due to NullPointerException in ProfileViewModel.kt line 57. Suggest adding null‑check for user.profileImage.You can automate this with a GitHub Action that runs a small Kotlin script calling the Claude API, then posts the summary to a Slack channel or your personal todo list.
Sudarshan Chaudhari
AI Systems Builder / Product Engineer
Bangkok, Thailand
Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.
Related Posts
Related Apps
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
Building something? Available for Android dev and QA consulting.
Work with meComments — powered by Giscus
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
ReadPrivate dream journal — structured entry capture, pattern tagging, and optional Claude-powered insight generation. All data stays on-device by default.
ReadWorkout tracker — exercise logging with set/rep/weight history, goal progression, and local Room DB persistence. No account, no cloud sync required.
Read