New to Android Development (2 months)
have a problem with emitting and collecting data. kinda hart to understand what's going on.
The problem is that, now i am getting a request token which i need to get session ID after logging in.
How can i store this token and then how can i use it in another flow which gives me session id?
Thanks!
i have this in ViewModel.kt
class LoginViewModel(application: Application): AndroidViewModel(application) {
val apiKey = "acdbc7ef61877f0d6b3e29d062218ccc"
private val _loginState = MutableSharedFlow<Resource<TokenResponse>>()
val loginState = _loginState.asSharedFlow()
fun getActualKey(apiKey: String){
viewModelScope.launch {
loginResponse(apiKey).collect{
_loginState.emit(it)
}
}
}
fun loginResponse(apiKey: String) = flow {
val response = RetrofitHelper.tokenService.getRequestToken(apiKey)
if (response.isSuccessful) {
val body = response.body()
Resource.Success(body).let {
emit(Resource.Success(body!!))
}
} else {
val error = response.errorBody()?.string()
emit(Resource.Error(error.toString()))
}
}
}
and this in Fragment where i collect.
class LoginFragment : BaseFragment<FragmentLoginBinding>(FragmentLoginBinding::inflate) {
private val loginViewModel: LoginViewModel by viewModels()
val apiKey = "acdbc7ef61877f0d6b3e29d062218ccc"
override fun viewCreated() {
observe()
}
override fun listeners() {
binding.btnLogin.setOnClickListener {
viewLifecycleOwner.lifecycleScope.launch {
loginViewModel.loginState.collectLatest {
loginViewModel.getActualKey(apiKey)
}
}
}
}
private fun observe() {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
loginViewModel.loginState.collectLatest {
when (it) {
is Resource.Success -> {
Log.d("tag", "cool ${it.data.requestToken}")
}
is Resource.Error -> {
Log.d("tag", "error")
}
is Resource.Loading -> {
Log.d("tag", "loading")
}
}
}
}
}
}
}
I don't figured out what you up to
if you want to simply get last emitted value from another flow without using collect you can use MutableStateFlow that always contains latest value in it without suspention
but if you want to combine two (or more) flows into one flow
you can use combine function, it passes latest emitted values to your provided lambda (transform), so you can transform them to one flow
here is a sample
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOf
fun main() {
val a = flowOf(1, 2, 3)
val b = flowOf("a", "b", "c")
val combinedFlow: Flow<Pair<Int, String>> =combine(a, b) { latestOfA, latestOfB ->
latestOfA to latestOfB
}
}
Related
I m relatively new in kotlin flows and I m creating the Login Module using Flows in android. I have been stuck from past few days in flows as I m collecting it in ViewModels but I m facing problem when requesting with wrong Credentials its caching all the state. After entering the right credentials the user navigate to main Activity but the instance of the MainActivity is being created with every emitted State: Example(User Enter 3 wrong Credential and 1 Right Credential: 4 Instance of MainActivity Created). So, Is there any way that I can cancel the previous emit and only show the latest request. I m using the collectLatest as well but its not working too. Below is the code.
LoginActivity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(mViewBinding.root)
loginListener()
}
override fun onStart() {
super.onStart()
initViews()
handleNetworkChanges()
}
private fun observeLogin() {
lifecycleScope.launchWhenCreated {
mViewModel.loginCredentials.collect { state ->
when(state){
is State.Loading -> {
showLoading()
}
is State.Success -> {
Timber.d("I m in Success" + state.data)
val intent = Intent(this#LoginActivity,MainActivity::class.java)
startActivity(intent)
closeLoading()
finish()
}
is State.Error -> {
val errorResponse = gson.fromJson(state.message,LoginResponse::class.java)
showToast(errorResponse.messages)
closeLoading()
}
}
}
}
}
private fun loginListener() {
mViewBinding.neumorphButtonSignIn.setOnClickListener {
observeLogin()
phoneNumber = mViewBinding.edtPhoneNumber.text.toString()
pin = mViewBinding.oldPIN.text.toString()
if (phoneNumber.isValidPhone()) {
sendLoginCredentials(phoneNumber ,pin)
}
else {
mViewBinding.edtPhoneNumber.snack("Please Enter valid phone number") {
action("ok") {
dismiss()
}
}
}
}
}
private fun sendLoginCredentials(phoneNumber: String , pin: String) = mViewModel.postLoginCredentials("03XXXX" , "1234")
LoginViewModel
#ExperimentalCoroutinesApi
#HiltViewModel
class LoginViewModel #Inject constructor(
private val loginRepository: LoginRepository,
) : ViewModel() {
private val _loginCredentials: MutableStateFlow<State<LoginResponse>> = MutableStateFlow(State.Empty())
val loginCredentials: StateFlow<State<LoginResponse>> get() = _loginCredentials
fun postLoginCredentials(phoneNumber: String, pin: String) {
Timber.d("postLoginCredentials: $phoneNumber + $pin")
_loginCredentials.value = State.loading()
viewModelScope.launch {
loginRepository.login(LoginRequest(phoneNumber,pin))
.map { response -> State.fromResource(response) }
.collect{state -> _loginCredentials.value = state }
}
}
}
LoginRepository
class LoginRepository #Inject constructor(
private val apiInterface: APIInterface
) {
fun login(loginRequest: LoginRequest): Flow<ResponseAPI<LoginResponse>> {
return object : NetworkBoundRepository<LoginRequest, LoginResponse>() {
override suspend fun fetchFromRemote(): Response<LoginResponse> = apiInterface.createLoginRequest(
loginRequest
)
}.asFlow()
}
NetworkBoundRepository
abstract class NetworkBoundRepository<RESULT, REQUEST> {
fun asFlow() = flow<ResponseAPI<REQUEST>> {
val apiResponse = fetchFromRemote()
val remotePosts = apiResponse.body()
if (apiResponse.isSuccessful && remotePosts != null) {
emit(ResponseAPI.Success(remotePosts))
} else {
// Something went wrong! Emit Error state.
emit(ResponseAPI.Failed(apiResponse.errorBody()!!.string()))
}
}.catch { e ->
e.printStackTrace()
emit(ResponseAPI.Failed("Network error! Can't get latest posts."))
}
#MainThread
protected abstract suspend fun fetchFromRemote(): Response<REQUEST>
}
Is there any way that I can create One Instance of MainAcitivity while ignoring the older emitted Responses? Any Operator which can work. Any help in this regard is highly appreciated. Thanks.
Actually, I was calling the observeLogin() from the login click Listener which was creating this mess in my project when I move this to onCreate(). Everything works the way as intended. So, posting this for newbie that won't stuck into this.
I have the following:
interface CartRepository {
fun getCart(): Flow<CartState>
}
interface ProductRepository {
fun getProductByEan(ean: String): Flow<Either<ServerError, Product?>>
}
class ScanViewModel(
private val productRepository: ProductRepository,
private val cartRepository: CartRepository
) :
BaseViewModel<ScanUiState>(Initial) {
fun fetchProduct(ean: String) = viewModelScope.launch {
setState(Loading)
productRepository
.getProductByEan(ean)
.combine(cartRepository.getCart(), combineToGridItem())
.collect { result ->
when (result) {
is Either.Left -> {
sendEvent(Error(R.string.error_barcode_product_not_found, null))
setState(Initial)
}
is Either.Right -> {
setState(ProductUpdated(result.right))
}
}
}
}
}
When a user scans a barcode fetchProduct is being called. Every time a new coroutine is being set up. And after a while, there are many running in the background and the combine is triggered when the cart state is updated on all of them, which can cause errors.
I want to cancel all old coroutines and only have the latest call running and update on cart change.
I know I can do the following by saving the job and canceling it before starting a new one. But is this really the way to go? Seems like I'm missing something.
var searchJob: Job? = null
private fun processImage(frame: Frame) {
barcodeScanner.process(frame.toInputImage(this))
.addOnSuccessListener { barcodes ->
barcodes.firstOrNull()?.rawValue?.let { ean ->
searchJob?.cancel()
searchJob = viewModel.fetchProduct(ean)
}
}
.addOnFailureListener {
Timber.e(it)
messageMaker.showError(
binding.root,
getString(R.string.unknown_error)
)
}
}
I could also have a MutableSharedFlow in my ViewModel to make sure the UI only react to the last product the user has been fetching:
private val productFlow = MutableSharedFlow<Either<ServerError, Product?>>(replay = 1)
init {
viewModelScope.launch {
productFlow.combine(
mycroftRepository.getCart(),
combineToGridItem()
).collect { result ->
when (result) {
is Either.Right -> {
setState(ProductUpdated(result.right))
}
else -> {
sendEvent(Error(R.string.error_barcode_product_not_found, null))
setState(Initial)
}
}
}
}
}
fun fetchProduct(ean: String) = viewModelScope.launch {
setState(Loading)
repository.getProductByEan(ean).collect { result ->
productFlow.emit(result)
}
}
What's considered best practice handling this scenario?
I can't think of a simpler pattern for cancelling any previous Job when starting a new one.
If you're concerned about losing your stored job reference on screen rotation (you probably won't since Fragment instances are typically reused on rotation), you can move Job storage and cancellation into the ViewModel:
private var fetchProductJob: Job? = null
fun fetchProduct(ean: String) {
fetchProductJob?.cancel()
fetchProductJob = viewModelScope.launch {
//...
}
}
If you're repeatedly using this pattern, you could create a helper class like this. Not sure if there's a better way.
class SingleJobPipe(val scope: CoroutineScope) {
private var job: Job? = null
fun launch(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> Unit
): Job = synchronized(this) {
job?.cancel()
scope.launch(context, start, block).also { job = it }
}
}
// ...
private val fetchProductPipe = SingleJobPipe(viewModelScope)
fun fetchProduct(ean: String) = fetchProductPipe.launch {
//...
}
I think my observable is set incorrectly here. I am using Retrofit2 + Moshi as the deserializer, and the API call from Retrofit is working.
But once I make the API call, I am trying to set up the Observable in my Activity and then use the API call data from the data class.
Here is my view model code:
class DealsViewModel(val repository: MainRepository) : ViewModel() {
val movieList = MutableLiveData<List<DealItems>>()
var job: Job? = null
val loading = MutableLiveData<Boolean>()
val errorMessage = MutableLiveData<String>()
val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
onError("Exception handled: ${throwable.localizedMessage}")
}
fun getMovies() {
viewModelScope.launch{
// View Model Scope gives the Coroutine that will be canceled when the ViewModel is cleared.
job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {
val items = repository.getProduct()
withContext(Dispatchers.Main) {
if (items.isNullOrEmpty()) {
loading.value = false
// put error message in here later
} else {
dealList.postValue(items)
return#withContext
}
}
}
}
}
private fun onError(message: String) {
errorMessage.value = message
loading.value = false
}
override fun onCleared() {
super.onCleared()
job?.cancel()
}
}
And here is my MainActivity code.
I am using JetpackCompose in my activity, LiveData for the API response container. In my main repository is where I am validating a successful API response and then the coroutines for the call are inside of the view model.
My API call is successful, but I am not sure where to call the ViewModel.GetMovies() inside of the activity and I am not sure if the observables are set properly and/or where to pass the API's livedata into my composable function.
Thanks for any help you can provide. I am new to android and trying to use Coroutines for the first time.
class MainActivity: ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val retrofitService = RetrofitService.getInstance()
val viewModel = ViewModelProvider(this,
MyViewModelFactory(MainRepository(retrofitService = retrofitService))).get(DealsViewModel::class.java)
// viewModel.getProducts()
setContent {
myApp {
MyScreenContent()
}
viewModel.movieList.observe(
this, { it ->
if( it != null) {
it.forEach {
var movieLocation = it.movieLocation
val description = it.description
val id = it.id
val title = it.title
val regularPrice = it.regularPrice
}
}
})
return#setContent
}
viewModel.errorMessage.observe(this, {
Toast.makeText(this, it, Toast.LENGTH_SHORT).show()
})
viewModel.loading.observe(
this,
Observer {
if (it) {
}
})
}
}
I assume that it always depends when should you call especially in the activity we have many lifecycles; however, the best way is to use the .also on the livedata/stateflow lazy creation so that you do guarantee as long as the view model is alive, the getMovies is called only one time, and also guarantee the service itself is not called unless someone is listening to it.
You may check the full documentation in this link
Here is a code example
class MyViewModel : ViewModel() {
private val users: MutableLiveData<List<User>> by lazy {
MutableLiveData<List<User>>().also {
loadUsers()
}
}
fun getUsers(): LiveData<List<User>> {
return users
}
private fun loadUsers() {
// Do an asynchronous operation to fetch users.
}
}
When using this code, you do not have to call getMovies at all in the activity, you just listen to the observer.
I am implementing an "Add to favourites" functionality in a detail screen. If the user taps the FAB, I want to set the fab as selected and update my database. How could I use the same value that I am sending to the database to be used in my fragment (to be consistent, in case there is some issue while updating the DB)
Fragment
class BeerDetailsFragment : Fragment(R.layout.fragment_beer_details) {
private val viewModel by viewModels<BeerDetailsViewModel>()
private val args by navArgs<BeerDetailsFragmentArgs>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
subscribeToObservers()
viewModel.getBeer(args.beerId)
}
private fun subscribeToObservers() {
viewModel.beer.observe(viewLifecycleOwner, { resource ->
when(resource.status) {
Status.SUCCESS -> {
loadData(resource.data)
}
Status.ERROR -> {
showError(resource.message)
}
Status.LOADING -> {}
}
})
}
private fun loadData(beerDetails: BeerDomainModel?) {
if (beerDetails != null) {
Glide.with(requireContext())
.load(beerDetails.imageMedium)
.placeholder(R.drawable.ic_beer)
.error(R.drawable.ic_beer)
.fallback(R.drawable.ic_beer)
.into(beerDetailsImage)
beerDetailsName.text = beerDetails.name
beerDetailsDescription.text = beerDetails.description
fab.isSelected = beerDetails.isFavourite
fab.setOnClickListener {
viewModel.updateBeer(beerDetails)
// I shouldn't do it like this in case there is an issue while updating the DB
fab.isSelected = !beerDetails.isFavourite
}
}
}
...
View Model class
class BeerDetailsViewModel #ViewModelInject constructor(private val repository: BreweryRepository) : ViewModel() {
private val beerId = MutableLiveData<String>()
fun getBeer(id: String) {
beerId.value = id
}
var beer = beerId.switchMap { id ->
liveData {
emit(Resource.loading(null))
emit(repository.getBeer(id))
}
}
fun updateBeer(beer: BeerDomainModel) {
viewModelScope.launch {
repository.updateBeer(beer)
}
}
}
Repository
class BreweryRepository #Inject constructor(private val breweryApi: BreweryApi, private val beerDao: BeerDao, private val responseHandler: ResponseHandler) {
...
suspend fun getBeer(id: String): Resource<BeerDomainModel> {
return try {
withContext(IO) {
val isInDB = beerDao.isInDB(id)
if (!isInDB) {
val response = breweryApi.getBeer(id).beer.toDomainModel()
beerDao.insert(response.toBeerEntity())
responseHandler.handleSuccess(response)
} else {
val beer = beerDao.get(id).toDomainModel()
responseHandler.handleSuccess(beer)
}
}
} catch (e: Exception) {
responseHandler.handleException(e)
}
}
suspend fun updateBeer(beer: BeerDomainModel) {
withContext(IO) {
val dbBeer = with(beer) {
copy(isFavourite = !isFavourite)
toBeerEntity()
}
beerDao.update(dbBeer)
}
}
}
I would prefer to use a unidirectional flow with the following implementation:
Not sure how is your DAO implemented, but if you are using Room you could update your get method to return a Flow instead. That way whenever your data is updated, you will get back the updated data.
Then in your VM you just get that Flow or stream of data and subscribe to the updates. Flow has a very convenient method: asLiveData() so your code will look much cleaner.
If you are not using Room, then what I'd do is either construct a Flow or some type of stream and on updates successful updates send out the new data.
My problem is, that when I try to get a document out of my database, that this document aka the object is always null. I only have this problem when I use Kotlin Coroutines to get the document out of my database. Using the standard approach with listeners do work.
EmailRepository
interface EmailRepository {
suspend fun getCalibratePrice(): Flow<EmailEntity?>
suspend fun getRepairPrice(): Flow<EmailEntity?>
}
EmailRepository Implementation
class EmailRepositoryImpl #Inject constructor(private val db: FirebaseFirestore) : EmailRepository {
fun hasInternet(): Boolean {
return true
}
// This works! When using flow to write a document, the document is written!
override fun sendEmail(email: Email)= flow {
emit(EmailStatus.loading())
if (hasInternet()) {
db.collection("emails").add(email).await()
emit(EmailStatus.success(Unit))
} else {
emit(EmailStatus.failed<Unit>("No Email connection"))
}
}.catch {
emit(EmailStatus.failed(it.message.toString()))
}.flowOn(Dispatchers.Main)
// This does not work! "EmailEntity" is always null. I checked the document path!
override suspend fun getCalibratePrice(): Flow<EmailEntity?> = flow {
val result = db.collection("emailprice").document("Kalibrieren").get().await()
emit(result.toObject<EmailEntity>())
}.catch {
}.flowOn(Dispatchers.Main)
// This does not work! "EmailEntity" is always null. I checked the document path!
override suspend fun getRepairPrice(): Flow<EmailEntity?> = flow {
val result = db.collection("emailprice").document("Reparieren").get().await()
emit(result.toObject<EmailEntity>())
}.catch {
}.flowOn(Dispatchers.Main)
}
Viewmodel where I get the data
init {
viewModelScope.launch {
withContext(Dispatchers.IO) {
if (subject.value != null){
when(subject.value) {
"Test" -> {
emailRepository.getCalibratePrice().collect {
emailEntity.value = it
}
}
"Toast" -> {
emailRepository.getRepairPrice().collect {
emailEntity.value = it
}
}
}
}
}
}
}
private val emailEntity = MutableLiveData<EmailEntity?>()
private val _subject = MutableLiveData<String>()
val subject: LiveData<String> get() = _subject
Fragment
#AndroidEntryPoint
class CalibrateRepairMessageFragment() : EmailFragment<FragmentCalibrateRepairMessageBinding>(
R.layout.fragment_calibrate_repair_message,
) {
// Get current toolbar Title and send it to the next fragment.
private val toolbarText: CharSequence by lazy { toolbar_title.text }
override val viewModel: EmailViewModel by navGraphViewModels(R.id.nav_send_email) { defaultViewModelProviderFactory }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Here I set the data from the MutableLiveData "subject". I don't know how to do it better
viewModel.setSubject(toolbarText.toString())
}
}
One would say, that the Firebase rules are the problems here, but that should not be the case here, because the database is open and using the listener approach does work.
I get the subject.value from my CalibrateRepairMessageFragment. When I don't check if(subject.value != null) I get a NullPointerException from my init block.
I will use the emailEntitiy only in my viewModel and not outside it.
I appreciate every help, thank you.
EDIT
This is the new way I get the data. The object is still null! I've also added Timber.d messages in my suspend functions which also never get executed therefore flow never throws an error.. With this new approach I don't get a NullPointerException anymore
private val emailEntity = liveData {
when(subject.value) {
"Test" -> emailRepository.getCalibratePrice().collect {
emit(it)
}
"Toast" -> emailRepository.getRepairPrice().collect {
emit(it)
}
// Else block is never executed, therefore "subject.value" is either Test or toast and the logic works. Still error when using flow!
else -> EmailEntity("ERROR", 0F)
}
}
I check if the emailEntity is null or not with Timber.d("EmailEntity is ${emailEntity.value}") in one of my functions.
I then set the price with val price = MutableLiveData(emailEntity.value?.basePrice ?: 1000F) but because emailentity is null the price is always 1000
EDIT 2
I have now further researched the problem and made a big step forward. When observing the emailEntity from a fragment like CalibrateRepairMessageFragment the value is no longer null.
Furthermore, when observing emailEntity the value is also not null in viewModel, but only when it is observed in one fragment! So how can I observe emailEntity from my viewModel or get the value from my repository and use it in my viewmodel?
Okay, I have solved my problem, this is the final solution:
Status class
sealed class Status<out T> {
data class Success<out T>(val data: T) : Status<T>()
class Loading<T> : Status<T>()
data class Failure<out T>(val message: String?) : Status<T>()
companion object {
fun <T> success(data: T) = Success<T>(data)
fun <T> loading() = Loading<T>()
fun <T> failed(message: String?) = Failure<T>(message)
}
}
EmailRepository
interface EmailRepository {
fun sendEmail(email: Email): Flow<Status<Unit>>
suspend fun getCalibratePrice(): Flow<Status<CalibrateRepairPricing?>>
suspend fun getRepairPrice(): Flow<Status<CalibrateRepairPricing?>>
}
EmailRepositoryImpl
class EmailRepositoryImpl (private val db: FirebaseFirestore) : EmailRepository {
fun hasInternet(): Boolean {
return true
}
override fun sendEmail(email: Email)= flow {
Timber.d("Executed Send Email Repository")
emit(Status.loading())
if (hasInternet()) {
db.collection("emails").add(email).await()
emit(Status.success(Unit))
} else {
emit(Status.failed<Unit>("No Internet connection"))
}
}.catch {
emit(Status.failed(it.message.toString()))
}.flowOn(Dispatchers.Main)
// Sends status and object to viewModel
override suspend fun getCalibratePrice(): Flow<Status<CalibrateRepairPricing?>> = flow {
emit(Status.loading())
val entity = db.collection("emailprice").document("Kalibrieren").get().await().toObject<CalibrateRepairPricing>()
emit(Status.success(entity))
}.catch {
Timber.d("Error on getCalibrate Price")
emit(Status.failed(it.message.toString()))
}
// Sends status and object to viewModel
override suspend fun getRepairPrice(): Flow<Status<CalibrateRepairPricing?>> = flow {
emit(Status.loading())
val entity = db.collection("emailprice").document("Kalibrieren").get().await().toObject<CalibrateRepairPricing>()
emit(Status.success(entity))
}.catch {
Timber.d("Error on getRepairPrice")
emit(Status.failed(it.message.toString()))
}
}
ViewModel
private lateinit var calibrateRepairPrice: CalibrateRepairPricing
private val _calirateRepairPriceErrorState = MutableLiveData<Status<Unit>>()
val calibrateRepairPriceErrorState: LiveData<Status<Unit>> get() = _calirateRepairPriceErrorState
init {
viewModelScope.launch {
when(_subject.value.toString()) {
"Toast" -> emailRepository.getCalibratePrice().collect {
when(it) {
is Status.Success -> {
calibrateRepairPrice = it.data!!
_calirateRepairPriceErrorState.postValue(Status.success(Unit))
}
is Status.Loading -> _calirateRepairPriceErrorState.postValue(Status.loading())
is Status.Failure -> _calirateRepairPriceErrorState.postValue(Status.failed(it.message))
}
}
else -> emailRepository.getRepairPrice().collect {
when(it) {
is Status.Success -> {
calibrateRepairPrice = it.data!!
_calirateRepairPriceErrorState.postValue(Status.success(Unit))
}
is Status.Loading -> _calirateRepairPriceErrorState.postValue(Status.loading())
is Status.Failure -> _calirateRepairPriceErrorState.postValue(Status.failed(it.message))
}
}
}
price.postValue(calibrateRepairPrice.head!!.basePrice)
}
}
You can now observe the status in one of your fragments (but you dont need to!)
Fragment
viewModel.calibrateRepairPriceErrorState.observe(viewLifecycleOwner) { status ->
when(status) {
is Status.Success -> requireContext().toast("Price successfully loaded")
is Status.Loading -> requireContext().toast("Price is loading")
is Status.Failure -> requireContext().toast("Error, Price could not be loaded")
}
}
This is my toast extensions function:
fun Context.toast(text: String, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(this, text, duration).show()
}