Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding compose code for FCM #1632

Open
wants to merge 5 commits into
base: compose
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 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
7 changes: 5 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,9 @@ 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'
implementation("com.google.accompanist:accompanist-permissions:0.30.1")

// 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,267 @@
package com.google.firebase.quickstart.fcm.kotlin

import android.Manifest
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.annotation.RequiresApi
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.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
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.CoroutineScope
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
) {
MainAppView()
}
}
}
}
}


@OptIn(ExperimentalPermissionsApi::class)
lehcar09 marked this conversation as resolved.
Show resolved Hide resolved
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
lehcar09 marked this conversation as resolved.
Show resolved Hide resolved
@Composable
private fun RequestNotificationPermissionDialog(
scope: CoroutineScope,
snackbarHostState: SnackbarHostState
) {
val permissionState = rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS)

val requestPermissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
var message = "Notifications permission granted"
lehcar09 marked this conversation as resolved.
Show resolved Hide resolved
if (!isGranted) {
message = "FCM can't post notifications without POST_NOTIFICATIONS permission"
lehcar09 marked this conversation as resolved.
Show resolved Hide resolved
}

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

LaunchedEffect(permissionState) {
if (!permissionState.status.isGranted) {
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}
}

@Composable
fun MainAppView(
lehcar09 marked this conversation as resolved.
Show resolved Hide resolved
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

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
RequestNotificationPermissionDialog(scope, snackbarHostState)
}

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)
)
}

if (intent != null) {
Copy link
Member

Choose a reason for hiding this comment

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

nit: we can use the more idiomatic Kotlin .let function:

Suggested change
if (intent != null) {
intent?.let {

fcmViewModel.getNotificationData(intent)
}
}
}

lifecycleOwner.lifecycle.addObserver(observer)

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

LaunchedEffect(key1 = fcmViewModel.token) {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
LaunchedEffect(key1 = fcmViewModel.token) {
LaunchedEffect(fcmViewModel.token) {

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

LaunchedEffect(key1 = fcmViewModel.subscriptionState) {
lehcar09 marked this conversation as resolved.
Show resolved Hide resolved
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)
) {
Text(
text = stringResource(R.string.app_name),
style = MaterialTheme.typography.h6,
textAlign = TextAlign.Center,
modifier = Modifier.padding(8.dp),
color = Color.White
)
}
},
content = { it ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(it)
lehcar09 marked this conversation as resolved.
Show resolved Hide resolved
) {
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)),
onClick = {
fcmViewModel.getSubscribe("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 {
MainAppView()
}
}

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