Debugging, Code Quality and Performance
|
This content was generated with the assistance of AI and should be verified against the official documentation of Android before being relied on in production. See the section bibliography for the reference material consulted while preparing these pages. |
Shipping TrailMate is not the end of the work: it has to keep working correctly and quickly on devices the team
never tests directly, and regressions have to be caught before users report them. This page covers the tools for
that in three groups: debugging (finding out why something is wrong, right now, on a connected device or
emulator), code quality (catching mistakes and inconsistencies automatically, before they are ever run), and
profiling and performance (measuring where CPU, memory and frame time actually go, and preventing
regressions). It closes with production monitoring — the same signals, aggregated from real users, once the
app has shipped. The baseline is the same one used throughout this section: compileSdk = 37, targetSdk = 36,
minSdk = 26, Android Studio Quail. Compose-specific performance work — stability, @Stable/@Immutable,
strong skipping and how to read a composable’s recomposition count in context — is covered in depth on
State, Recomposition and Side Effects in Compose; this
page introduces the Layout Inspector’s recomposition counts as a debugging tool and cross-links there for the
underlying model. Testing (unit and instrumented) is covered on
Unit Testing with JUnit, Mockito, MockK and
Robolectric and Instrumented and UI Testing (Espresso, Compose
Testing, UI Automator); R8’s role in signing and packaging a release build is covered on
Building, Signing and Preparing a Release, and R8’s role
in reducing attack surface is covered on
Permissions, Privacy and Security. This page focuses on
R8 purely as a performance and app-size optimization.
Debugging
The Android Studio Debugger
Android Studio embeds a standard JVM debugger (the same one IntelliJ uses for any Kotlin/Java project), attached
to the app process running on a device or emulator. Setting a breakpoint by clicking a line’s gutter, then
running the app in debug mode, suspends execution when that line is reached and exposes the usual tools: a call
stack, variable inspection (including TrailMate’s own types, such as a suspended TrailListUiState.Success
showing its trails list), watches, and stepping (into/over/out). Two features matter specifically for
Kotlin coroutines, which dominate TrailMate’s codebase:
-
Coroutine debugger. The Debug tool window’s Coroutines tab lists every coroutine’s state (running, suspended, created) grouped by dispatcher and job hierarchy, which a plain thread view cannot show because many coroutines share the same thread pool. Stepping through a suspend function such as
OfflineFirstTrailRepository.refreshTrailsfollows the logical coroutine, not the thread it happens to resume on. -
Conditional and logging breakpoints. A breakpoint can carry a condition expression (
trailId == "t-42") so it only suspends for one item in a loop over trails, or be turned into a non-suspending "logpoint" that prints an expression to Logcat without stopping execution — useful when stepping throughTrailListViewModel.uiStateconstruction would otherwise interfere with theWhileSubscribed(5_000)timing being investigated.
Logcat
Logcat is the system-wide log stream: every Log.d/Log.w/Log.e call, plus framework and system messages
(activity lifecycle transitions, ANR reports, uncaught exceptions with full stack traces), tagged by process,
priority level and tag string. Android Studio’s Logcat window filters by package (scoped to TrailMate’s process
automatically), by tag, by priority, and by a search query; saved filters let a session jump straight to, for
example, tag:TrailSyncWorker level:WARN while debugging a sync failure.
// core/data/src/main/kotlin/com/example/trailmate/core/data/sync/TrailSyncWorker.kt
class TrailSyncWorker(
context: Context,
params: WorkerParameters,
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
return try {
trailRepository.refreshTrails()
Result.success()
} catch (e: IOException) {
Log.w(TAG, "Trail sync failed, will retry", e)
Result.retry()
}
}
companion object {
private const val TAG = "TrailSyncWorker"
}
}
See Android Developers — Write and view logs with Logcat and
Android Developers — Android Debug Bridge (adb) (adb logcat is the
command-line equivalent, useful in CI and shell scripts). A common mistake is leaving verbose Log.d calls with
string concatenation in a hot path such as a RecyclerView.onBindViewHolder or a Compose recomposition scope — even when the log level is filtered out on screen, the string is still built on every call unless guarded or
removed. Never log tokens, passwords or full user PII to Logcat; on a release build, strip debug/verbose logging
with R8 rules (see R8 App Optimization) rather than relying on log level alone.
Layout Inspector
The Layout Inspector attaches to a running app and renders a live, inspectable view of what is actually on
screen — not what the source code says should be there. For a legacy TrailListFragment screen it shows the
full View hierarchy in 3D, with each view’s resolved layout params, padding and resource IDs, which catches
issues like an unexpected ConstraintLayout bias or a view that is present but has zero size. For Compose, it
shows the composition tree (TrailListScreen → LazyColumn → each TrailListItem call site) with each
composable’s resolved parameter values — the actual Trail passed to a given TrailListItem, not just its
declared type.
Two Compose-specific features make it central to debugging performance problems on this page:
-
Recomposition counts. Enabling Show Recomposition Counts annotates each composable in the tree with how many times it recomposed and skipped since the inspector attached. A
TrailListItemcount climbing on every keystroke in an unrelated search field is the first, fastest signal of an over-broad state read; the why — unstable parameter types, alambdacapturing more than it needs, missing@Stable/@Immutable— is explained on State, Recomposition and Side Effects in Compose. -
System UI and view attribute inspection, including live editing of some attributes for quick experiments without a rebuild.
App Inspection
The App Inspection tool window groups several live inspectors that attach to a debuggable process without any extra code, backed by the AndroidX Inspection libraries:
| Inspector | What it shows |
|---|---|
Database Inspector |
A live, queryable view of TrailMate’s Room database ( |
Network Inspector |
Every OkHttp/Retrofit (and Ktor-over-OkHttp) request TrailMate’s |
Background Task Inspector |
The state and history of every |
See Android Developers — Inspect database data with
Database Inspector, Android Developers — Inspect network
traffic with Network Inspector and Android Developers — Inspect WorkManager jobs with Background Task Inspector. All three only work against a debuggable process on
API 26+, which matches TrailMate’s minSdk.
StrictMode
StrictMode is a developer-only guard that detects accidental main-thread I/O and resource leaks by throwing or
logging when a violation happens, rather than letting it silently degrade frame time. It is wired up once, in
TrailMateApplication.onCreate, guarded so it never ships to production:
// app/src/main/kotlin/com/example/trailmate/TrailMateApplication.kt
@HiltAndroidApp
class TrailMateApplication : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) {
StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build(),
)
StrictMode.setVmPolicy(
StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.penaltyLog()
.build(),
)
}
}
}
See Android Developers — StrictMode. A
ThreadPolicy violation logged here — for example a SharedPreferences read from TrailListFragment.onCreate
before DataStore fully replaced it — almost always maps to real jank on lower-end devices even if it is not
visible on a fast development phone.
LeakCanary
LeakCanary is a memory-leak detection library (Square) added only to the debugImplementation configuration, so
it never ships in a release build. It hooks Activity/Fragment/ViewModel destruction, waits for garbage
collection, and if an instance that should have been collected is still reachable, walks the reference chain and
shows it as a notification with the full path to the GC root — for example a TrailDetailFragment retained
because RecordHikeService held an anonymous listener referencing its view.
// build.gradle.kts (:app)
dependencies {
debugImplementation("com.squareup.leakcanary:leakcanary-android:2.14")
}
See Android Developers — Reduce memory leaks with
LeakCanary and LeakCanary documentation. No manual wiring is required
beyond the dependency; LeakCanary installs itself via a ContentProvider at app startup. The most common source
of a TrailMate leak is a Job/Flow collection started with a lifecycle-unaware CoroutineScope, or a
long-lived singleton (a Hilt @Singleton repository) holding a reference to something scoped to an activity or
fragment — both are covered from the coroutine-scoping side on
ViewModel, Lifecycle-Aware Components, Coroutines and
Flow.
Chucker
Chucker is an in-app HTTP inspector: an OkHttp interceptor that captures every request/response TrailMate’s
TrailApi makes and surfaces them through a persistent notification and a small in-app UI, browsable directly on
the device — useful for inspecting traffic on a physical device with no Android Studio attached, or for handing
a QA build to someone outside the team without exposing Android Studio’s Network Inspector.
// build.gradle.kts (:core:network)
dependencies {
debugImplementation("com.chuckerteam.chucker:library:4.2.0")
releaseImplementation("com.chuckerteam.chucker:library-no-op:4.2.0")
}
// core/network/src/main/kotlin/com/example/trailmate/core/network/NetworkModule.kt
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(@ApplicationContext context: Context): OkHttpClient =
OkHttpClient.Builder()
.addInterceptor(ChuckerInterceptor.Builder(context).build())
.build()
}
See Chucker on GitHub. The library-no-op artifact is a drop-in,
no-behavior substitute used for releaseImplementation, so the interceptor call above compiles in release builds
without ever recording traffic there. Chucker and the Network Inspector solve the same problem from different
angles — Chucker needs an explicit interceptor and works without Android Studio; the Network Inspector needs
Android Studio and no code changes.
Code Quality
Automated code-quality checks catch a class of bugs and inconsistencies that tests do not: unused resources, suspicious API usage flagged by the platform itself, and style drift across a growing team. TrailMate wires three tools into its Gradle build so violations surface in every local build and in CI, not only when someone remembers to run them by hand.
Android Lint
Android Lint ships with the Android Gradle Plugin and analyzes both Kotlin/Java source and Android-specific
files (manifest, resources, Gradle files) for problems a generic Kotlin compiler cannot see: a missing permission
declaration, an unused resource, a hardcoded string that should be externalized for localization, an
ImageView missing a contentDescription, a call into an API level higher than minSdk without a guard, or an
overload that is deprecated in favor of a newer one. Each check has a severity (error, warning, informational)
and can be tuned per-project:
// app/build.gradle.kts
android {
lint {
warningsAsErrors = false
checkReleaseBuilds = true
disable += setOf("MissingTranslation")
baseline = file("lint-baseline.xml")
}
}
Run it with ./gradlew lint (or lintDebug/lintRelease for a single variant); it produces an HTML and XML
report under app/build/reports/lint-results-<variant>.html. baseline.xml freezes today’s existing findings so
CI only fails on new ones — essential when adopting Lint on a codebase that already has a backlog, which keeps
the check useful without requiring an unrelated cleanup before it can be enabled.
ktlint
ktlint enforces the official Kotlin style guide (kotlinlang.org’s coding conventions) as a formatter and linter: indentation, import ordering, trailing commas, blank-line rules. Because it has effectively no configuration surface by design, it removes style debates from code review entirely — either the formatter’s output matches what is checked in, or it does not. TrailMate applies it through the community Gradle plugin:
// build.gradle.kts (root)
plugins {
id("org.jlleitschuh.gradle.ktlint") version "13.1.0"
}
./gradlew ktlintCheck fails the build on a violation; ./gradlew ktlintFormat rewrites files in place to
match. Wiring ktlintFormat as a pre-commit hook (or ktlintCheck as a required CI step) keeps formatting
arguments out of pull request reviews, since a diff that only reflects a formatter’s output rarely needs
discussion. See ktlint documentation and
Kotlin coding conventions.
detekt
detekt is a static-analysis tool for Kotlin that goes beyond formatting into code smells and complexity:
long methods, deeply nested conditionals, magic numbers, !! non-null assertions, unused private members, and
custom rules a team defines for its own conventions (for example, forbidding direct Context injection into a
ViewModel). Findings are grouped into rule sets with configurable severities, and a detekt.yml file tunes
which rules apply and their thresholds:
# config/detekt/detekt.yml
complexity:
LongMethod:
threshold: 40
TooManyFunctions:
active: false
style:
ForbiddenComment:
comments:
- reason: 'Forbidden TODO marker, please fix or file a ticket.'
value: 'TODO:'
// build.gradle.kts (root)
plugins {
id("io.gitlab.arturbosch.detekt") version "1.23.8"
}
detekt {
config.setFrom(file("config/detekt/detekt.yml"))
buildUponDefaultConfig = true
}
./gradlew detekt runs the analysis and writes a baseline the same way Lint does
(detektBaseline/baseline.setFrom(…)), for the same reason: adopt it on an existing codebase without a
mandatory full cleanup first. detekt also ships a Compose rule set (detekt-compose) that flags Compose-specific
smells such as a composable that is not named in PascalCase, or Modifier not accepted as the first optional
parameter. See detekt documentation. ktlint and detekt are complementary, not overlapping:
ktlint governs how code is formatted, detekt governs how it is structured.
Profiling and Performance
Android Studio Profiler
The Profiler window attaches to a running (debuggable or profileable) process and gives real-time graphs for CPU, memory, and network use, each drillable into a dedicated recording:
-
CPU Profiler. Records a System Trace (system-wide, backed by Perfetto — see below), a Callstack Sample (periodic stack sampling, low overhead, statistical), or Java/Kotlin Method Trace (full instrumentation, exact call counts and timings, higher overhead). For diagnosing why
TrailListViewModel’s `combineblock is slow to produce its firstSuccessstate, a System Trace correlates CPU work with the exact frame timeline, while a Method Trace gives exact time spent insideOfflineFirstTrailRepository.refreshTrailsversusTrailDaoquery execution. -
Memory Profiler. Shows live allocated memory over time, and can trigger a heap dump — a full snapshot of every object on the Kotlin/Java heap at that instant, browsable by class, with retained size and the shortest path to a GC root for any instance. A rising sawtooth that never returns to baseline after repeated navigation into and out of
TrailDetailScreenis the classic signature of a leak that LeakCanary would also catch automatically, but the Memory Profiler lets the count and retained size be inspected interactively for cases LeakCanary’s heuristics do not flag. -
Network Profiler. A lighter-weight, timeline view of TrailMate’s HTTP traffic; the Network Inspector (App Inspection, above) is preferred for actually reading request/response bodies.
Perfetto and System Traces
Perfetto is the underlying system-tracing infrastructure the CPU Profiler’s System Trace mode is built on, and it
also runs standalone (https://ui.perfetto.dev, or adb shell perfetto) to capture a trace outside Android
Studio — for example from a CI-run Macrobenchmark, or a field device. A trace captures every thread’s scheduling
state, binder transactions, and any custom trace sections an app adds with androidx.tracing.Trace.beginSection/
endSection, all on one zoomable timeline, which is what makes it possible to see, in one view, that a dropped
frame in TrailListScreen coincided with a TrailDao query running on the main thread rather than a background
dispatcher.
// feature/trails/src/main/kotlin/com/example/trailmate/feature/trails/TrailListViewModel.kt
import androidx.tracing.traceAsync
private var loadTrailsCookie = 0
fun loadTrails() {
viewModelScope.launch {
// refreshTrails() suspends and may resume on another thread, so use an async section
traceAsync("TrailListViewModel#loadTrails", cookie = loadTrailsCookie++) {
trailRepository.refreshTrails()
}
}
}
See Android Developers — Overview of app tracing and
Android Developers — Add custom trace
events. androidx.tracing:tracing (which since 1.3.0 also contains the former tracing-ktx helpers) provides
two block wrappers over the beginSection/endSection pair. trace("label") { } opens a synchronous section,
which must begin and end on the same thread, so it only suits a block that doesn’t suspend. A suspend call such as
refreshTrails() can suspend on one thread and resume on another (for example after a withContext(Dispatchers.IO)),
which would close the section on the wrong thread and corrupt the trace. The suspend overload
traceAsync(methodName, cookie) { } records an async section via Trace.beginAsyncSection/endAsyncSection
instead, which isn’t tied to a thread; the cookie must be unique among overlapping sections with the same name,
which is why each call above gets its own value. See
Android Developers — androidx.tracing.
ANRs: Application Not Responding
The system shows an ANR dialog when the main thread does not respond to input within its timeout (roughly five
seconds for most kinds of input dispatch, shorter for a broadcast receiver or service). The most common causes in
an app like TrailMate are a synchronous database or network call made directly from the main thread (rather than
through viewModelScope/Dispatchers.IO), a main-thread Binder call taking too long, or a deadlock between
the main thread and a background thread holding a lock it needs. Logcat records an ANR’s full trace (every
thread’s stack at the moment of the freeze) even without Android Studio attached, which is the primary tool for
diagnosing one that happened on a user’s device rather than during development. StrictMode (above) and
avoiding any I/O in a ViewModel constructor or a Compose composition are the main preventive measures; App
Inspection’s Background Task Inspector rules out a stuck WorkManager job as the cause.
Rendering and Jank
Android renders at a fixed refresh interval (typically every ~16.6 ms at 60 Hz, ~8.3 ms at 120 Hz); a frame that
takes longer than that to produce is dropped, and the previous frame is shown again — perceived as jank, a
stutter in scrolling or animation. The Layout Inspector and Profiler’s rendering overlays (and, historically,
Developer Options' Profile HDR rendering/GPU rendering profile) show per-frame timing broken into stages
(input handling, layout/measure, draw, GPU work), which localizes a dropped frame to, for example, an expensive
Modifier.drawWithCache recomputation in TrailListItem, or a RecyclerView binding doing a synchronous Glide
disk read on the main thread in TrailViewHolder.bind. Compose recomposition counts (Layout Inspector, above)
and stability annotations (covered on
State, Recomposition and Side Effects in Compose) are the
Compose-specific half of this same problem: an over-broad recomposition scope produces the same symptom — a
dropped frame — as an expensive draw call.
App Startup
Startup is classified by what state the process was in beforehand:
| Type | What happens |
|---|---|
Cold start |
The system creates a new process: |
Warm start |
The process already exists (backgrounded, not killed) but the activity has to be recreated; skips
|
Hot start |
The activity is simply brought back to the foreground; the fastest case, with the least room for a developer to influence timing. |
Application.onCreate and a launching activity’s onCreate/onResume are the two places TrailMate’s own code
can add cold-start latency — most often through eager singleton initialization (Hilt component graph
construction, a ContentProvider-based library initializer, or an eagerly created OkHttp client). The
App Startup library (androidx.startup) consolidates multiple ContentProvider-based initializers into a
single one, and lets TrailMate declare explicit ordering and lazy/on-demand initialization instead of relying on
each dependency’s own provider running independently at process start:
// core/data/src/main/kotlin/com/example/trailmate/core/data/startup/TrailDatabaseInitializer.kt
class TrailDatabaseInitializer : Initializer<TrailMateDatabase> {
override fun create(context: Context): TrailMateDatabase =
TrailMateDatabase.getInstance(context)
override fun dependencies(): List<Class<out Initializer<*>>> = emptyList()
}
<!-- app/src/main/AndroidManifest.xml -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="com.example.trailmate.core.data.startup.TrailDatabaseInitializer"
android:value="androidx.startup" />
</provider>
See Android Developers — App Startup and
Android Developers — App startup time. The
Profiler’s System Trace, or adb shell am start -W, measures actual cold-start time end to end; see
Android Developers — Analyze
and optimize your app’s startup time for the full diagnostic workflow.
Baseline Profiles
The Android Runtime (ART) compiles code just-in-time on first run and ahead-of-time in the background once a device is idle; a Baseline Profile is a list of classes and methods TrailMate declares as performance-critical (startup, or a frequently-scrolled screen), shipped inside the app so ART pre-compiles exactly those paths at install time instead of waiting for repeated interpretation or a later background compile. This removes a measurable amount of cold-start and first-scroll jank for the very first sessions a real user has with the app — precisely when a bad first impression is most costly.
A Baseline Profile is generated, not hand-written, using the androidx.baselineprofile Gradle plugin and a
dedicated benchmark module that exercises the critical paths under instrumentation:
// benchmark/src/main/kotlin/com/example/trailmate/benchmark/BaselineProfileGenerator.kt
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator {
@get:Rule
val rule = BaselineProfileRule()
@Test
fun generate() = rule.collect(packageName = "com.example.trailmate") {
pressHome()
startActivityAndWait()
device.findObject(By.text("Trails")).click()
device.waitForIdle()
}
}
Running it produces a baseline-prof.txt merged into :app’s release build automatically by the
`androidx.baselineprofile plugin. See
Android Developers — Baseline
profiles and
Android Developers — Create Baseline Profiles.
Macrobenchmark and Microbenchmark
Both are androidx.benchmark JUnit test types that produce statistically stable, regression-trackable
measurements instead of one-off manual timing:
-
Macrobenchmark measures end-to-end, black-box scenarios on a real or emulated device: cold-start time, scroll jank on
TrailListScreen, or the effect a Baseline Profile has (it can run the same scenarioCompilationMode.None()versusCompilationMode.Partial()and report the delta directly). -
Microbenchmark measures a single function’s execution time in tight isolation, with JIT warm-up and statistical outlier handling — for example, comparing two implementations of a
Traillist sort/filter operation used insideTrailListViewModel.
// benchmark/src/main/kotlin/com/example/trailmate/benchmark/TrailListStartupBenchmark.kt
@RunWith(AndroidJUnit4::class)
class TrailListStartupBenchmark {
@get:Rule
val benchmarkRule = MacrobenchmarkRule()
@Test
fun startup() = benchmarkRule.measureRepeated(
packageName = "com.example.trailmate",
metrics = listOf(StartupTimingMetric()),
iterations = 5,
startupMode = StartupMode.COLD,
) {
pressHome()
startActivityAndWait()
}
}
Both run in a dedicated Gradle module (conventionally :benchmark) rather than :app, and both report results
that can be tracked over time in CI to catch a performance regression the same way a failing test catches a
correctness regression. See
Android Developers — Benchmarking and
Android Developers — Macrobenchmark and
Android Developers — Microbenchmark.
R8 App Optimization
R8 is the AGP’s default compiler and optimizer for release builds: it shrinks (removes unreachable code and resources), obfuscates (renames classes/members to shorter names, harder to reverse-engineer), and optimizes (inlines, removes dead branches, rewrites bytecode for smaller/faster execution). Beyond the security-oriented obfuscation covered on Permissions, Privacy and Security, the performance angle is app size and method count: a smaller APK/AAB installs faster, downloads faster on constrained networks, and a smaller DEX reduces the amount of code ART has to JIT/AOT compile, which feeds back into cold-start time.
// app/build.gradle.kts
android {
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro",
)
}
}
}
For a library module such as :core:network, R8’s library optimization mode (android.experimental.
com.android.libraryOptimization in newer AGP, replacing the older consumerProguardFiles-only model) applies
the same shrinking/optimization to code that is only ever consumed by :app, rather than deferring all of it to
the final app module. See
Android Developers — Shrink, obfuscate, and optimize your app and
Android Developers — Optimize library code with R8. R8’s shrinking and Baseline Profiles' pre-compilation are independent and
compatible — one changes what ships, the other changes when the shipped code gets compiled by ART — and a
release build normally applies both.
Production Monitoring
Everything above runs during development or in CI, against a known set of devices. Once TrailMate is in users' hands, the same signals have to come from real sessions on real, unpredictable hardware.
Android Vitals
Android vitals is Google Play’s own aggregation of technical quality signals across every device running TrailMate, visible in the Play Console with no extra SDK required: ANR rate, crash rate, excessive wakeups, stuck partial wake locks, and (where available) startup time and slow/frozen frame rates. Play uses vitals thresholds — crash rate and ANR rate above Google’s "bad behavior" bar — to affect store visibility (a poor rate can reduce an app’s ranking or trigger a warning), which makes vitals both a debugging signal and a distribution-relevant one.
Firebase Crashlytics
Crashlytics is a crash- and non-fatal-error-reporting SDK: it captures every uncaught exception (and, if
reported explicitly with recordException, handled ones) with a full, de-obfuscated stack trace (using the
mappingFileUpload/R8 mapping file uploaded from the release build), groups crashes into issues by signature,
and tracks how many users and sessions each issue affects across app versions.
// core/data/src/main/kotlin/com/example/trailmate/core/data/repository/OfflineFirstTrailRepository.kt
override suspend fun refreshTrails() {
try {
val response = trailApi.getTrails()
trailDao.upsertAll(response.map { it.toEntity() })
} catch (e: IOException) {
FirebaseCrashlytics.getInstance().recordException(e)
throw e
}
}
FirebaseCrashlytics.getInstance().setCustomKey(…)/setUserId(…) (a hashed or internal identifier, never a
real name or email) attaches context to a report — for example the active Units preference or onlyFavorites
filter at crash time — which narrows down a report that only reproduces under a specific combination of state.
Firebase Performance Monitoring
Performance Monitoring is Crashlytics’s counterpart for latency rather than crashes: it automatically traces app start time, screen rendering (slow/frozen frames per screen), and every network request made through a supported HTTP client, and lets custom traces wrap any operation worth tracking end to end from real user sessions:
// feature/trails/src/main/kotlin/com/example/trailmate/feature/trails/TrailListViewModel.kt
fun loadTrails() {
val trace = Firebase.performance.newTrace("trail_list_load")
trace.start()
viewModelScope.launch {
try {
trailRepository.refreshTrails()
} finally {
trace.stop()
}
}
}
Both Crashlytics and Performance Monitoring aggregate across the full install base the way Baseline Profiles, Macrobenchmark and the Profiler cannot — those tools measure known scenarios on known devices before release, while Crashlytics and Performance Monitoring reveal what actually happens on devices, OS versions and network conditions the team never explicitly tested. See Firebase Documentation — Crashlytics and Firebase Documentation — Performance Monitoring.
References
-
Android Developers — Inspect database data with Database Inspector
-
Android Developers — Inspect network traffic with Network Inspector
-
Android Developers — Inspect WorkManager jobs with Background Task Inspector
-
Android Developers — Analyze and optimize your app’s startup time
-
Android Developers — Shrink, obfuscate, and optimize your app
-
detekt Documentation — Run detekt using the Detekt Gradle Plugin