From 9ea872f403823cc26596e8d7f6382e13bb4e6873 Mon Sep 17 00:00:00 2001 From: lehcar09 Date: Tue, 20 Aug 2024 21:22:33 +0800 Subject: [PATCH 1/3] -Fix alignment --- .../analytics/EntryChoiceActivity.kt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/EntryChoiceActivity.kt b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/EntryChoiceActivity.kt index 512512bb37..785ebf134c 100644 --- a/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/EntryChoiceActivity.kt +++ b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/EntryChoiceActivity.kt @@ -8,18 +8,18 @@ class EntryChoiceActivity : BaseEntryChoiceActivity() { override fun getChoices(): List { return listOf( - Choice( - "Java", - "Run the Firebase Analytics quickstart written in Java.", - Intent( - this, - com.google.firebase.quickstart.analytics.java.MainActivity::class.java)), - Choice( - "Kotlin", - "Run the Firebase Analytics quickstart written in Kotlin.", - Intent( - this, - com.google.firebase.quickstart.analytics.kotlin.MainActivity::class.java)) + Choice( + "Java", + "Run the Firebase Analytics quickstart written in Java.", + Intent( + this, + com.google.firebase.quickstart.analytics.java.MainActivity::class.java)), + Choice( + "Kotlin", + "Run the Firebase Analytics quickstart written in Kotlin.", + Intent( + this, + com.google.firebase.quickstart.analytics.kotlin.MainActivity::class.java)) ) } } From 0d97aabc180844d357cc909daec0dbbb1c806a50 Mon Sep 17 00:00:00 2001 From: lehcar09 Date: Tue, 20 Aug 2024 21:33:26 +0800 Subject: [PATCH 2/3] Compose code for analytics --- analytics/app/build.gradle | 7 +- analytics/app/src/main/AndroidManifest.xml | 3 + .../analytics/EntryChoiceActivity.kt | 26 +- .../analytics/kotlin/ComposeMainActivity.kt | 321 ++++++++++++++++++ .../kotlin/FirebaseAnalyticsViewModel.kt | 83 +++++ .../analytics/kotlin/MainActivity.kt | 101 ++---- .../analytics/kotlin/data/Constants.kt | 16 + .../analytics/kotlin/{ => data}/ImageInfo.kt | 2 +- .../analytics/kotlin/ui/theme/Color.kt | 11 + .../analytics/kotlin/ui/theme/Shape.kt | 11 + .../analytics/kotlin/ui/theme/Theme.kt | 38 +++ .../analytics/kotlin/ui/theme/Type.kt | 18 + 12 files changed, 551 insertions(+), 86 deletions(-) create mode 100644 analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ComposeMainActivity.kt create mode 100644 analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/FirebaseAnalyticsViewModel.kt create mode 100644 analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/data/Constants.kt rename analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/{ => data}/ImageInfo.kt (68%) create mode 100644 analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ui/theme/Color.kt create mode 100644 analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ui/theme/Shape.kt create mode 100644 analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ui/theme/Theme.kt create mode 100644 analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ui/theme/Type.kt diff --git a/analytics/app/build.gradle b/analytics/app/build.gradle index 53a4704f25..ea9f964651 100644 --- a/analytics/app/build.gradle +++ b/analytics/app/build.gradle @@ -8,12 +8,12 @@ check.dependsOn 'assembleDebugAndroidTest' android { namespace 'com.google.firebase.quickstart.analytics' - compileSdk 33 + compileSdk 34 defaultConfig { applicationId "com.google.firebase.quickstart.analytics" minSdk 21 // minSdk would be 19 without compose - targetSdk 33 + targetSdk 34 versionCode 1 versionName "1.0" testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' @@ -77,6 +77,9 @@ dependencies { implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" implementation 'androidx.activity:activity-compose:1.5.1' + implementation 'androidx.compose.foundation:foundation-android:1.6.8' + implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation 'androidx.test:rules:1.4.0' androidTestImplementation 'androidx.test:runner:1.4.0' diff --git a/analytics/app/src/main/AndroidManifest.xml b/analytics/app/src/main/AndroidManifest.xml index ae27d65cf2..7c7971f4c8 100644 --- a/analytics/app/src/main/AndroidManifest.xml +++ b/analytics/app/src/main/AndroidManifest.xml @@ -13,6 +13,9 @@ + + { return listOf( Choice( - "Java", - "Run the Firebase Analytics quickstart written in Java.", - Intent( - this, - com.google.firebase.quickstart.analytics.java.MainActivity::class.java)), + "Java", + "Run the Firebase Analytics quickstart written in Java.", + Intent( + this, + com.google.firebase.quickstart.analytics.java.MainActivity::class.java)), Choice( - "Kotlin", - "Run the Firebase Analytics quickstart written in Kotlin.", - Intent( - this, - com.google.firebase.quickstart.analytics.kotlin.MainActivity::class.java)) + "Kotlin", + "Run the Firebase Analytics quickstart written in Kotlin.", + Intent( + this, + com.google.firebase.quickstart.analytics.kotlin.MainActivity::class.java)), + Choice( + "Compose", + "Run the Firebase Analytics quickstart written in Compose.", + Intent( + this, + com.google.firebase.quickstart.analytics.kotlin.ComposeMainActivity::class.java)) ) } } diff --git a/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ComposeMainActivity.kt b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ComposeMainActivity.kt new file mode 100644 index 0000000000..c483493f60 --- /dev/null +++ b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ComposeMainActivity.kt @@ -0,0 +1,321 @@ +package com.google.firebase.quickstart.analytics.kotlin + +import android.content.Context +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.AlertDialog +import androidx.compose.material.Button +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.material.MaterialTheme +import androidx.compose.material.RadioButton +import androidx.compose.material.Scaffold +import androidx.compose.material.SnackbarHost +import androidx.compose.material.SnackbarHostState +import androidx.compose.material.Surface +import androidx.compose.material.Tab +import androidx.compose.material.TabRow +import androidx.compose.material.TabRowDefaults +import androidx.compose.material.TabRowDefaults.tabIndicatorOffset +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Share +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringArrayResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.app.ShareCompat +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.viewmodel.compose.viewModel +import com.google.firebase.quickstart.analytics.R +import com.google.firebase.quickstart.analytics.kotlin.data.Constants +import com.google.firebase.quickstart.analytics.kotlin.data.ImageInfo +import com.google.firebase.quickstart.analytics.kotlin.ui.theme.FirebaseAnalyticsTheme +import kotlinx.coroutines.launch + +class ComposeMainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + FirebaseAnalyticsTheme { + // A surface container using the 'background' color from the theme + Surface( + modifier = Modifier.fillMaxSize(), + color = MaterialTheme.colors.background + ) { + MainAppView() + } + } + } + } +} + +@Composable +fun MainAppView( + lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, + analyticsViewModel: FirebaseAnalyticsViewModel = viewModel(factory = FirebaseAnalyticsViewModel.Factory) +) { + val context = LocalContext.current + val snackbarHostState = remember { SnackbarHostState() } + + 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) + } + } + + // Load favorite food on initial composition + LaunchedEffect(Unit) { + analyticsViewModel.getUserFavoriteFood(context) + if(analyticsViewModel.userFavoriteFood.value == null) + analyticsViewModel.showFavoriteFoodDialog.value = true + else + analyticsViewModel.setUserFavoriteFood(context, analyticsViewModel.userFavoriteFood.value) + } + + Scaffold( + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + }, + modifier = Modifier.fillMaxSize(), + topBar = { + MainAppBar(analyticsViewModel) + }, + content = { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues), + ) { + MainContent(analyticsViewModel) + FavoriteFoodDialog(analyticsViewModel, snackbarHostState) + } + } + ) +} + +@Composable +fun MainAppBar(analyticsViewModel: FirebaseAnalyticsViewModel) { + val context = LocalContext.current + TopAppBar( + title = { + Text( + text = stringResource(R.string.app_name), + style = MaterialTheme.typography.h6, + textAlign = TextAlign.Center, + modifier = Modifier.padding(8.dp), + color = Color.White + ) + }, + backgroundColor = colorResource(R.color.colorPrimary), + actions = { + IconButton(onClick = { + val imageTitle = getCurrentImageTitle(context, analyticsViewModel.selectedImageIndex.value) + val text = "I'd love you to hear about $imageTitle" + + ShareCompat.IntentBuilder(context).setType("text/plain") + .setText(text) + .startChooser() + + analyticsViewModel.recordShareEvent(imageTitle, text) + }) { + Icon(Icons.Filled.Share, contentDescription = "Share") + } + } + ) +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun MainContent(analyticsViewModel: FirebaseAnalyticsViewModel) { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + val pagerState = rememberPagerState(initialPage = 0, pageCount = { Constants.IMAGE_INFOS.size }) + + LaunchedEffect(pagerState) { + snapshotFlow { pagerState.targetPage }.collect { page -> + analyticsViewModel.setSelectedImageIndex(page) + recordImageView(analyticsViewModel, context) + recordScreenView(analyticsViewModel, context) + } + } + + TabRow( + backgroundColor = Color.White, + selectedTabIndex = pagerState.currentPage, + indicator = { tabPositions -> + TabRowDefaults.Indicator( + modifier = Modifier.tabIndicatorOffset(tabPositions[pagerState.currentPage]), + color = colorResource(id = R.color.colorPrimary) + ) + }, + ) { + Constants.IMAGE_INFOS.forEachIndexed { index, info -> + val isSelected = pagerState.currentPage == index + Tab( + text = { + Text(text = stringResource(id = info.title), + color = if(isSelected) colorResource(id = R.color.colorPrimary) else Color.Black) + }, + selected = isSelected, + onClick = { + coroutineScope.launch { + pagerState.animateScrollToPage(index) + } + }, + ) + } + } + //Image Pager + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxSize() + ) { page -> + ImageCard(imageInfo = Constants.IMAGE_INFOS[page]) + } +} + +@Composable +fun ImageCard(imageInfo: ImageInfo) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(id = imageInfo.image), + contentDescription = stringResource(id = imageInfo.title), + modifier = Modifier + .shadow(elevation = 2.dp, shape = CircleShape) + .background(Color.White, shape = CircleShape) + .padding(16.dp) + ) + } +} + +@Composable +fun FavoriteFoodDialog( + analyticsViewModel: FirebaseAnalyticsViewModel, + snackbarHostState: SnackbarHostState +) { + if (analyticsViewModel.showFavoriteFoodDialog.value) { + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + val choices = stringArrayResource(id = R.array.food_items) + var selectedItem by remember { mutableIntStateOf(0) } + + AlertDialog( + onDismissRequest = { + analyticsViewModel.showFavoriteFoodDialog.value = false + }, + title = { Text(stringResource(id = R.string.food_dialog_title)) }, + text = { + Column { + choices.forEachIndexed { index, item -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.clickable { selectedItem = index } + ) { + RadioButton( + selected = selectedItem == index, + onClick = { selectedItem = index } + ) + Text(item) + } + } + } + }, + confirmButton = { + Button(onClick = { + analyticsViewModel.showFavoriteFoodDialog.value = false + val selectedFood = choices[selectedItem] + analyticsViewModel.setUserFavoriteFood(context, selectedFood) + coroutineScope.launch { + snackbarHostState.showSnackbar("Favorite Food selected: $selectedFood") + } + }) { + Text("OK") + } + } + ) + } +} + +private fun getCurrentImageTitle(context: Context, position: Int): String { + val imageDetails = Constants.IMAGE_INFOS[position] + return context.getString(imageDetails.title) +} + +private fun getCurrentImageId(context: Context, position: Int): String { + val imageDetails = Constants.IMAGE_INFOS[position] + return context.getString(imageDetails.id) +} + +private fun recordScreenView(analyticsViewModel: FirebaseAnalyticsViewModel, context: Context) { + val position = analyticsViewModel.selectedImageIndex.value + val imageId = getCurrentImageId(context, position) + val imageTitle = getCurrentImageTitle(context, position) + + analyticsViewModel.recordScreenView("${imageId}-${imageTitle}", "ComposeMainActivity") +} + +private fun recordImageView(analyticsViewModel: FirebaseAnalyticsViewModel, context: Context) { + val position = analyticsViewModel.selectedImageIndex.value + val imageId = getCurrentImageId(context, position) + val imageTitle = getCurrentImageTitle(context, position) + + analyticsViewModel.recordImageView(imageId, imageTitle) +} + +@Preview(showBackground = true) +@Composable +fun MainAppPreview() { + FirebaseAnalyticsTheme { + MainAppView(viewModel(factory = FirebaseAnalyticsViewModel.Factory)) + } +} \ No newline at end of file diff --git a/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/FirebaseAnalyticsViewModel.kt b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/FirebaseAnalyticsViewModel.kt new file mode 100644 index 0000000000..7065b2ad7d --- /dev/null +++ b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/FirebaseAnalyticsViewModel.kt @@ -0,0 +1,83 @@ +package com.google.firebase.quickstart.analytics.kotlin + +import android.content.Context +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.CreationExtras +import androidx.preference.PreferenceManager +import com.google.firebase.analytics.FirebaseAnalytics +import com.google.firebase.analytics.ktx.analytics +import com.google.firebase.analytics.ktx.logEvent +import com.google.firebase.ktx.Firebase +import com.google.firebase.quickstart.analytics.kotlin.data.Constants + +class FirebaseAnalyticsViewModel ( + private val firebaseAnalytics: FirebaseAnalytics +): ViewModel() { + + val showFavoriteFoodDialog = mutableStateOf(false) + + private val _selectedImageIndex = mutableIntStateOf(0) + val selectedImageIndex: MutableState = _selectedImageIndex + + private val _userFavoriteFood = mutableStateOf(null) + val userFavoriteFood: MutableState = _userFavoriteFood + + fun setSelectedImageIndex(index: Int) { + _selectedImageIndex.intValue = index + } + + fun setUserFavoriteFood(context: Context, food: String?) { + _userFavoriteFood.value = food + PreferenceManager.getDefaultSharedPreferences(context).edit() + .putString(Constants.KEY_FAVORITE_FOOD, food) + .apply() + + firebaseAnalytics.setUserProperty("favorite_food", food) + } + + fun getUserFavoriteFood(context: Context) { + _userFavoriteFood.value = PreferenceManager.getDefaultSharedPreferences(context) + .getString(Constants.KEY_FAVORITE_FOOD, null) + } + + fun recordShareEvent(imageTitle: String, text: String) { + firebaseAnalytics.logEvent("share_image") { + param("image_name", imageTitle) + param("full_text", text) + } + } + + fun recordScreenView(screenName: String, screenClass: String) { + firebaseAnalytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW) { + param(FirebaseAnalytics.Param.SCREEN_NAME, screenName) + param(FirebaseAnalytics.Param.SCREEN_CLASS, screenClass) + } + } + + fun recordImageView(id: String, name: String) { + firebaseAnalytics.logEvent(FirebaseAnalytics.Event.SELECT_ITEM) { + param(FirebaseAnalytics.Param.ITEM_ID, id) + param(FirebaseAnalytics.Param.ITEM_NAME, name) + param(FirebaseAnalytics.Param.CONTENT_TYPE, "image") + } + } + + companion object { + // Used to inject this ViewModel's dependencies + // See also: https://developer.android.com/topic/libraries/architecture/viewmodel/viewmodel-factories + val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { + @Suppress("UNCHECKED_CAST") + override fun create( + modelClass: Class, + extras: CreationExtras + ): T { + val firebaseAnalytics = Firebase.analytics + return FirebaseAnalyticsViewModel(firebaseAnalytics) as T + } + } + } +} \ No newline at end of file diff --git a/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/MainActivity.kt b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/MainActivity.kt index 003fd62fc5..07c6f42b6a 100644 --- a/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/MainActivity.kt +++ b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/MainActivity.kt @@ -1,47 +1,34 @@ package com.google.firebase.quickstart.analytics.kotlin -import android.annotation.SuppressLint +import android.content.Context import android.content.Intent import android.os.Bundle -import android.util.Log import android.view.Menu import android.view.MenuItem +import androidx.activity.viewModels import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.lifecycle.Lifecycle -import androidx.preference.PreferenceManager import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator -import com.google.firebase.analytics.FirebaseAnalytics -import com.google.firebase.analytics.ktx.analytics -import com.google.firebase.analytics.ktx.logEvent -import com.google.firebase.ktx.Firebase import com.google.firebase.quickstart.analytics.R import com.google.firebase.quickstart.analytics.databinding.ActivityMainBinding -import com.google.firebase.quickstart.analytics.java.MainActivity -import com.google.firebase.quickstart.analytics.kotlin.MainActivity.Companion.IMAGE_INFOS +import com.google.firebase.quickstart.analytics.kotlin.data.Constants +import com.google.firebase.quickstart.analytics.kotlin.data.ImageInfo import java.util.Locale - /** * Activity which displays numerous background images that may be viewed. These background images * are shown via {@link ImageFragment}. */ class MainActivity : AppCompatActivity() { + companion object { private const val TAG = "MainActivity" - private const val KEY_FAVORITE_FOOD = "favorite_food" - - private val IMAGE_INFOS = arrayOf( - ImageInfo(R.drawable.favorite, R.string.pattern1_title, R.string.pattern1_id), - ImageInfo(R.drawable.flash, R.string.pattern2_title, R.string.pattern2_id), - ImageInfo(R.drawable.face, R.string.pattern3_title, R.string.pattern3_id), - ImageInfo(R.drawable.whitebalance, R.string.pattern4_title, R.string.pattern4_id) - ) } private lateinit var binding: ActivityMainBinding @@ -52,11 +39,13 @@ class MainActivity : AppCompatActivity() { */ private lateinit var imagePagerAdapter: ImagePagerAdapter - /** - * The `FirebaseAnalytics` used to record screen views. - */ + private lateinit var context: Context + + // Injects FirebaseAnalytics and app measurement configuration from the factory for centralized management. // [START declare_analytics] - private lateinit var firebaseAnalytics: FirebaseAnalytics + // [START shared_app_measurement] + private val viewModel: FirebaseAnalyticsViewModel by viewModels { FirebaseAnalyticsViewModel.Factory } + // [END shared_app_measurement] // [END declare_analytics] override fun onCreate(savedInstanceState: Bundle?) { @@ -64,22 +53,21 @@ class MainActivity : AppCompatActivity() { binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) - // [START shared_app_measurement] - // Obtain the FirebaseAnalytics instance. - firebaseAnalytics = Firebase.analytics - // [END shared_app_measurement] + context = applicationContext // On first app open, ask the user his/her favorite food. Then set this as a user property // on all subsequent opens. - val userFavoriteFood = getUserFavoriteFood() - if (userFavoriteFood == null) { + viewModel.getUserFavoriteFood(context) + if (viewModel.userFavoriteFood.value == null) { askFavoriteFood() } else { - setUserFavoriteFood(userFavoriteFood) + // [START user_property] + viewModel.setUserFavoriteFood(context, viewModel.userFavoriteFood.value) + // [END user_property] } // Create the adapter that will return a fragment for each image. - imagePagerAdapter = ImagePagerAdapter(supportFragmentManager, IMAGE_INFOS, lifecycle) + imagePagerAdapter = ImagePagerAdapter(supportFragmentManager, Constants.IMAGE_INFOS, lifecycle) // Set up the ViewPager with the pattern adapter. binding.viewPager.adapter = imagePagerAdapter @@ -95,7 +83,7 @@ class MainActivity : AppCompatActivity() { val tabLayout: TabLayout = binding.tabLayout TabLayoutMediator(tabLayout, binding.viewPager) { tab, position -> - tab.setText(IMAGE_INFOS[position].title) + tab.setText(Constants.IMAGE_INFOS[position].title) }.attach() // Send initial screen screen view hit. @@ -118,37 +106,14 @@ class MainActivity : AppCompatActivity() { .setTitle(R.string.food_dialog_title) .setItems(choices) { _, which -> val food = choices[which] - setUserFavoriteFood(food) - }.create() + // [START user_property] + viewModel.setUserFavoriteFood(context, food) + // [END user_property] + }.create() ad.show() } - /** - * Get the user's favorite food from shared preferences. - * @return favorite food, as a string. - */ - private fun getUserFavoriteFood(): String? { - return PreferenceManager.getDefaultSharedPreferences(this) - .getString(KEY_FAVORITE_FOOD, null) - } - - /** - * Set the user's favorite food as an app measurement user property and in shared preferences. - * @param food the user's favorite food. - */ - private fun setUserFavoriteFood(food: String) { - Log.d(TAG, "setFavoriteFood: $food") - - PreferenceManager.getDefaultSharedPreferences(this).edit() - .putString(KEY_FAVORITE_FOOD, food) - .apply() - - // [START user_property] - firebaseAnalytics.setUserProperty("favorite_food", food) - // [END user_property] - } - override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.main, menu) return true @@ -167,10 +132,7 @@ class MainActivity : AppCompatActivity() { startActivity(sendIntent) // [START custom_event] - firebaseAnalytics.logEvent("share_image") { - param("image_name", name) - param("full_text", text) - } + viewModel.recordShareEvent(name, text) // [END custom_event] } return false @@ -183,7 +145,7 @@ class MainActivity : AppCompatActivity() { */ private fun getCurrentImageTitle(): String { val position = binding.viewPager.currentItem - val info = IMAGE_INFOS[position] + val info = Constants.IMAGE_INFOS[position] return getString(info.title) } @@ -194,7 +156,7 @@ class MainActivity : AppCompatActivity() { */ private fun getCurrentImageId(): String { val position = binding.viewPager.currentItem - val info = IMAGE_INFOS[position] + val info = Constants.IMAGE_INFOS[position] return getString(info.id) } @@ -207,11 +169,7 @@ class MainActivity : AppCompatActivity() { val name = getCurrentImageTitle() // [START image_view_event] - firebaseAnalytics.logEvent(FirebaseAnalytics.Event.SELECT_ITEM) { - param(FirebaseAnalytics.Param.ITEM_ID, id) - param(FirebaseAnalytics.Param.ITEM_NAME, name) - param(FirebaseAnalytics.Param.CONTENT_TYPE, "image") - } + viewModel.recordImageView(id, name) // [END image_view_event] } @@ -224,10 +182,7 @@ class MainActivity : AppCompatActivity() { val screenName = "${getCurrentImageId()}-${getCurrentImageTitle()}" // [START set_current_screen] - firebaseAnalytics.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW) { - param(FirebaseAnalytics.Param.SCREEN_NAME, screenName) - param(FirebaseAnalytics.Param.SCREEN_CLASS, "MainActivity") - } + viewModel.recordScreenView(screenName, "MainActivity") // [END set_current_screen] } diff --git a/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/data/Constants.kt b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/data/Constants.kt new file mode 100644 index 0000000000..02bc81e6ff --- /dev/null +++ b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/data/Constants.kt @@ -0,0 +1,16 @@ +package com.google.firebase.quickstart.analytics.kotlin.data + +import com.google.firebase.quickstart.analytics.R + +class Constants { + companion object { + const val KEY_FAVORITE_FOOD = "favorite_food" + + val IMAGE_INFOS = arrayOf( + ImageInfo(R.drawable.favorite, R.string.pattern1_title, R.string.pattern1_id), + ImageInfo(R.drawable.flash, R.string.pattern2_title, R.string.pattern2_id), + ImageInfo(R.drawable.face, R.string.pattern3_title, R.string.pattern3_id), + ImageInfo(R.drawable.whitebalance, R.string.pattern4_title, R.string.pattern4_id) + ) + } +} \ No newline at end of file diff --git a/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ImageInfo.kt b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/data/ImageInfo.kt similarity index 68% rename from analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ImageInfo.kt rename to analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/data/ImageInfo.kt index c06f152a04..1f95036167 100644 --- a/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ImageInfo.kt +++ b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/data/ImageInfo.kt @@ -1,4 +1,4 @@ -package com.google.firebase.quickstart.analytics.kotlin +package com.google.firebase.quickstart.analytics.kotlin.data /** * Pair of resource IDs representing an image and its title. diff --git a/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ui/theme/Color.kt b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ui/theme/Color.kt new file mode 100644 index 0000000000..602e815126 --- /dev/null +++ b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.google.firebase.quickstart.analytics.kotlin.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val FirebaseBlue = Color(0xFF0288D1) // copied from colors.xml +val FirebaseBannerBlue = Color(0xFF039BE5) // copied from colors.xml +val FirebaseOrange = Color(0xFFFFA000) // copied from colors.xml \ No newline at end of file diff --git a/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ui/theme/Shape.kt b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ui/theme/Shape.kt new file mode 100644 index 0000000000..93070eb611 --- /dev/null +++ b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ui/theme/Shape.kt @@ -0,0 +1,11 @@ +package com.google.firebase.quickstart.analytics.kotlin.ui.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Shapes +import androidx.compose.ui.unit.dp + +val Shapes = Shapes( + small = RoundedCornerShape(16.dp), + medium = RoundedCornerShape(2.dp), + large = RoundedCornerShape(0.dp) +) \ No newline at end of file diff --git a/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ui/theme/Theme.kt b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ui/theme/Theme.kt new file mode 100644 index 0000000000..d426d69ed1 --- /dev/null +++ b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ui/theme/Theme.kt @@ -0,0 +1,38 @@ +package com.google.firebase.quickstart.analytics.kotlin.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material.MaterialTheme +import androidx.compose.material.darkColors +import androidx.compose.material.lightColors +import androidx.compose.runtime.Composable + +private val DarkColorPalette = darkColors( + primary = Purple80, + primaryVariant = PurpleGrey80, + secondary = Pink80 +) + +private val LightColorPalette = lightColors( + primary = FirebaseBlue, + primaryVariant = FirebaseBannerBlue, + secondary = FirebaseOrange +) + +@Composable +fun FirebaseAnalyticsTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit +) { + val colors = if (darkTheme) { + DarkColorPalette + } else { + LightColorPalette + } + + MaterialTheme( + colors = colors, + typography = Typography, + shapes = Shapes, + content = content + ) +} \ No newline at end of file diff --git a/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ui/theme/Type.kt b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ui/theme/Type.kt new file mode 100644 index 0000000000..a0184e8011 --- /dev/null +++ b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ui/theme/Type.kt @@ -0,0 +1,18 @@ +package com.google.firebase.quickstart.analytics.kotlin.ui.theme + +import androidx.compose.material.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + body1 = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) +) \ No newline at end of file From 1182dba71a4ee04c832186c5ab7eefcd4e534ac7 Mon Sep 17 00:00:00 2001 From: lehcar09 Date: Wed, 4 Sep 2024 00:21:48 +0800 Subject: [PATCH 3/3] - Move ImageInfo and Constants file out of data folder - Create local variable for showFavoriteDialog - Pass shared preference in view mode factory --- .../analytics/kotlin/ComposeMainActivity.kt | 22 +++++------ .../analytics/kotlin/{data => }/Constants.kt | 2 +- .../kotlin/FirebaseAnalyticsViewModel.kt | 39 ++++++++++++------- .../analytics/kotlin/{data => }/ImageInfo.kt | 2 +- .../analytics/kotlin/MainActivity.kt | 30 +++++++------- 5 files changed, 53 insertions(+), 42 deletions(-) rename analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/{data => }/Constants.kt (90%) rename analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/{data => }/ImageInfo.kt (68%) diff --git a/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ComposeMainActivity.kt b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ComposeMainActivity.kt index c483493f60..fbf1d7eed1 100644 --- a/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ComposeMainActivity.kt +++ b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ComposeMainActivity.kt @@ -61,9 +61,10 @@ import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.preference.PreferenceManager +import com.google.firebase.analytics.ktx.analytics +import com.google.firebase.ktx.Firebase import com.google.firebase.quickstart.analytics.R -import com.google.firebase.quickstart.analytics.kotlin.data.Constants -import com.google.firebase.quickstart.analytics.kotlin.data.ImageInfo import com.google.firebase.quickstart.analytics.kotlin.ui.theme.FirebaseAnalyticsTheme import kotlinx.coroutines.launch @@ -87,17 +88,19 @@ class ComposeMainActivity : ComponentActivity() { @Composable fun MainAppView( - lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current, - analyticsViewModel: FirebaseAnalyticsViewModel = viewModel(factory = FirebaseAnalyticsViewModel.Factory) + lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current ) { val context = LocalContext.current + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + val analyticsViewModel: FirebaseAnalyticsViewModel = viewModel( + factory = FirebaseAnalyticsViewModel.Factory(Firebase.analytics, sharedPreferences) + ) val snackbarHostState = remember { SnackbarHostState() } 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) } @@ -112,11 +115,8 @@ fun MainAppView( // Load favorite food on initial composition LaunchedEffect(Unit) { - analyticsViewModel.getUserFavoriteFood(context) if(analyticsViewModel.userFavoriteFood.value == null) analyticsViewModel.showFavoriteFoodDialog.value = true - else - analyticsViewModel.setUserFavoriteFood(context, analyticsViewModel.userFavoriteFood.value) } Scaffold( @@ -244,7 +244,6 @@ fun FavoriteFoodDialog( snackbarHostState: SnackbarHostState ) { if (analyticsViewModel.showFavoriteFoodDialog.value) { - val context = LocalContext.current val coroutineScope = rememberCoroutineScope() val choices = stringArrayResource(id = R.array.food_items) var selectedItem by remember { mutableIntStateOf(0) } @@ -272,9 +271,8 @@ fun FavoriteFoodDialog( }, confirmButton = { Button(onClick = { - analyticsViewModel.showFavoriteFoodDialog.value = false val selectedFood = choices[selectedItem] - analyticsViewModel.setUserFavoriteFood(context, selectedFood) + analyticsViewModel.setUserFavoriteFood(selectedFood) coroutineScope.launch { snackbarHostState.showSnackbar("Favorite Food selected: $selectedFood") } @@ -316,6 +314,6 @@ private fun recordImageView(analyticsViewModel: FirebaseAnalyticsViewModel, cont @Composable fun MainAppPreview() { FirebaseAnalyticsTheme { - MainAppView(viewModel(factory = FirebaseAnalyticsViewModel.Factory)) + MainAppView() } } \ No newline at end of file diff --git a/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/data/Constants.kt b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/Constants.kt similarity index 90% rename from analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/data/Constants.kt rename to analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/Constants.kt index 02bc81e6ff..773d2b81d2 100644 --- a/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/data/Constants.kt +++ b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/Constants.kt @@ -1,4 +1,4 @@ -package com.google.firebase.quickstart.analytics.kotlin.data +package com.google.firebase.quickstart.analytics.kotlin import com.google.firebase.quickstart.analytics.R diff --git a/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/FirebaseAnalyticsViewModel.kt b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/FirebaseAnalyticsViewModel.kt index 7065b2ad7d..3f1e48b94e 100644 --- a/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/FirebaseAnalyticsViewModel.kt +++ b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/FirebaseAnalyticsViewModel.kt @@ -1,47 +1,56 @@ package com.google.firebase.quickstart.analytics.kotlin -import android.content.Context +import android.content.SharedPreferences import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.CreationExtras -import androidx.preference.PreferenceManager import com.google.firebase.analytics.FirebaseAnalytics import com.google.firebase.analytics.ktx.analytics import com.google.firebase.analytics.ktx.logEvent import com.google.firebase.ktx.Firebase -import com.google.firebase.quickstart.analytics.kotlin.data.Constants class FirebaseAnalyticsViewModel ( - private val firebaseAnalytics: FirebaseAnalytics + private val firebaseAnalytics: FirebaseAnalytics, + private val sharedPreferences: SharedPreferences ): ViewModel() { - val showFavoriteFoodDialog = mutableStateOf(false) - private val _selectedImageIndex = mutableIntStateOf(0) val selectedImageIndex: MutableState = _selectedImageIndex private val _userFavoriteFood = mutableStateOf(null) val userFavoriteFood: MutableState = _userFavoriteFood + private val _showFavoriteFoodDialog = mutableStateOf(false) + val showFavoriteFoodDialog: MutableState = _showFavoriteFoodDialog + + init { + getUserFavoriteFood() + if(_userFavoriteFood.value == null) { + _showFavoriteFoodDialog.value = true + } else { + setUserFavoriteFood(_userFavoriteFood.value) + } + } + fun setSelectedImageIndex(index: Int) { _selectedImageIndex.intValue = index } - fun setUserFavoriteFood(context: Context, food: String?) { + fun setUserFavoriteFood(food: String?) { + _showFavoriteFoodDialog.value = false _userFavoriteFood.value = food - PreferenceManager.getDefaultSharedPreferences(context).edit() + sharedPreferences.edit() .putString(Constants.KEY_FAVORITE_FOOD, food) .apply() firebaseAnalytics.setUserProperty("favorite_food", food) } - fun getUserFavoriteFood(context: Context) { - _userFavoriteFood.value = PreferenceManager.getDefaultSharedPreferences(context) - .getString(Constants.KEY_FAVORITE_FOOD, null) + private fun getUserFavoriteFood() { + _userFavoriteFood.value = sharedPreferences.getString(Constants.KEY_FAVORITE_FOOD, null) } fun recordShareEvent(imageTitle: String, text: String) { @@ -69,14 +78,16 @@ class FirebaseAnalyticsViewModel ( companion object { // Used to inject this ViewModel's dependencies // See also: https://developer.android.com/topic/libraries/architecture/viewmodel/viewmodel-factories - val Factory: ViewModelProvider.Factory = object : ViewModelProvider.Factory { + fun Factory( + firebaseAnalytics: FirebaseAnalytics = Firebase.analytics, + sharedPreferences: SharedPreferences + ): ViewModelProvider.Factory = object : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create( modelClass: Class, extras: CreationExtras ): T { - val firebaseAnalytics = Firebase.analytics - return FirebaseAnalyticsViewModel(firebaseAnalytics) as T + return FirebaseAnalyticsViewModel(firebaseAnalytics, sharedPreferences) as T } } } diff --git a/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/data/ImageInfo.kt b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ImageInfo.kt similarity index 68% rename from analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/data/ImageInfo.kt rename to analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ImageInfo.kt index 1f95036167..c06f152a04 100644 --- a/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/data/ImageInfo.kt +++ b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/ImageInfo.kt @@ -1,4 +1,4 @@ -package com.google.firebase.quickstart.analytics.kotlin.data +package com.google.firebase.quickstart.analytics.kotlin /** * Pair of resource IDs representing an image and its title. diff --git a/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/MainActivity.kt b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/MainActivity.kt index 07c6f42b6a..acb7c35a3a 100644 --- a/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/MainActivity.kt +++ b/analytics/app/src/main/java/com/google/firebase/quickstart/analytics/kotlin/MainActivity.kt @@ -2,6 +2,7 @@ package com.google.firebase.quickstart.analytics.kotlin import android.content.Context import android.content.Intent +import android.content.SharedPreferences import android.os.Bundle import android.view.Menu import android.view.MenuItem @@ -11,14 +12,15 @@ import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.lifecycle.Lifecycle +import androidx.preference.PreferenceManager import androidx.viewpager2.adapter.FragmentStateAdapter import androidx.viewpager2.widget.ViewPager2 import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayoutMediator +import com.google.firebase.analytics.ktx.analytics +import com.google.firebase.ktx.Firebase import com.google.firebase.quickstart.analytics.R import com.google.firebase.quickstart.analytics.databinding.ActivityMainBinding -import com.google.firebase.quickstart.analytics.kotlin.data.Constants -import com.google.firebase.quickstart.analytics.kotlin.data.ImageInfo import java.util.Locale /** @@ -40,13 +42,10 @@ class MainActivity : AppCompatActivity() { private lateinit var imagePagerAdapter: ImagePagerAdapter private lateinit var context: Context + private lateinit var sharedPrefs: SharedPreferences // Injects FirebaseAnalytics and app measurement configuration from the factory for centralized management. - // [START declare_analytics] - // [START shared_app_measurement] - private val viewModel: FirebaseAnalyticsViewModel by viewModels { FirebaseAnalyticsViewModel.Factory } - // [END shared_app_measurement] - // [END declare_analytics] + private lateinit var viewModel: FirebaseAnalyticsViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -54,16 +53,19 @@ class MainActivity : AppCompatActivity() { setContentView(binding.root) context = applicationContext + sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context) + // [START declare_analytics] + // [START shared_app_measurement] + viewModel = viewModels { + FirebaseAnalyticsViewModel.Factory(Firebase.analytics, sharedPrefs) + }.value + // [END shared_app_measurement] + // [END declare_analytics] // On first app open, ask the user his/her favorite food. Then set this as a user property // on all subsequent opens. - viewModel.getUserFavoriteFood(context) - if (viewModel.userFavoriteFood.value == null) { + if (viewModel.showFavoriteFoodDialog.value) { askFavoriteFood() - } else { - // [START user_property] - viewModel.setUserFavoriteFood(context, viewModel.userFavoriteFood.value) - // [END user_property] } // Create the adapter that will return a fragment for each image. @@ -108,7 +110,7 @@ class MainActivity : AppCompatActivity() { val food = choices[which] // [START user_property] - viewModel.setUserFavoriteFood(context, food) + viewModel.setUserFavoriteFood(food) // [END user_property] }.create() ad.show()