Conversation
...ics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ComposeMainActivity.kt
Outdated
Show resolved
Hide resolved
.../src/main/java/com/google/firebase/quickstart/analytics/kotlin/FirebaseAnalyticsViewModel.kt
Outdated
Show resolved
Hide resolved
.../src/main/java/com/google/firebase/quickstart/analytics/kotlin/FirebaseAnalyticsViewModel.kt
Outdated
Show resolved
Hide resolved
analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/data/Constants.kt
Outdated
Show resolved
Hide resolved
- Create local variable for showFavoriteDialog - Pass shared preference in view mode factory
|
/gemini review |
There was a problem hiding this comment.
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.
| 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) | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
| 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" |
There was a problem hiding this comment.
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.
| RadioButton( | ||
| selected = selectedItem == index, | ||
| onClick = { selectedItem = index } | ||
| ) |
There was a problem hiding this comment.
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.
| 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") |
There was a problem hiding this comment.
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 { |
| init { | ||
| getUserFavoriteFood() | ||
| if(_userFavoriteFood.value == null) { | ||
| _showFavoriteFoodDialog.value = true | ||
| } else { | ||
| setUserFavoriteFood(_userFavoriteFood.value) | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| 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) | |
| } | |
| } |
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