I can't figure it out, how to impalement Stripe API integration into an Compose app
Here is Stripe provided snippet of code
class CheckoutActivity : AppCompatActivity() {
lateinit var paymentSheet: PaymentSheet
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
paymentSheet = PaymentSheet(this, ::onPaymentSheetResult)
}
fun onPaymentSheetResult(paymentSheetResult: PaymentSheetResult) {
// implemented in the next steps
}
}
In my case I am out of ideas where to put paymentSheet = PaymentSheet(this, ::onPaymentSheetResult) in compose code as it shows that:
None of the following functions can be called with the arguments supplied.
(ComponentActivity, PaymentSheetResultCallback) defined in com.stripe.android.paymentsheet.PaymentSheet
(Fragment, PaymentSheetResultCallback) defined in com.stripe.android.paymentsheet.PaymentSheet
class MainActivity : ComponentActivity() {
lateinit var paymentSheet: PaymentSheet
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
PayTheme {
LoginUi()
}
}
fun onPaymentSheetResult(paymentSheetResult: PaymentSheetResult) {
// implemented in the next steps
}
}
}
First of all, you can check the stripe compose sample on github stripe-android (ComposeExampleActivity.kt).
Add stripe dependency
implementation "com.stripe:stripe-android:20.17.0"
Initialize stripe PaymentConfiguration in the Application class
#HiltAndroidApp
class BookstoreApplication : Application() {
override fun onCreate() {
super.onCreate()
PaymentConfiguration.init(applicationContext, BuildConfig.STRIPE_PUBLISHABLE_KEY)
}
}
Stripe provides many ways to implement payments in the app. Let's consider payment confirmation using PaymentSheetContract and PaymentLauncher.
Example #1: Confirm payment using PaymentSheetContract
In this case, we should use rememberLauncherForActivityResult() with PaymentSheetContract() to launch stripe payment form.
PaymentScreen.kt (Compose)
#Composable
fun PaymentScreen(
viewModel: PaymentViewModel = hiltViewModel()
) {
val stripeLauncher = rememberLauncherForActivityResult(
contract = PaymentSheetContract(),
onResult = {
viewModel.handlePaymentResult(it)
}
)
val clientSecret by viewModel.clientSecret.collectAsStateWithLifecycle()
clientSecret?.let {
val args = PaymentSheetContract.Args.createPaymentIntentArgs(it)
stripeLauncher.launch(args)
viewModel.onPaymentLaunched()
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Button(
onClick = {
viewModel.makePayment()
}
) {
Text(text = "Confirm payment")
}
}
}
PaymentViewModel.kt
#HiltViewModel
class PaymentViewModel #Inject constructor(
private val repository: PaymentRepository
) : ViewModel() {
private val _clientSecret = MutableStateFlow<String?>(null)
val clientSecret = _clientSecret.asStateFlow()
fun makePayment() {
val paymentIntent = repository.createPaymentIntent()
_clientSecret.update { paymentIntent.clientSecret }
}
fun onPaymentLaunched() {
_clientSecret.update { null }
}
fun handlePaymentResult(result: PaymentSheetResult) {
when(result) {
PaymentSheetResult.Canceled -> TODO()
PaymentSheetResult.Completed -> TODO()
is PaymentSheetResult.Failed -> TODO()
}
}
}
Example #2: Confirm payment using PaymentLauncher
In this case, we should use rememberLauncherForActivityResult() with PaymentSheetContract() to launch stripe payment form.
PaymentScreen.kt (Compose)
#Composable
fun PaymentScreen(
viewModel: PaymentViewModel = hiltViewModel()
) {
val paymentLauncher = rememberPaymentLauncher(viewModel::handlePaymentResult)
val confirmPaymentParams by viewModel.confirmPaymentParams.collectAsStateWithLifecycle()
confirmPaymentParams?.let { payment ->
paymentLauncher.confirm(payment)
viewModel.onPaymentLaunched()
}
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Button(
onClick = {
viewModel.makePayment()
}
) {
Text(text = "Confirm payment")
}
}
}
#Composable
fun rememberPaymentLauncher(
callback: PaymentLauncher.PaymentResultCallback
): PaymentLauncher {
val config = PaymentConfiguration.getInstance(LocalContext.current)
return PaymentLauncher.rememberLauncher(
publishableKey = config.publishableKey,
stripeAccountId = config.stripeAccountId,
callback = callback
)
}
PaymentViewModel.kt
#HiltViewModel
class PaymentViewModel #Inject constructor(
private val repository: PaymentRepository
) : ViewModel() {
private val _confirmPaymentParams = MutableStateFlow<ConfirmPaymentIntentParams?>(null)
val confirmPaymentParams = _confirmPaymentParams.asStateFlow()
fun makePayment() {
val paymentIntent = repository.createPaymentIntent()
// For example, pay with hardcoded test card
val configuration = ConfirmPaymentIntentParams.createWithPaymentMethodCreateParams(
paymentMethodCreateParams = PaymentMethodCreateParams.create(
card = PaymentMethodCreateParams.Card(
number = "4242424242424242",
expiryMonth = 1,
expiryYear = 24,
cvc = "111"
)
),
clientSecret = paymentIntent.clientSecret
)
_confirmPaymentParams.update { configuration }
}
fun onPaymentLaunched() {
_confirmPaymentParams.update { null }
}
fun handlePaymentResult(result: PaymentResult) {
when(result) {
PaymentResult.Canceled -> TODO()
PaymentResult.Completed -> TODO()
is PaymentResult.Failed -> TODO()
}
}
}
Data layer
The functions described below should be implemented somewhere on the server side. So, the client should only request some data from payment intent (client_secret for example).
Please, read stripe Accept a payment doc to understand better.
You can also watch youtube video: How to integrate Stripe in Android Studio 2022.
PaymentRepository.kt
class PaymentRepository #Inject constructor(
private val stripeApiService: StripeApiService,
private val paymentDao: PaymentDao
) {
/*
Create customer before payment (attach to app user)
*/
suspend fun createCustomer() = withContext(Dispatchers.IO) {
val customer = stripeApiService.createCustomer()
// save customer in the database or preferences
// customerId required to confirm payment
paymentDao.insertCustomer(customer)
}
suspend fun refreshCustomerEphemeralKey() = withContext(Dispatchers.IO) {
val customer = paymentDao.getCustomer()
val key = stripeApiService.createEphemeralKey(customer.customerId)
paymentDao.insertEphemeralKey(key)
}
suspend fun createPaymentIntent() = withContext(Dispatchers.IO) {
val customer = paymentDao.getCustomer()
refreshCustomerEphemeralKey()
val paymentIntent = stripeApiService.createPaymentIntent(
customerId = customer.customerId,
amount = 1000,
currency = "usd", // or your currency
autoPaymentMethodsEnable = true
)
return#withContext paymentIntent
}
}
StripeApiService.kt
private const val SECRET = BuildConfig.STRIPE_SECRET_KEY
interface StripeApiService {
#Headers(
"Authorization: Bearer $SECRET",
"Stripe-Version: 2022-08-01"
)
#POST("v1/customers")
suspend fun createCustomer() : CustomerApiModel
#Headers(
"Authorization: Bearer $SECRET",
"Stripe-Version: 2022-08-01"
)
#POST("v1/ephemeral_keys")
suspend fun createEphemeralKey(
#Query("customer") customerId: String
): EphemeralKeyApiModel
#Headers(
"Authorization: Bearer $SECRET"
)
#POST("v1/payment_intents")
suspend fun createPaymentIntent(
#Query("customer") customerId: String,
#Query("amount") amount: Int,
#Query("currency") currency: String,
#Query("automatic_payment_methods[enabled]") autoPaymentMethodsEnable: Boolean,
): PaymentIntentApiModel
}
Related
Hi Im currently struggling with navigation in Jetpack Compose due to #composable invocations can only happen from the context of an #composable function. I have a function:
private fun signInResult(result: FirebaseAuthUIAuthenticationResult) {
val response = result.idpResponse
if (result.resultCode == RESULT_OK) {
user = FirebaseAuth.getInstance().currentUser
Log.e("MainActivity.kt", "Innlogging vellykket")
ScreenMain()
} else {
Log.e("MainActivity.kt", "Feil med innlogging" + response?.error?.errorCode)
}
}
and used with my navigation class shown under I only get the error message shown above, how do I fix it?
#Composable
fun ScreenMain(){
val navController = rememberNavController()
NavHost(navController = navController, startDestination = Routes.Vareliste.route) {
composable(Routes.SignUp.route) {
SignUp(navController = navController)
}
composable(Routes.ForgotPassword.route) { navBackStack ->
ForgotPassword(navController = navController)
}
composable(Routes.Vareliste.route) { navBackStack ->
Vareliste(navController = navController)
}
composable(Routes.Handlekurv.route) { navBackStack ->
Handlekurv(navController = navController)
}
composable(Routes.Profileromoss.route) { navBackStack ->
Profileromoss(navController = navController)
}
}
}
EDIT WITH COMPLETE CODE
Here is the whole code for the class if you guys wanted to see it!
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
JetpackComposeDemoTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
LoginPage()
}
}
}
}
private var user: FirebaseUser? = FirebaseAuth.getInstance().currentUser
private lateinit var auth: FirebaseAuth
#Composable
fun LoginPage() {
Box(modifier = Modifier.fillMaxSize()) {
}
Column(
modifier = Modifier.padding(20.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "Velkommen til ITGuys", style = TextStyle(fontSize = 36.sp))
Spacer(modifier = Modifier.height(20.dp))
Box(modifier = Modifier.padding(40.dp, 0.dp, 40.dp, 0.dp)) {
Button(
onClick = { signIn() },
shape = RoundedCornerShape(50.dp),
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
) {
Text(text = "Logg inn")
}
}
}
}
private fun signIn() {
val providers = arrayListOf(
AuthUI.IdpConfig.EmailBuilder().build(),
AuthUI.IdpConfig.GoogleBuilder().build()
)
val signinIntent = AuthUI.getInstance()
.createSignInIntentBuilder()
.setAvailableProviders(providers)
.build()
signInLauncher.launch(signinIntent)
}
private val signInLauncher = registerForActivityResult(
FirebaseAuthUIActivityResultContract()
) {
res -> this.signInResult(res)
}
private fun signInResult(result: FirebaseAuthUIAuthenticationResult) {
val response = result.idpResponse
if (result.resultCode == RESULT_OK) {
user = FirebaseAuth.getInstance().currentUser
Log.e("MainActivity.kt", "Innlogging vellykket")
ScreenMain()
} else {
Log.e("MainActivity.kt", "Feil med innlogging" + response?.error?.errorCode)
}
}
}
I need to add more text to be allowed to post this much code you can ignore this text cause it is just for being able to post.
As #z.y mentioned, you can pass a lambda with a onFirebaseAuthSuccess. I would also add that as you are passing the navController to the signup screen, the lambda callback you need to pass should look something like
onFirebaseAuthSuccess = { navController.navigate(Routes.Profileromoss.route) } - or whatever route you need
Based on
composable(Routes.SignUp.route) {
SignUp(navController = navController)
}
I would assume your signIn screen is called from inside the scope of a composable. If you can add the extract of code containing how you are calling the signInResult function we can be sure about it.
I'm not familiar with Firebase Authentication so I'm not sure where do you call or how you use your signInResult function but you cannot invoke a function that is annotated with #Composable (ScreenMain) from a scope that is not annotated by it such as ordinary function (signInResult).
You can consider adding a lambda callback for signInResult which will be called in the RESULT_OK condition block.
private fun signInResult(result: FirebaseAuthUIAuthenticationResult, onFirebaseAuthSuccess: () -> Unit) {
val response = result.idpResponse
if (result.resultCode == RESULT_OK) {
...
...
onFirebaseAuthSuccess() // this callback
} else {
...
}
}
Edit: #sgtpotatoe has better answer, you can invoke a navigation in your root composable from the lambda callback that will navigate to your target screen.
Ok so, in your MainActivity, you want your navigational component to be at the top:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
JetpackComposeDemoTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
ScreenMain()
}
}
}
}
Then add a route to your navhost for the login page:
composable(Routes.LoginPage.route) {
LoginPage(navController = navController)
}
I think its a bit of a major change, but you would have to rely on a view model to make the authentication, so it can handle the calls, not blocking the ui or showing a loading screen, and communicate with the view
It would look something like this:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val viewModel = MyViewModel()
setContent {
JetpackComposeDemoTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
ScreenMain(viewModel)
}
}
}
}
In the LoginPage you want to access the viewmodel to start the auth service calls
In the LoginPage you want to access the viewmodel to observe if the call is succesfull, and in that case do the navigation
In the MyViewModel you want to have the authentication calls, and to update the variable that triggers the navigation if auth is succesfull
Here is an example of a sample firebase authentication app in compose, I would use it as a guide https://firebase.blog/posts/2022/05/adding-firebase-auth-to-jetpack-compose-app
I'm trying to get a display scaling feature to work with JetPack Compose. I have a ViewModel that exposes a shared preferences value as a flow, but it's definitely incorrect, as you can see below:
#HiltViewModel
class MyViewModel #Inject constructor(
#ApplicationContext private val context: Context
) : ViewModel() {
private val _densityFactor: MutableStateFlow<Float> = MutableStateFlow(1.0f)
val densityFactor: StateFlow<Float>
get() = _densityFactor.asStateFlow()
private fun getDensityFactorFromSharedPrefs(): Float {
val sharedPreference = context.getSharedPreferences(
"MY_PREFS",
Context.MODE_PRIVATE
)
return sharedPreference.getFloat("density", 1.0f)
}
// This is what I look at and go, "this is really bad."
private fun densityFactorFlow(): Flow<Float> = flow {
while (true) {
emit(getDensityFactorFromSharedPrefs())
}
}
init {
viewModelScope.launch(Dispatchers.IO) {
densityFactorFlow().collectLatest {
_densityFactor.emit(it)
}
}
}
}
Here's my Composable:
#Composable
fun MyPageRoot(
modifier: Modifier = Modifier,
viewModel: MyViewModel = hiltViewModel()
) {
val densityFactor by viewModel.densityFactor.collectAsState(initial = 1.0f)
CompositionLocalProvider(
LocalDensity provides Density(
density = LocalDensity.current.density * densityFactor
)
) {
// Content
}
}
And here's a slider that I want to slide with my finger to set the display scaling (the slider is outside the content from the MyPageRoot and will not change size on screen while the user is using the slider).
#Composable
fun ScreenDensitySetting(
modifier: Modifier = Modifier,
viewModel: SliderViewModel = hiltViewModel()
) {
var sliderValue by remember { mutableStateOf(viewModel.getDensityFactorFromSharedPrefs()) }
Text(
text = "Zoom"
)
Slider(
value = sliderValue,
onValueChange = { sliderValue = it },
onValueChangeFinished = { viewModel.setDisplayDensity(sliderValue) },
enabled = true,
valueRange = 0.5f..2.0f,
steps = 5,
colors = SliderDefaults.colors(
thumbColor = MaterialTheme.colors.secondary,
activeTrackColor = MaterialTheme.colors.secondary
)
)
}
The slider composable has its own viewmodel
#HiltViewModel
class PersonalizationMenuViewModel #Inject constructor(
#ApplicationContext private val context: Context
) : ViewModel() {
fun getDensityFactorFromSharedPrefs(): Float {
val sharedPreference = context.getSharedPreferences(
"MY_PREFS",
Context.MODE_PRIVATE
)
return sharedPreference.getFloat("density", 1.0f)
}
fun setDisplayDensity(density: Float) {
viewModelScope.launch {
val sharedPreference = context.getSharedPreferences(
"MEAL_ASSEMBLY_PREFS",
Context.MODE_PRIVATE
)
val editor = sharedPreference.edit()
editor.putFloat("density", density)
editor.apply()
}
}
}
I know that I need to move all the shared prefs code into a single class. But how would I write the flow such that it pulled from shared prefs when the value changed? I feel like I need a listener of some sort, but very new to Android development.
Your comment is right, that's really bad. :) You should create a OnSharedPreferenceChangeListener so it reacts to changes instead of locking up the CPU to constantly check it preemptively.
There's callbackFlow for converting listeners into Flows. You can use it like this:
fun SharedPreferences.getFloatFlowForKey(keyForFloat: String) = callbackFlow<Float> {
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
if (keyForFloat == key) {
trySend(getFloat(key, 0f))
}
}
registerOnSharedPreferenceChangeListener(listener)
if (contains(key)) {
send(getFloat(key, 0f)) // if you want to emit an initial pre-existing value
}
awaitClose { unregisterOnSharedPreferenceChangeListener(listener) }
}.buffer(Channel.UNLIMITED) // so trySend never fails
Then your ViewModel becomes:
#HiltViewModel
class MyViewModel #Inject constructor(
#ApplicationContext private val context: Context
) : ViewModel() {
private val sharedPreference = context.getSharedPreferences(
"MY_PREFS",
Context.MODE_PRIVATE
)
val densityFactor: StateFlow<Float> = sharedPreferences
.getFloatFlowForKey("density")
.stateIn(viewModelScope, SharingStarted.Eagerly, 1.0f)
}
The SoundViewModel is a ViewModel class, and val listSoundRecordState may be used by some modules in the App.
In Code A, I invoke fun collectListSoundRecord() when I need to use the data listSoundRecordState. But fun collectListSoundRecord() may be launched again and again because of Jetpack Compose recomposition, I don't know if it will cost many system resources?
In Code B, I launch private fun collectListSoundRecord() in init { }, collectListSoundRecord() will be launched only one time, but it will persist in memory until the App code closed even if I needn't to use the data listSoundRecordState, will the way cost many system resources?
Code A
#HiltViewModel
class SoundViewModel #Inject constructor(
...
): ViewModel() {
private val _listSoundRecordState = MutableStateFlow<Result<List<MRecord>>>(Result.Loading)
val listSoundRecordState = _listSoundRecordState.asStateFlow()
init { }
//It may be launched again and again
fun collectListSoundRecord(){
viewModelScope.launch {
listRecord().collect {
result -> _listSoundRecordState.value =result
}
}
}
private fun listRecord(): Flow<Result<List<MRecord>>> {
return aSoundMeter.listRecord()
}
}
Code B
#HiltViewModel
class SoundViewModel #Inject constructor(
...
): ViewModel() {
private val _listSoundRecordState = MutableStateFlow<Result<List<MRecord>>>(Result.Loading)
val listSoundRecordState = _listSoundRecordState.asStateFlow()
init { collectListSoundRecord() }
private fun collectListSoundRecord(){
viewModelScope.launch {
listRecord().collect {
result -> _listSoundRecordState.value =result
}
}
}
private fun listRecord(): Flow<Result<List<MRecord>>> {
return aSoundMeter.listRecord()
}
}
You would probably benefit from collecting the original flow (from listRecord()) only when there is a subscriber to your intermediate flow (the one you keep in your SoundViewModel) and cache the results.
A subscriber, in your case, would be a Composable function that collects the values (and may recompose often).
You can achieve this using the non-suspending variant of stateIn(), since you have a default value.
#HiltViewModel
class SoundViewModel #Inject constructor(
...
): ViewModel() {
val listSoundRecordState = listRecord().stateIn(viewModelScope, SharingStarted.WhileSubscribed(), Result.Loading)
private fun listRecord(): Flow<Result<List<MRecord>>> {
return aSoundMeter.listRecord()
}
}
In order to use the StateFlow from the UI layer (a #Composable function), you will have to transform it into a State, like so:
val viewModel: SoundViewModel = viewModel()
val listSoundRecord = viewModel.listSoundRecordState.collectAsState()
I use the below implementation of composable with view-model using LaunchedEffect
PokemonDetailScreen.kt
#Composable
fun PokemonDetailScreen(
dominantColor: Color,
pokemonName: String,
navController: NavController,
topPadding: Dp = 20.dp,
pokemonImageSize: Dp = 200.dp,
viewModel: PokemonDetailVm = hiltViewModel()
) {
var pokemonDetailData by remember { mutableStateOf<PokemonDetailView>(PokemonDetailView.DisplayLoadingView) }
val pokemonDetailScope = rememberCoroutineScope()
LaunchedEffect(key1 = true){
viewModel.getPokemonDetails(pokemonName)
viewModel.state.collect{ pokemonDetailData = it }
}
Box(
modifier = Modifier
.fillMaxSize()
.background(dominantColor)
.padding(bottom = 16.dp)
) {
PokemonHeader(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.2f)
.align(Alignment.TopCenter)
) {
navController.popBackStack()
}
PokemonBody(
pokemonInfo = pokemonDetailData,
topPadding = topPadding,
pokemonImageSize = pokemonImageSize
) {
pokemonDetailScope.launch {
viewModel.getPokemonDetails(pokemonName)
}
}
Box(
contentAlignment = Alignment.TopCenter,
modifier = Modifier
.fillMaxSize()
) {
if(pokemonDetailData is PokemonDetailView.DisplayPokemonView){
val data = (pokemonDetailData as PokemonDetailView.DisplayPokemonView).data
data.sprites.let {
// Image is available
val url = PokemonUtils.formatPokemonDetailUrl(it.frontDefault)
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data(url)
.crossfade(true)
.build(),
contentDescription = data.name,
contentScale = ContentScale.Fit,
modifier = Modifier
// Set the default size passed to the composable
.size(pokemonImageSize)
// Shift the image down from the top
.offset(y = topPadding)
)
}
}
}
}
}
PokemonDetailVm.kt
#HiltViewModel
class PokemonDetailVm #Inject constructor(
private val repository: PokemonRepositoryFeature
): ViewModel(){
private val _state = MutableSharedFlow<PokemonDetailView>()
val state = _state.asSharedFlow()
suspend fun getPokemonDetails(pokemonName:String) {
viewModelScope.launch {
when(val pokemonInfo = repository.getPokemonInfo(pokemonName)){
is Resource.Error -> {
pokemonInfo.message?.let {
_state.emit(PokemonDetailView.DisplayErrorView(message = it))
}
}
is Resource.Loading -> {
_state.emit(PokemonDetailView.DisplayLoadingView)
}
is Resource.Success -> {
pokemonInfo.data?.let {
_state.emit(PokemonDetailView.DisplayPokemonView(data = it))
}
}
}
}
}
}
MainActivity
#AndroidEntryPoint
class MainActivity : ComponentActivity() {
#Inject
lateinit var connectivityManager: ConnectivityManager
#Inject
lateinit var settingsDataStore: SettingsDataStore
override fun onStart() {
super.onStart()
connectivityManager.registerConnectionObserver(this)
}
override fun onDestroy() {
super.onDestroy()
connectivityManager.unregisterConnectionObserver(this)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Screen.Login.route) {
composable(
route = Screen.Login.route
) { navBackStackEntry ->
val factory = HiltViewModelFactory(LocalContext.current, navBackStackEntry)
val viewModel: LoginViewModel = viewModel(key = "LoginViewModel", factory = factory)
LoginScreen(
isNetworkAvailable = connectivityManager.isNetworkAvailable.value,
onNavigateToHomeScreen = navController::navigate,
viewModel = viewModel
)
}
composable(
route = Screen.Home.route
) { navBackStackEntry ->
val factory = HiltViewModelFactory(LocalContext.current, navBackStackEntry)
val viewModel: HomeViewModel = viewModel(key = "HomeViewModel", factory = factory)
HomeScreen(
)
}
}
}
}
}
LoginScreen
#Composable
fun LoginScreen(
isNetworkAvailable: Boolean,
onNavigateToHomeScreen: (String) -> Unit,
viewModel: LoginViewModel
) {
val loading = viewModel.loading.value
val user = viewModel.user.value
Scaffold(
modifier = Modifier.fillMaxSize()
) {
if (!loading && user == null) {
Button(onClick = { viewModel.onTriggerEvent(LoginEvent.AuthStateEvent) }) {
Column(
) {
Text(text = "Log in")
}
}
}else {
user?.let {
val route = Screen.Home.route
onNavigateToHomeScreen(route)
}
}
}
}
LoginViewModel
#HiltViewModel
class LoginViewModel #Inject constructor(
private val postLogin: PostLogin,
private val connectivityManager: ConnectivityManager
) : ViewModel() {
val user: MutableState<User?> = mutableStateOf(null)
val loading = mutableStateOf(false)
val onLoad: MutableState<Boolean> = mutableStateOf(false)
fun onTriggerEvent(event: LoginEvent) {
viewModelScope.launch {
try {
when(event) {
is LoginEvent.AuthStateEvent -> {
Log.d(TAG,"Phase 1")
if (user.value == null) {
val auth: Auth = Auth([***AuthData***])
postAuth(auth)
}
}
}
}catch (e: Exception) {
Log.d(TAG, "launchJob: Exception: ${e}, ${e.cause}")
e.printStackTrace()
}
}
}
private fun postAuth(auth: Auth) {
Log.d(TAG,"Phase 2")
postLogin.execute(auth, connectivityManager.isNetworkAvailable.value).onEach { dataState ->
loading.value = dataState.loading
dataState.data?.let { data ->
user.value = data
Log.d(TAG,"Phase 3")
Log.d(TAG,"User Data File")
}
dataState.error?.let { error ->
Log.d(TAG, "postAuth: ${error}")
}
}.launchIn(viewModelScope)
}
}
LoginEvent
sealed class LoginEvent {
object AuthStateEvent: LoginEvent()
}
PostLogin
class PostLogin(
private val apiService: ApiService,
private val authDtoMapper: AuthDtoMapper,
private val userDtoMapper: UserDtoMapper
) {
fun execute(
auth: Auth,
isNetworkAvailable: Boolean
): Flow<DataState<User>> = flow {
//auth -> authDto -> InterActions -> userDto -> user
try {
emit(DataState.loading())
delay(1000)
if (isNetworkAvailable) {
val networkUser = postLoginFromNetwork(auth)
if (networkUser != null) {
emit(DataState.success(networkUser))
}else {
emit(DataState.error<User>("User data is null"))
}
}else {
emit(DataState.error<User>("Unable to connect network"))
}
}catch (e: Exception) {
emit(DataState.error<User>(e.message?: "Unknown Error"))
}
}
private suspend fun postLoginFromNetwork(auth: Auth): User {
return userDtoMapper.mapToDomainModel(apiService.login(authDtoMapper.fromDomain(auth)))
}
}
HomeScreen
#Composable
fun HomeScreen() {
Card(
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "HomeScreen",
fontWeight = FontWeight.Bold,
fontSize = 40.sp
)
}
}
}
Hello, I made the above code go to HomeScreen when login is successful by pressing the Login button on LoginScreen.
It was confirmed that the communication was smooth and the value was also brought.
However, when I tried debugging, I confirmed that this part was repeatedly done on Login Screen.
user?.let {
val route = Screen.Home.route
onNavigateToHomeScreen(route)
}
As you can see, when the user has a value, the screen is moved, but that part is repeatedly done, so the HomeScreen screen glitters like a flash.
That's why I'm asking you this question. When the value changes after communication, please tell me how to move the screen.
Use the LauchedEffect
LaunchedEffect(user){
user.let {
navController.navigate("")
}
}
A very newbie programmer here and not a good English typer. Im trying to create a checker for purchase that already made previous using the PurchaseHistoryResponseListener. And When a checker found something, it will add to a list and then feed the recyclerview_MYBook with that data. The issue is that when launching the app, the data is flow through the recyclerview_MYBook perfectly, but when moving to different activity and going back to the previous activity through a different method (button click) the data on the recyclerview_MYBook doesn't show up, only through a conventional back button, the data on the recyclerview show up. Below here is my noob code
class MainActivity : AppCompatActivity(), PurchasesUpdatedListener {
private lateinit var billingClient: BillingClient
private lateinit var blogadapternew: BlogRecyclerAdapterNew
private lateinit var blogadapterpremium: BlogRecyclerAdapterPremium
private lateinit var blogadapterfree: BlogRecyclerAdapterFree
private lateinit var blogadaptermybook: BlogRecyclerAdapterMyBook
private lateinit var auth: FirebaseAuth
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
auth = FirebaseAuth.getInstance()
//FirebaseAuth.getInstance().signOut()
window.setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE)
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
setContentView(R.layout.activity_main)
recycler_viewNew.layoutManager = LinearLayoutManager(this, RecyclerView.HORIZONTAL,false)
recycler_viewNew.adapter= BlogRecyclerAdapterNew()
recycler_viewPremium.layoutManager = LinearLayoutManager(this,RecyclerView.HORIZONTAL,false)
recycler_viewPremium.adapter= BlogRecyclerAdapterPremium()
recycler_viewFree.layoutManager = LinearLayoutManager(this,RecyclerView.HORIZONTAL,false)
recycler_viewFree.adapter= BlogRecyclerAdapterFree()
recycler_viewMyBook.layoutManager = LinearLayoutManager(this,RecyclerView.HORIZONTAL,false)
recycler_viewMyBook.adapter= BlogRecyclerAdapterMyBook()
if (supportActionBar != null)
supportActionBar?.hide()
setupBillingClient()
initrecyclerView()
initrecyclerViewPremium()
initrecyclerViewFree()
initrecyclerViewMyBook()
addDataSetNew()
addDataSetPremium()
addDataSetFree()
Logo.setOnClickListener{
val intent = Intent(MonstaLogo.context, MainActivity::class.java)
MonstaLogo.context.startActivity(intent)
}
MainFeaturedButton.setOnClickListener {
val intent = Intent(MainFeaturedButton.context, MainActivity::class.java)
MainFeaturedButton.context.startActivity(intent)
}
MainNewButton.setOnClickListener {
val intent = Intent(MainNewButton.context, NewActivity::class.java)
MainNewButton.context.startActivity(intent)
}
NewMore.setOnClickListener{
val intent = Intent(NewMore.context, NewActivity::class.java)
NewMore.context.startActivity(intent)
}
MainPremiumButton.setOnClickListener {
val intent = Intent(MainPremiumButton.context, PremiumActivity::class.java)
MainPremiumButton.context.startActivity(intent)
}
PremiumMore.setOnClickListener{
val intent = Intent(PremiumMore.context, PremiumActivity::class.java)
PremiumMore.context.startActivity(intent)
}
MainFreeButton.setOnClickListener {
val intent = Intent(MainFreeButton.context, FreeActivity::class.java)
MainFreeButton.context.startActivity(intent)
}
FreeMore.setOnClickListener {
val intent = Intent(FreeMore.context, FreeActivity::class.java)
FreeMore.context.startActivity(intent)
}
MainMyBookButton.setOnClickListener {
val intent = Intent(MainMyBookButton.context, MyBookActivity::class.java)
MainMyBookButton.context.startActivity(intent)
}
MyBookMore.setOnClickListener {
val intent = Intent(MyBookMore.context, MyBookActivity::class.java)
MyBookMore.context.startActivity(intent)
}
}
private fun setupBillingClient() {
billingClient = BillingClient.newBuilder(this)
.enablePendingPurchases()
.setListener(this)
.build()
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
// The BillingClient is ready. You can query purchases here.
println("Setup Billing Done")
PurchaseHistoryResponseListener()
}
}
override fun onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
println("Failed")
setupBillingClient()
println("Restart Connection")
}
})
}
override fun onPurchasesUpdated(
billingResult: BillingResult?,
purchases: MutableList<Purchase>?
) {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
private fun PurchaseHistoryResponseListener (){
billingClient.queryPurchaseHistoryAsync(BillingClient.SkuType.INAPP) {
responseCode, result ->
// println("queryPurchasesAsync INAPP results: ${result?.size}")
// println("Getting Purchase History")
println("$result")
val dataMyBook1 = DataSourceMyBook.createDataSet()
if ("testcode1" in result.toString()) {
println("found it 1")
dataMyBook1.add((BlogPost( "BookName","Link","No")))
}
if ("testcode2" in result.toString()) {
println("found it 2")
dataMyBook1.add((BlogPost( "BookName","Link","No")))
}
if ("testcode3" in result.toString()) {
println("found it 3")
dataMyBook1.add((BlogPost( "BookName","Link","No")))
}
blogadaptermybook.submitList(dataMyBook1)
println(dataMyBook1)
}
}
private fun addDataSetNew(){
val dataNew = DataSourceNew.createDataSet()
blogadapternew.submitList(dataNew)
}
private fun addDataSetPremium(){
val dataPremium = DataSourcePremium.createDataSet()
blogadapterpremium.submitList(dataPremium)
}
private fun addDataSetFree(){
val dataFree = DataSourceFree.createDataSet()
blogadapterfree.submitList(dataFree)
}
/*private fun addDataSetMyBook(){
val dataMyBook1 = DataSourceMyBook.createDataSet()
blogadaptermybook.submitList(dataMyBook1)
}*/
/*private fun addDataSetMyBook(){
val dataMyBook1 = DataSourceMyBook.createDataSet()
billingClient.queryPurchaseHistoryAsync(BillingClient.SkuType.INAPP) {
responseCode, result ->
println("$result")
if ("bbbg_s2_c1_testcode1" in result.toString()){
dataMyBook1.add((BlogPost( "Mini Comic 1","Link","No")))
}
if ("bbbg_s2_c1_testcode2" in result.toString()){
dataMyBook1.add((BlogPost( "Mini Comic 2","Link","No")))
}
if ("bbbg_s2_c1_testcode3" in result.toString()){
dataMyBook1.add((BlogPost( "Mini Comic 3","Link","No")))
}
blogadaptermybook.submitList(dataMyBook1)
}}*/
/*dataMyBook.add((BlogPost( "Mini Comic 1","Link","No")))
dataMyBook.add((BlogPost( "Mini Comic 1","Link","No")))
dataMyBook.add((BlogPost( "Mini Comic 1","Link","No")))*/
private fun initrecyclerView(){
recycler_viewNew.apply {
layoutManager = LinearLayoutManager(this#MainActivity,RecyclerView.HORIZONTAL,false)
val topSpacingItemDecoration = TopSpacingItemDecoration(padding = 30)
addItemDecoration(topSpacingItemDecoration)
blogadapternew = BlogRecyclerAdapterNew()
adapter = blogadapternew
}
}
private fun initrecyclerViewPremium(){
recycler_viewPremium.apply {
layoutManager = LinearLayoutManager(this#MainActivity,RecyclerView.HORIZONTAL,false)
val topSpacingItemDecoration = TopSpacingItemDecoration(padding = 30)
addItemDecoration(topSpacingItemDecoration)
blogadapterpremium = BlogRecyclerAdapterPremium()
adapter = blogadapterpremium
}
}
private fun initrecyclerViewFree(){
recycler_viewFree.apply {
layoutManager = LinearLayoutManager(this#MainActivity,RecyclerView.HORIZONTAL,false)
val topSpacingItemDecoration = TopSpacingItemDecoration(padding = 30)
addItemDecoration(topSpacingItemDecoration)
blogadapterfree = BlogRecyclerAdapterFree()
adapter = blogadapterfree
}
}
private fun initrecyclerViewMyBook(){
recycler_viewMyBook.apply {
layoutManager =
LinearLayoutManager(this#MainActivity, RecyclerView.HORIZONTAL, false)
val topSpacingItemDecoration = TopSpacingItemDecoration(padding = 30)
addItemDecoration(topSpacingItemDecoration)
blogadaptermybook = BlogRecyclerAdapterMyBook()
adapter = blogadaptermybook
}
}
public override fun onStart() {
super.onStart()
val currentUser = auth.currentUser
updateUI(currentUser)
}
private fun updateUI(currentUser: FirebaseUser?) {
if (currentUser != null) {
AccountSettingButton.setImageResource(R.drawable.profileicon)
}
}
}
Here is adapter
class BlogRecyclerAdapterMyBook : RecyclerView.Adapter() {
private var items: List<BlogPost> = ArrayList()
private var items2: List<BlogPost> = ArrayList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return BlogViewHolder(
LayoutInflater.from(parent.context).inflate(
R.layout.layout_blog_list_item_mybook,
parent,
false
)
)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is BlogViewHolder -> {
holder.bind(items.get(position))
holder.bind(items2.get(position))
}
}
}
override fun getItemCount(): Int {
return items.size
}
fun submitList(bloglist: List<BlogPost>) {
items = bloglist
items2 = bloglist
}
class BlogViewHolder constructor(itemView: View) : RecyclerView.ViewHolder(itemView) {
val blogImage: ImageButton = itemView.blog_imagemybook
val blogTitle: TextView = itemView.blog_titlemybook
val premiumImage: ImageView = itemView.premiumicon
fun bind(blogPost: BlogPost) {
blogTitle.setText(blogPost.title)
val requestOptions = RequestOptions()
.placeholder(R.drawable.mocksplash)
.error(R.drawable.disconnect)
Glide.with(itemView.blog_imagemybook)
.applyDefaultRequestOptions(requestOptions)
.load(blogPost.image)
.into(blogImage)
blogImage.setOnClickListener {
Toast.makeText(blogImage.context, "<<Swipe left<<", Toast.LENGTH_SHORT).show()
val intent = Intent(blogTitle.context, ComicReadingActivity::class.java)
var KomikName = blogTitle.text.toString()
intent.putExtra("KomikName",Name)
blogImage.context.startActivity(intent)
}
}
}
}
and here the data source file where that will store the data for the adapter
class DataSourceMyBook{
companion object{
fun createDataSet(): ArrayList<BlogPost> {
val dataMyBook1 = ArrayList<BlogPost>()
return dataMyBook1
}
}
}