Jetpack Compose is Android’s recommended toolkit for building native UIs, representing the platform’s demonstrative shift from imperative to declarative UIs. Google is making a big push to drive adoption, and it’s paying off. As announced at the Android Dev Summit ’22 last October, 160 of the top 1,000 apps on the Google Play store are shipping Jetpack Compose, including companies like Airbnb, Lyft, and Square.Jetpack Compose offers many benefits—it’s more intuitive, requires less code, and accelerates development. But it’s not without its challenges. Moving from an imperative toolkit to Jetpack Compose comes with a learning curve, which is exacerbated by limited documentation, a smaller community, and performance issues.Sentry recently announced their support of Jetpack Compose, with an out-of-the-box integration that allows developers to quickly identify and solve issues in their application. Here’s exactly how Sentry helps teams get started with Jetpack Compose.Start with Android StudioIf you are building a new application from scratch with Jetpack Compose, first download and install Android Studio, an integrated development environment (IDE) optimized for Android apps. Then, create a new project and select either the Empty Compose Activity, which uses Material v2, or Empty Compose Activity (Material3), which uses Material v3. You can see both options in the top right of this screenshot: SentryIf you’d like to integrate Jetpack Compose into an existing Android application, add the following build configurations in your app’s build.gradle file.android {
buildFeatures {
// this flag enables Jetpack Compose
compose true
}
composeOptions {
// the compiler version should match
// your project’s Kotlin version
kotlinCompilerExtensionVersion = “1.3.2”
}
}
Then, add the Compose BOM (Bill of Materials) and the subset of Compose dependencies to your dependencies. dependencies {
def composeBom = platform(‘androidx.compose:compose-bom:2023.01.00’)
implementation composeBom
androidTestImplementation composeBom
// Choose one of the following:
// Material Design 3
implementation ‘androidx.compose.material3:material3’
// or Material Design 2
implementation ‘androidx.compose.material:material’
// or skip Material Design and build directly on top of foundational components
implementation ‘androidx.compose.foundation:foundation’
// or only import the main APIs for the underlying toolkit systems,
// such as input and measurement/layout
implementation ‘androidx.compose.ui:ui’
// Android Studio Preview support
implementation ‘androidx.compose.ui:ui-tooling-preview’
debugImplementation ‘androidx.compose.ui:ui-tooling’
// UI Tests
androidTestImplementation ‘androidx.compose.ui:ui-test-junit4’
debugImplementation ‘androidx.compose.ui:ui-test-manifest’
// Optional – Included automatically by material, only add when you need
// the icons but not the material library (e.g. when using Material3 or a
// custom design system based on Foundation)
implementation ‘androidx.compose.material:material-icons-core’
// Optional – Add full set of material icons
implementation ‘androidx.compose.material:material-icons-extended’
// Optional – Add window size utils
implementation ‘androidx.compose.material3:material3-window-size-class’
// Optional – Integration with activities
implementation ‘androidx.activity:activity-compose:1.5.1’
// Optional – Integration with ViewModels
implementation ‘androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1’
// Optional – Integration with LiveData
implementation ‘androidx.compose.runtime:runtime-livedata’
// Optional – Integration with RxJava
implementation ‘androidx.compose.runtime:runtime-rxjava2’
}
Integrate SentryTo integrate Sentry into your new Jetpack Compose app, all you need to do is add Sentry’s Gradle plugin in your module’s build.gradle file and perform a Gradle sync afterwards.buildscript {
repositories {
mavenCentral()
}
}
plugins {
id “com.android.application”
id “io.sentry.android.gradle” version “3.4.2”
}
And then add the necessary values in the AndroidManifest.xml file. <application>
<!– Required: set your sentry.io project identifier (DSN) –>
<meta-data android:name=”io.sentry.dsn” android:value=”https://examplePublicKey@o0.ingest.sentry.io/0″ />
<!– enable automatic breadcrumbs for user interactions (clicks, swipes, scrolls) –>
<meta-data android:name=”io.sentry.traces.user-interaction.enable” android:value=”true” />
<!– enable screenshot for crashes –>
<meta-data android:name=”io.sentry.attach-screenshot” android:value=”true” />
<!– enable view hierarchy for crashes –>
<meta-data android:name=”io.sentry.attach-view-hierarchy” android:value=”true” />
<!– enable the performance API by setting a sample-rate, adjust in production env –>
<meta-data android:name=”io.sentry.traces.sample-rate” android:value=”1.0″ />
<!– enable profiling when starting transactions, adjust in production env –>
<meta-data android:name=”io.sentry.traces.profiling.sample-rate” android:value=”1.0″ />
</application>
These two steps install and configure Sentry into your project. Aside from error reporting, your project now also has automatically instrumented performance monitoring. The Sentry SDK will automatically collect and analyze performance profiles so you can see how your application performs on different user devices in production.Capturing errorsBy default, Sentry captures all errors and crashes automatically for you. If you want to capture errors and exceptions manually, you can use the captureException method.import io.sentry.Sentry
try {
aMethodThatMightFail()
} catch (e: Exception) {
Sentry.captureException(e)
}
Adding contextYou have the option to add additional context to all of the errors that happen within your app. That’s arbitrary data that automatically gets attached to the event, and is viewable on the issue details page. To do that, we can attach custom contexts on the current scope like this:import io.sentry.Sentry
Sentry.configureScope { scope ->
scope.setContexts(“Hero Details”, mapOf(
“Name” to “Mighty Fighter”,
“Age” to 19,
“Attack type” to “Melee”,
))
}
This data will now be appended to each issue. We can check it out at the issue details page: SentryImportant: There are two limitations you should be aware of. Namely:
This data is not searchable. It’s only used to attach values to the events. If you need to be able to search on custom data, use tags instead.
There are size limitations on how much data you can add to the scope. Sentry does not recommend sending the entire application state and large data blobs in contexts. In the event of appending too much data, Sentry will respond with the HTTP error “413 Payload Too Large” and reject the event.
Adding tagsJust like the additional context, we can also add custom tags on your events, which by contrast are indexed and searchable. You can use tags to quickly access related events and view the tag distribution for a set of events. Common uses for tags include hostname, platform version, and user language.Adding tags is very similar to adding additional contexts. They’re key-value pairs and they can be added to the current scope by using the setTag method.import io.sentry.Sentry
Sentry.configureScope { scope ->
scope.setTag(“user-type”, “premium”)
}
SentryAs mentioned, tags are indexed and searchable, so if you add “user-type:premium” in the Custom Search field in the Issues page you’ll see all of the issues that have that tag: SentryThree things to be aware of when working with tags:
Sentry automatically adds some tags to every issue. It is not a good idea to overwrite those tags. Instead, name your tags using your organization’s nomenclature.
The keys have a maximum length of 32 characters and they can contain only letters, numbers, underscores, periods, colons, and dashes.
The values have a maximum length of 200 characters and they cannot contain the newline (n) character.
Adding attachmentsAdding attachments is yet another way to supplement the events with additional data, and it’s the recommended way if you need to add larger data than contexts and tags. Attachments can be any type of file.To add an attachment, you can either add it to the scope, pass it to any of the capture methods, or manipulate the list of attachments in an EventProcessor or beforeSend.Some rules you need to be aware of when working with attachments:Passing attachments to capture methodsThis is probably the simplest way to add attachments. Whenever you’re using one of the capture methods, you can append the attachment as the second argument by using the Hint.withAttachment method.import io.sentry.Attachment
import io.sentry.Hint
import io.sentry.Sentry
…
try {
…
} catch (e: Exception) {
Sentry.captureException(e, Hint.withAttachment(“/path/to/file.txt”))
}
Adding attachments in beforeSendAnother way of adding attachments is using the beforeSend callback.import io.sentry.Sentry
import io.sentry.SentryOptions.BeforeSendCallback
import io.sentry.Hint
import io.sentry.Attachment
Sentry.init(this) { options ->
options.beforeSend = BeforeSendCallback { event, hint ->
hint.addAttachment(Attachment(“/path/to/file.txt”))
}
}
This configuration will add the file.txt file to every issue before sending it to the cloud.Viewing attachmentsYou can see the attachments for a given issue at the bottom of the issue details page. There’s an Attachments section that lists all attachments and you have the option to delete, download, or preview them. SentryYou can also access attachments through the Attachments tab on the same page, where you can view the type of attachment, as well as associated events. You can click the Event ID to open the Issue Details of that specific event. SentryMeasuring performanceIf you’ve provided the Sample Rate value (io.sentry.traces.sample-rate) in your AndroidManifest.xml file, then you’ve already configured Sentry to automatically instrument your application. Sentry will automatically capture transactions for lifecycle events of activities and fragments, cold and warm app start, slow and frozen frames, and other events.It’s also possible to manually instrument a specific function, for example a function that spends some time reshaping a large chunk of data, or a function that obtains data from an API and puts it in the local storage, etc. In order to create custom instrumentations, you’d need to start a transaction by calling the startTransaction method.import io.sentry.Sentry
import io.sentry.SpanStatus
val transaction = Sentry.startTransaction(“processOrderBatch()”, “task”)
try {
processOrderBatch()
} catch (e: Exception) {
transaction.throwable = e
transaction.status = SpanStatus.INTERNAL_ERROR
throw e
} finally {
transaction.finish()
}
Warning: Don’t forget to call the finish() method on the transaction, otherwise the transaction won’t be sent to Sentry.If the function you’re trying to instrument is more complex and involves multiple sub-functions that you’d prefer to instrument individually, you can create child spans for each of them and attach them to the main transaction.import io.sentry.SpanStatus
val transaction = Sentry.startTransaction(“processOrderBatch()”, “task”)
try {
processOrderBatch(transaction)
} catch (e: Exception) {
transaction.throwable = e
transaction.status = SpanStatus.INTERNAL_ERROR
throw e
} finally {
transaction.finish()
}
fun processOrderBatch(span: ISpan) {
// span operation: task, span description: operation
val innerSpan = it.startChild(“task”, “operation”)
try {
// omitted code
} catch (e: FileNotFoundException) {
innerSpan.throwable = e
innerSpan.status = SpanStatus.NOT_FOUND
throw e
} finally {
innerSpan.finish()
}
}
Don’t forget to call the finish() method on each of the spans before calling the main transaction’s finish() method! If you don’t, they won’t be attached to the main transaction.