This blog will walk you through the step-by-step process of building a scalable and testable analytics logging architecture in Android.
We'll explore its design, implementation, and best practices, empowering you to create your own robust solution.
A big thanks to NowInAndroid Repository for providing the code example used in this blog, which was taken from their open-source project.
The architecture centers around the AnalyticsHelper
interface, which abstracts the actual logging mechanism. Multiple implementations of this interface handle different scenarios, such as:
NoOpAnalyticsHelper: A no-operation implementation for testing and previews.
TestAnalyticsHelper: A mock implementation to verify logging behavior in unit tests.
StubAnalyticsHelper: A simple implementation for debugging in local environments.
FirebaseAnalyticsHelper: The actual implementation using Firebase Analytics for production.
This setup adheres to the Dependency Inversion Principle (DIP) of SOLID, enabling modular, scalable, and testable code.
The AnalyticsHelper
interface ensures that all implementations conform to a standard contract:
interface AnalyticsHelper {
fun logEvent(event: AnalyticsEvent)
}
This interface allows us to inject different implementations depending on the environment (e.g., production, testing).
This implementation does nothing and is useful for tests or when no logging is needed:
class NoOpAnalyticsHelper : AnalyticsHelper {
override fun logEvent(event: AnalyticsEvent) = Unit
}
Used in unit tests to verify if specific events are logged:
class TestAnalyticsHelper : AnalyticsHelper {
private val events = mutableListOf<AnalyticsEvent>()
override fun logEvent(event: AnalyticsEvent) {
events.add(event)
}
fun hasLogged(event: AnalyticsEvent) = event in events
}
Logs events to the console for debugging:
@Singleton
internal class StubAnalyticsHelper @Inject constructor() : AnalyticsHelper {
override fun logEvent(event: AnalyticsEvent) {
Log.d(TAG, "Received analytics event: $event")
}
}
The production implementation that logs events to Firebase Analytics:
internal class FirebaseAnalyticsHelper @Inject constructor(
private val firebaseAnalytics: FirebaseAnalytics,
) : AnalyticsHelper {
override fun logEvent(event: AnalyticsEvent) {
firebaseAnalytics.logEvent(event.type) {
for (extra in event.extras) {
param(
key = extra.key.take(40),
value = extra.value.take(100),
)
}
}
}
}
The AnalyticsEvent
class represents each logged event:
data class AnalyticsEvent(
val type: String,
val extras: List<Param> = emptyList(),
) {
data class Param(val key: String, val value: String)
}
This structure ensures consistency and type safety in event logging.
Use Jetpack Compose's CompositionLocal
to provide a default AnalyticsHelper
:
val LocalAnalyticsHelper = staticCompositionLocalOf<AnalyticsHelper> {
NoOpAnalyticsHelper()
}
This approach ensures that tests and previews do not require manual AnalyticsHelper
injection.
How to use:
@Inject
lateinit var analyticsHelper: AnalyticsHelper
setContent {
CompositionLocalProvider(
LocalAnalyticsHelper provides analyticsHelper
) { NiaTheme() ... }
}
Use Hilt to provide the appropriate implementation of AnalyticsHelper
:
@Module
@InstallIn(SingletonComponent::class)
object AnalyticsModule {
@Provides
fun provideAnalyticsHelper(
firebaseAnalytics: FirebaseAnalytics,
): AnalyticsHelper = FirebaseAnalyticsHelper(firebaseAnalytics)
}
Create extensions for your features AnalyticsExtensions
:
internal fun AnalyticsHelper.logNewsResourceBookmarkToggled(newsResourceId: String, isBookmarked: Boolean) {
val eventType = if (isBookmarked) "news_resource_saved" else "news_resource_unsaved"
val paramKey = if (isBookmarked) "saved_news_resource_id" else "unsaved_news_resource_id"
logEvent(
AnalyticsEvent(
type = eventType,
extras = listOf(
Param(key = paramKey, value = newsResourceId),
),
),
)
}
and use it via analyticsHelper
instance:
@Inject
lateinit var analyticsHelper: AnalyticsHelper
Interface-Based Architecture: By using the AnalyticsHelper
interface, we decouple the logging logic from the rest of the app, adhering to the Dependency Inversion Principle of SOLID.
Data Class for Events: The AnalyticsEvent
data class ensures type safety and enforces a consistent structure for events.
Dynamic Flexibility vs. Type Safety: Representing AnalyticsEvent.extras
as a List<Param>
provides flexibility but lacks the strong typing of a map.
Data Integrity: Truncating keys and values for Firebase limits can cause discrepancies when comparing data across platforms.
Performance: Logging events on the main thread can cause bottlenecks. Always offload logging to background threads using coroutines:
CoroutineScope(Dispatchers.IO).launch {
analyticsHelper.logEvent(event)
}
Batching Logs: In high-traffic apps, batching events can reduce network overhead and improve performance.
Error Handling: Implement a retry mechanism for failed logs due to network issues.
Avoid Logging PII: Ensure no personally identifiable information (PII) like emails or phone numbers is logged. Use hashed or anonymized values if needed.
Unit Testing: Use TestAnalyticsHelper
to verify that specific events are logged during tests:
val testHelper = TestAnalyticsHelper()
val event = AnalyticsEvent(type = "test_event")
testHelper.logEvent(event)
assertTrue(testHelper.hasLogged(event))
Integration Testing: Use tools like Firebase Test Lab to inspect analytics logs in real-world scenarios.
Global Event Queue: Add a queue to handle retrying failed logs during intermittent network issues.
Custom Sampling Rates: Control the volume of logged events by introducing sampling rates for high-frequency events.
With a clean, modular architecture like this, your analytics logging system becomes a powerful tool for understanding user behavior. Start small, keep your implementation testable, and iterate as your app grows. Remember, analytics is not just about logging events—it's about creating actionable insights that drive meaningful decisions.
By following the steps and best practices outlined in this guide, you can create a robust analytics logging system tailored to your app's needs.
Akshay Nandwana
Founder AndroidEngineers
You can connect with me on:
Join our upcoming classes
https://www.androidengineers.in/courses