Skip to content

Compose code for analytics#1654

Open
lehcar09 wants to merge 3 commits intofirebase:composefrom
lehcar09:compose-ga
Open

Compose code for analytics#1654
lehcar09 wants to merge 3 commits intofirebase:composefrom
lehcar09:compose-ga

Conversation

@lehcar09
Copy link
Contributor

Initial commit with fixes from previous review

Initial changes made:
Add Jetpack Compose option in EntryChoiceActivity
Removed business logic from MainActivity
Added FirebaseAnalyticsViewModel for business logic
Added ComposeMainActivity for the Jetpack Compose UI

Changes made after review
-Rename recordShare API to recordShareEvent
-Pass screenClass parameter in recordScreenView

@lehcar09 lehcar09 requested a review from thatfiredev August 20, 2024 13:37
- Create local variable for showFavoriteDialog
- Pass shared preference in view mode factory
@thatfiredev
Copy link
Member

/gemini review

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a Jetpack Compose version of the analytics screen, refactoring the business logic into a FirebaseAnalyticsViewModel. The changes are well-structured, separating UI from logic effectively. I've found a few areas for improvement, mainly related to fixing a bug with duplicate analytics events, improving encapsulation in the ViewModel, and adhering to Kotlin and Compose best practices. I've also noted some hardcoded strings that should be moved to resources.

Comment on lines +100 to +114
DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_CREATE) {
recordImageView(analyticsViewModel, context)
} else if (event == Lifecycle.Event.ON_RESUME) {
recordScreenView(analyticsViewModel, context)
}
}

lifecycleOwner.lifecycle.addObserver(observer)

onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The analytics events recordImageView and recordScreenView are being triggered from both this DisposableEffect (on ON_CREATE and ON_RESUME) and the LaunchedEffect(pagerState) below. This will result in duplicate analytics events being sent on initial app launch. The LaunchedEffect already handles the initial state correctly. You should remove this DisposableEffect to avoid sending redundant events.

Comment on lines +21 to +27
val selectedImageIndex: MutableState<Int> = _selectedImageIndex

private val _userFavoriteFood = mutableStateOf<String?>(null)
val userFavoriteFood: MutableState<String?> = _userFavoriteFood

private val _showFavoriteFoodDialog = mutableStateOf(false)
val showFavoriteFoodDialog: MutableState<Boolean> = _showFavoriteFoodDialog

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The ViewModel is exposing MutableState properties publicly. This breaks encapsulation, as it allows external classes (like the UI) to modify the state directly. The common practice is to expose immutable State properties to the UI and keep the MutableState private to the ViewModel.

Suggested change
val selectedImageIndex: MutableState<Int> = _selectedImageIndex
private val _userFavoriteFood = mutableStateOf<String?>(null)
val userFavoriteFood: MutableState<String?> = _userFavoriteFood
private val _showFavoriteFoodDialog = mutableStateOf(false)
val showFavoriteFoodDialog: MutableState<Boolean> = _showFavoriteFoodDialog
val selectedImageIndex: androidx.compose.runtime.State<Int> = _selectedImageIndex
private val _userFavoriteFood = mutableStateOf<String?>(null)
val userFavoriteFood: androidx.compose.runtime.State<String?> = _userFavoriteFood
private val _showFavoriteFoodDialog = mutableStateOf(false)
val showFavoriteFoodDialog: androidx.compose.runtime.State<Boolean> = _showFavoriteFoodDialog

actions = {
IconButton(onClick = {
val imageTitle = getCurrentImageTitle(context, analyticsViewModel.selectedImageIndex.value)
val text = "I'd love you to hear about $imageTitle"

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The string for the share intent is hardcoded. For better maintainability and to support internationalization, you should move this string to a string resource file (strings.xml). For example, you could add <string name="share_message">I'd love you to hear about %1$s</string> to your strings.xml file and use stringResource(R.string.share_message, imageTitle) here.

Comment on lines +263 to +266
RadioButton(
selected = selectedItem == index,
onClick = { selectedItem = index }
)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The onClick handler is set on both the Row (via Modifier.clickable) and the RadioButton. This is redundant. To improve clarity and follow best practices, the onClick logic should be in one place. It's recommended to make the entire row clickable and disable the RadioButton's own click handler by passing null to its onClick parameter.

Suggested change
RadioButton(
selected = selectedItem == index,
onClick = { selectedItem = index }
)
RadioButton(
selected = selectedItem == index,
onClick = null
)

val selectedFood = choices[selectedItem]
analyticsViewModel.setUserFavoriteFood(selectedFood)
coroutineScope.launch {
snackbarHostState.showSnackbar("Favorite Food selected: $selectedFood")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The string for the snackbar message is hardcoded. For better maintainability and to support internationalization, you should move this string to a string resource file (strings.xml) and use context.getString() to retrieve it. You can get the context via LocalContext.current within the FavoriteFoodDialog composable.


import com.google.firebase.quickstart.analytics.R

class Constants {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For declaring singletons or collections of constants, it's more idiomatic in Kotlin to use an object declaration instead of a class with a companion object.

Suggested change
class Constants {
object Constants {

Comment on lines +29 to +36
init {
getUserFavoriteFood()
if(_userFavoriteFood.value == null) {
_showFavoriteFoodDialog.value = true
} else {
setUserFavoriteFood(_userFavoriteFood.value)
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In the init block, when a favorite food is already present in SharedPreferences, you're calling setUserFavoriteFood(). This function writes the same value back to SharedPreferences and also sets the user property. The write to SharedPreferences is redundant. The else block should only set the user property.

Suggested change
init {
getUserFavoriteFood()
if(_userFavoriteFood.value == null) {
_showFavoriteFoodDialog.value = true
} else {
setUserFavoriteFood(_userFavoriteFood.value)
}
}
init {
getUserFavoriteFood()
if(_userFavoriteFood.value == null) {
_showFavoriteFoodDialog.value = true
} else {
firebaseAnalytics.setUserProperty("favorite_food", _userFavoriteFood.value)
}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants