Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions messaging/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ check.dependsOn 'assembleDebugAndroidTest'

android {
namespace 'com.google.firebase.quickstart.fcm'
compileSdk 33
compileSdk 34

defaultConfig {
applicationId "com.google.firebase.quickstart.fcm"
minSdk 21 // minSdk would be 19 without compose
targetSdk 33
targetSdk 34
versionCode 1
versionName "1.0"
multiDexEnabled true
Expand Down Expand Up @@ -97,6 +97,8 @@ dependencies {
implementation "androidx.compose.ui:ui-tooling-preview:$compose_version"
implementation 'androidx.activity:activity-compose:1.5.1'

implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0'

// Testing dependencies
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0'
androidTestImplementation 'androidx.test:runner:1.5.1'
Expand Down
1 change: 1 addition & 0 deletions messaging/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
</activity>

<activity android:name=".kotlin.MainActivity" />
<activity android:name=".kotlin.ComposeMainActivity"/>
<activity android:name=".java.MainActivity" />

<service
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,18 @@ class EntryChoiceActivity : BaseEntryChoiceActivity() {

override fun getChoices(): List<Choice> {
return kotlin.collections.listOf(
Choice(
"Java",
"Run the Firebase Cloud Messaging quickstart written in Java.",
Intent(this, MainActivity::class.java)),
Choice(
"Kotlin",
"Run the Firebase Cloud Messaging written in Kotlin.",
Intent(this, com.google.firebase.quickstart.fcm.kotlin.MainActivity::class.java))
Choice(
"Java",
"Run the Firebase Cloud Messaging quickstart written in Java.",
Intent(this, MainActivity::class.java)),
Choice(
"Kotlin",
"Run the Firebase Cloud Messaging written in Kotlin.",
Intent(this, com.google.firebase.quickstart.fcm.kotlin.MainActivity::class.java)),
Choice(
"Compose",
"Run the Firebase Cloud Messaging written in Compose.",
Intent(this, com.google.firebase.quickstart.fcm.kotlin.ComposeMainActivity::class.java))
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
package com.google.firebase.quickstart.fcm.kotlin

import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import android.os.Build
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.SnackbarHost
import androidx.compose.material.SnackbarHostState
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.firebase.quickstart.fcm.R
import com.google.firebase.quickstart.fcm.kotlin.data.SubscriptionState
import com.google.firebase.quickstart.fcm.kotlin.ui.theme.FirebaseMessagingTheme
import kotlinx.coroutines.launch

class ComposeMainActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
FirebaseMessagingTheme {
// A surface container using the 'background' color from the theme
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
MainScreen()
}
}
}
}
}

@Composable
fun MainScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
fcmViewModel: FirebaseMessagingViewModel = viewModel(factory = FirebaseMessagingViewModel.Factory)
) {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }

val activity = context.findActivity()
val intent = activity?.intent

val requestPermissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
var message = context.getString(R.string.msg_permission_granted)
if (!isGranted) {
message = context.getString(R.string.msg_permission_failed)
}
Comment on lines +87 to +90

Choose a reason for hiding this comment

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

medium

The message variable is assigned once and never changed within its scope. It can be declared as a val using an if-else expression, which is better for readability and immutability.

Suggested change
var message = context.getString(R.string.msg_permission_granted)
if (!isGranted) {
message = context.getString(R.string.msg_permission_failed)
}
val message = if (isGranted) {
context.getString(R.string.msg_permission_granted)
} else {
context.getString(R.string.msg_permission_failed)
}


scope.launch {
snackbarHostState.showSnackbar(message)
}
}

DisposableEffect(lifecycleOwner) {
val observer = LifecycleEventObserver { _, event ->
if (event == Lifecycle.Event.ON_CREATE) {
//Create Notification Channel
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
fcmViewModel.setNotificationChannel(
context,
context.getString(R.string.default_notification_channel_id),
context.getString(R.string.default_notification_channel_name)
)
}

intent?.let { intentData ->
fcmViewModel.logNotificationData(intentData)
}

scope.launch {
snackbarHostState.showSnackbar(context.getString(R.string.msg_setup_readme_instructions))
}

fcmViewModel.askNotificationPermission(context, requestPermissionLauncher)
}
}

lifecycleOwner.lifecycle.addObserver(observer)

onDispose {
lifecycleOwner.lifecycle.removeObserver(observer)
}
}
Comment on lines +97 to +126

Choose a reason for hiding this comment

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

medium

Using DisposableEffect with Lifecycle.Event.ON_CREATE for one-time setup can lead to the code running multiple times, for example on configuration changes. A more idiomatic and safer way to perform one-time setup in Compose is to use LaunchedEffect(Unit). This ensures the code runs only once when the composable enters the composition.

    LaunchedEffect(Unit) {
        //Create Notification Channel
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            fcmViewModel.setNotificationChannel(
                context,
                context.getString(R.string.default_notification_channel_id),
                context.getString(R.string.default_notification_channel_name)
            )
        }

        intent?.let { intentData ->
            fcmViewModel.logNotificationData(intentData)
        }

        snackbarHostState.showSnackbar(context.getString(R.string.msg_setup_readme_instructions))

        fcmViewModel.askNotificationPermission(context, requestPermissionLauncher)
    }


LaunchedEffect(fcmViewModel.token) {
fcmViewModel.token.collect {
if(it.isNotEmpty()) {
snackbarHostState.showSnackbar(context.getString(R.string.msg_token_fmt, it))
}
}
}

LaunchedEffect(fcmViewModel.subscriptionState) {
fcmViewModel.subscriptionState.collect { state ->
when (state) {
SubscriptionState.Success -> { snackbarHostState.showSnackbar(context.getString(R.string.msg_subscribed)) }
SubscriptionState.Failed -> { snackbarHostState.showSnackbar(context.getString(R.string.msg_subscribe_failed)) }
SubscriptionState.Loading -> { }
}
}
}

Scaffold(
snackbarHost = {
SnackbarHost(hostState = snackbarHostState)
},
modifier = Modifier
.fillMaxSize(),
topBar = {
TopAppBar(
backgroundColor = colorResource(R.color.colorPrimary)

Choose a reason for hiding this comment

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

medium

The TopAppBar's background color is hardcoded using colorResource. To better support theming (including dark theme), you should use colors from MaterialTheme.colors. The Theme.kt file already defines FirebaseBlue as the primary color for the light theme.

                backgroundColor = MaterialTheme.colors.primary

) {
Text(
text = stringResource(R.string.app_name),
style = MaterialTheme.typography.h6,
textAlign = TextAlign.Center,
modifier = Modifier.padding(8.dp),
color = Color.White
)
}
},
content = { innerPadding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding)
) {
MainContent(fcmViewModel)
}
}
)
}

@Composable
fun MainContent(
fcmViewModel: FirebaseMessagingViewModel
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.Top,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
painter = painterResource(R.drawable.firebase_lockup_400),
contentDescription = "Firebase logo",
modifier = Modifier.fillMaxWidth(),
)
Text(
text = stringResource(R.string.quickstart_message),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.body1,
modifier = Modifier
.padding(top = 16.dp, start=8.dp, end=8.dp, bottom=16.dp)
)
Button(
modifier = Modifier
.width(200.dp)
.wrapContentHeight()
.padding(0.dp, 20.dp, 0.dp, 0.dp),
colors = ButtonDefaults.buttonColors(backgroundColor = colorResource(R.color.colorPrimary)),

Choose a reason for hiding this comment

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

medium

The Button's background color is hardcoded using colorResource. To better support theming (including dark theme), you should use colors from MaterialTheme.colors. The Theme.kt file already defines FirebaseBlue as the primary color for the light theme. This also applies to the other button.

            colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.primary),

onClick = {
fcmViewModel.subscribeToTopic("weather")
}
) {
Text(
textAlign = TextAlign.Center,
text = stringResource(R.string.subscribe_to_weather).uppercase(),
color = Color.White,
)
}
Button(
modifier = Modifier
.width(200.dp)
.wrapContentHeight()
.padding(0.dp, 20.dp, 0.dp, 0.dp),
colors = ButtonDefaults.buttonColors(backgroundColor = colorResource(R.color.colorPrimary)),
onClick = {
fcmViewModel.getToken()
}
) {
Text(
text = stringResource(R.string.log_token).uppercase(),
color = Color.White,
)
}
}
}

@Preview(showBackground = true)
@Composable
fun MainAppViewPreview() {
FirebaseMessagingTheme {
MainScreen()
}
Comment on lines +236 to +239

Choose a reason for hiding this comment

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

medium

This preview will likely crash because MainScreen's default viewModel() call will try to instantiate FirebaseMessagingViewModel. The factory for this ViewModel calls FirebaseMessaging.getInstance(), which is not available in a preview context. To make previews work, you should provide a mock or fake implementation of the ViewModel.

}

fun Context.findActivity(): Activity? = when (this) {
is Activity -> this
is ContextWrapper -> baseContext.findActivity()
else -> null
}
Loading