I want to post some data to webservice and get the result . this is my code :
fab.setOnClickListener {
viewModel.newBimeGozar(name)
.observe(this#BimeGozarAct, Observer {
dialogbimegozarNew?.hidePg()
})
}
this is my viewmodel :
class BimeNewViewModel:ViewModel() {
private val repository=BimeNewRepository()
fun newBimeGozar(name: String): MutableLiveData<StatModel> {
return repository.newBimeGozar(name)
}
this is my repository :
fun newBimeShode(
name: String
): MutableLiveData<StatModel> {
scope.launch {
val request = api.newBimeShode(name)
withContext(Dispatchers.Main) {
try {
val response = request.await()
regBimeshodeLiveData.value = response
} catch (e: HttpException) {
Log.v("this", e.message);
} catch (e: Throwable) {
Log.v("this", e.message);
}
}
}
return regBimeshodeLiveData;
}
it works fine but there is a problem . I think the observer keeps running and if the result's answer is an error and user press fab button again , it creates a new observer and after this , it returns two value , the first value is the first run and the second value is the second run
how can I fix this ? what is the correct way for submitting forms ?
If your problem is because of LiveData, you should use SingleLiveEvent like as follow
// For first article
val _liveData = MutableLiveData<Event<StatModel>>()
// For second article
val _liveData = SingleLiveEvent<StatModel>()
If you do not know SingleLiveEvent, you can find it here and here.
If your problem is because of your ui element, I think the best solution is to disable the submit button after submitting for the first time.
Related
I am using Health Connect to read records, like steps and exercises. I use Health Connect in a few different places in Kotlin, and the code generally looks something like:
suspend fun fetchStepData(
healthConnectClient: HealthConnectClient,
viewModel: StepViewViewModel,
): StepViewViewModel {
kotlin.runCatching {
val todayStart = Instant.now().atZone(ZoneId.systemDefault()).toLocalDate().atStartOfDay();
val response: ReadRecordsResponse<StepsRecord>
try {
response = healthConnectClient.readRecords(
ReadRecordsRequest(
StepsRecord::class,
timeRangeFilter = TimeRangeFilter.after(todayStart)
)
)
var steps: Long = 0;
if (response.records.isNotEmpty()) {
for (stepRecord in response.records) {
steps += stepRecord.count
}
}
return viewModel
} catch (e: Exception) {
Log.e("StepUtil", "Unhandled exception, ", e)
}
}
return viewModel
}
I have an update function that is run when focus changes to ensure that the app is in the foreground.
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
binding.root.invalidate()
val fragment =
supportFragmentManager.findFragmentById(R.id.nav_host_fragment_activity_main)?.childFragmentManager?.primaryNavigationFragment
if (fragment is MyFragment) {
displayLoadingIndicator(true)
runBlocking {
if (fragment.fetchStepData(this#MainActivity.healthConnectClient, fragment.getViewModel()) != null) {
displayLoadingIndicator(false)
}
}
}
I have a loading indicator present when I am fetching the data.
I use a drawer, and if I wait about 15 seconds and press the drawer button corresponding with MyFragment, the application hangs on the loading indicator, never successfully dismissing it.
I've tried stepping through the application in debug mode, and as I do, I always hang on
response = healthConnectClient.readRecords(
ReadRecordsRequest(
StepsRecord::class,
timeRangeFilter = TimeRangeFilter.after(todayStart)
)
)
in fetchStepData. I did at one point have my application making multiple requests for HealthConnectClient.getOrCreate(context), but I have since consolidated them to one instantiation call. I'm thinking I may be reading the data wrong and maybe I need to use getChanges, or maybe I'm being rate limited. Does anyone have any insight? Thanks in advance!
In my case scenario as I use Compose, I add data to the server and read them from it, but after adding the new data and storing it in MutableListState when I go back to the main screen where the list of data is represented, the new data isn't shown as the API call was already done and If I close the app and reopen the new data read normally, so I want the best way to refresh the data from the server after adding. I am thinking about recalling the API again on my screen by using a launchedEffect, but I believe there are better ways than this. something like Firebase real-time database feature.
My Code for example is like this:
Class HomeRepository #Inject constructor(
val service : ApiServieces
) : IHomeRepository {
override suspend fun getHomeData() = service.getHomeData()
override suspend fun getProfileData() = service.getProfileData()
.................
Then I use it in my UseCase
class CheckPhoneUseCase #Inject constructor(private val repository: IRegistrationRepository) {
fun checkPhone(phoneBodyModel: PhoneBodyModel) : Flow<Resource<BaseResponse>> = flow{
emit(Resource.Loading())
try {
val response = repository.checkPhoneNumber(phoneBodyModel).toDomain()
emit(Resource.Success(response))
} catch (e: Exception) {
emit(Resource.Error(e.message ?: "Error Occurred!"))
} catch (e : HttpException) {
if (e.code() == HttpURLConnection.HTTP_NOT_FOUND) {
emit(Resource.Error("Phone Number Not Found!"))
} else {
emit(Resource.Error("Error Occurred!"))
}
} catch (e : IOException){
emit(Resource.Error("Failed to connect to server! check you internet connection!"))
}
}
}
and Use it in the ViewModel and Call the API method in the Composable function
private fun getCampaigns() {
getCampaignsUseCase().onEach{ response ->
when(response){
is Resource.Loading -> {
_isLoadingProgressBar.emit(true)
}
is Resource.Success -> {
_state.value = ResponseState(isSuccess = response.data?.status ?: false)
_campaignsData.value = ResponseCampaigns(isSuccess = response.data?.data)
Log.v("TAG", "getCampaigns: ${_campaignsData.value.isSuccess?.current?.items?.data?.size}")
for (item in response.data?.data?.current?.items?.data!!){
_campaignListCurrent.add(item)
}
_campaignListCurrent.removeAt(0)
for (item in response.data.data.completed.items.data!!){
_campaignListCompleted.add(item)
}
_campaignListCompleted.removeAt(0)
_isLoadingProgressBar.emit(false)
Log.v("TAG", "_campaignListCompleted: ${_campaignListCompleted}")
}
is Resource.Error -> {
_isLoadingProgressBar.emit(false)
Log.v("TAG", "_campaignListCompletedErorr: ${_campaignsData.value.isError}")
Log.v("TAG", "_campaignListCompletedErorrMessage: ${response.message}")
}
}
}.launchIn(viewModelScope)
}
This way I get the data, bat cant refresh it when I add new data from the server unless I close the app and recall the Api method again.
I have a situation where I want the livedata to be observed only once in the app. The problem is that I am working on the authentication for an app using some Node.js backend.
As I am sending the values to receive the response from the backend it's working fine till now. I observe that response and based on that I make changes to my fragment ( that is if the response received is true then move to next fragment, otherwise if it is false show a toast message ).
Now the problem is that :
Case 1: I opened the app, entered the right credentials and pressed the button, received true response from the server and goes to the next fragment.
Case 2: I opened the app, but entered the wrong credentials, I received a false from server and based on that the Toast is shown.
Case 3 (The issue): I opened the app, entered the wrong credentials and then without closing the fragment screen entered the right credentials by editing them, the app crashes and at the same time I receive multiple responses from the server via LiveData.
My observation: Looking more into that I found that the LiveData is attached to the fragment/activity and therefore it shows the last state. So as in case 3 the the last state was receiving the false value from backend it was used again and we were shown the error instead of going to the next screen.
Can anyone guide me how to solve this. Thanks
Some code that might be needed:
binding.btnContinue.setOnClickListener {
val number = binding.etMobileNumber.text.toString().toLong()
Timber.d("Number: $number")
authRiderViewModel.authDriver(number)
checkNumber()
}
Function which checks the number :
private fun checkNumber() {
authRiderViewModel.response.observe(viewLifecycleOwner, Observer {
Timber.d("Response: $it")
if (it!!.success == true) {
val action = LoginFragmentDirections.actionLoginFragmentToOtpFragment()
findNavController().navigate(action)
Timber.d("${it.message}")
} else {
Toast.makeText(requireContext(), "Number not registered", Toast.LENGTH_SHORT).show()
binding.etMobileNumber.setText("")
}
})
}
ViewModel code:
private val _response = MutableLiveData<AuthResponse>()
val response: LiveData<AuthResponse>
get() = _response
fun authDriver(number: Long) = viewModelScope.launch {
Timber.d("Number: $number")
myRepo.authDriver(number).let {
_response.postValue(it)
}
}
P.S I have tried using something called SingleLiveEvent but it doesn't seem to work.
I would create a separate class that tracks the UI state you need and update it when the state is consumed. Something like the following. I don't really know what the parameter is for authDriver, so this is a more generic example.
sealed interface AuthState {
object NotYetRequested: AuthState
object AwaitingResponse: AuthState
class ResponseReceived(val response: AuthResponse): AuthState {
var isHandled = false
private set
fun markHandled() {
isHandled = true
}
}
}
// In ViewModel:
private val _authState = MutableLiveData<AuthState>().also {
it.value = AuthState.NotYetRequested
}
val authState: LiveData<AuthState> get() = _authState
fun requestAuthentication() = viewModelScope.launch {
_authState.value = AuthState.AwaitingResponse
val response = myRepo.authenticate()
_authState.value = AuthState.ResponseReceived(response)
}
// In Fragment:
viewModel.authState.observe(viewLifecycleOwner) { authState ->
when (authState) {
AuthState.NotYetRequested -> ShowUiRequestingAuthentication()
AuthStateAwaitingResponse -> ShowIndeterminateProgressUi()
is AuthStateResponseReceived -> when {
authState.isHandled -> {} // do nothing? depends on your setup, might need to navigate to next screen if handled response is successful
authState.response.isSuccessful -> {
goToNextScreen()
authState.markHandled()
}
else -> {
showErrorToast()
ShowUiRequestingAuthentication()
authState.markHandled()
}
}
}
}
A have a screen where I display 10 users. Each user is represented by a document in Firestore. On user click, I need to get its details. This is what I have tried:
fun getUserDetails(uid: String) {
LaunchedEffect(uid) {
viewModel.getUser(uid)
}
when(val userResult = viewModel.userResult) {
is Result.Loading -> CircularProgressIndicator()
is Result.Success -> Log.d("TAG", "You requested ${userResult.data.name}")
is Result.Failure -> Log.d("TAG", userResult.e.message)
}
}
Inside the ViewModel class, I have this code:
var userResult by mutableStateOf<Result<User>>(Result.Loading)
private set
fun getUser(uid: String) = viewModelScope.launch {
repo.getUser(uid).collect { result ->
userResult = result
}
}
As you see, I use Result.Loading as a default value, because the document is heavy, and it takes time to download it. So I decided to display a progress bar. Inside the repo class I do:
override fun getUser(uid: String) = flow {
try {
emit(Result.Loading)
val user = usersRef.document(uid).get().await().toObject(User::class.java)
emit(Result.Success(user))
} catch (e: Exception) {
emit(Result.Failure(e))
}
}
I have two questions, if I may.
Is there something wrong with this code? As it works fine when I compile.
I saw some questions here, that recommend using collectAsState() or .collectAsStateWithLifecycle(). I tried changing userResult.collectAsState() but I cannot find that function. Is there any benefit in using collectAsState() or .collectAsStateWithLifecycle() than in my actual code? I'm really confused.
If you wish to follow Uncle Bob's clean architecture you can split your architecture into Data, Domain and Presentation layers.
For android image below shows how that onion shape can be simplified to
You emit your result from Repository and handle states or change data, if you Domain Driven Model, you store DTOs for data from REST api, if you have db you keep database classes instead of passing classes annotated with REST api annotation or db annotation to UI you pass a UI.
In repository you can pass data as
override fun getUser(uid: String) = flow {
val user usersRef.document(uid).get().await().toObject(User::class.java)
emit(user)
}
In UseCase you check if this returns error, or your User and then convert this to a Result or a class that returns error or success here. You can also change User data do Address for instance if your business logic requires you to return an address.
If you apply business logic inside UseCase you can unit test what you should return if you retrieve data successfully or in case error or any data manipulation happens without error without using anything related to Android. You can just take this java/kotlin class and unit test anywhere not only in Android studio.
In ViewModel after getting a Flow< Result<User>> you can pass this to Composable UI.
Since Compose requires a State to trigger recomposition you can convert your Flow with collectAsState to State and trigger recomposition with required data.
CollectAsState is nothing other than Composable function produceState
#Composable
fun <T : R, R> Flow<T>.collectAsState(
initial: R,
context: CoroutineContext = EmptyCoroutineContext
): State<R> = produceState(initial, this, context) {
if (context == EmptyCoroutineContext) {
collect { value = it }
} else withContext(context) {
collect { value = it }
}
}
And produceState
#Composable
fun <T> produceState(
initialValue: T,
key1: Any?,
key2: Any?,
#BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
val result = remember { mutableStateOf(initialValue) }
LaunchedEffect(key1, key2) {
ProduceStateScopeImpl(result, coroutineContext).producer()
}
return result
}
As per discussion in comments, you can try this approach:
// Repository
suspend fun getUser(uid: String): Result<User> {
return try {
val user = usersRef.document(uid).get().await().toObject(User::class.java)
Result.Success(user)
} catch (e: Exception) {
Result.Failure(e)
}
}
// ViewModel
var userResult by mutableStateOf<Result<User>?>(null)
private set
fun getUser(uid: String) {
viewModelScope.launch {
userResult = Result.Loading // set initial Loading state
userResult = repository.getUser(uid) // update the state again on receiving the response
}
}
In an android application, i'm trying to do a firebase call that fills an ArrayAdapter in order to show a list of ships.
When i'm using a local ArrayList, it works, but my firebase call doesn't work properly.
Because that firebase call is asynchronous, android shows me the application before ending the firebase call, so my ArrayAdapter is empty and my layout is empty too.
I tryed to use a Coroutine method i've seen online but i doesn't seem to work.
Can someone help me ?
Here is my source code :
MainActivity :
override fun onCreate(savedInstanceState: Bundle?){
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
var containerShips : List<Containership> = listOf()
val db = Database()
runBlocking {
containerShips = db.getAllContainerships()
}
val arrayAdapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, containerShips)
val listShipDetails = findViewById<ListView>(R.id.listShipDetails)
listShipDetails.adapter = arrayAdapter
}
Database:
suspend fun getAllContainerships() : List<Containership> {
val list : MutableList<Containership> = mutableListOf()
val job = GlobalScope.launch {
db.collection("Containership").get().addOnSuccessListener { result ->
for (containership in result) {
list.add(containership.toObject(Containership::class.java))
println(containership.toObject(Containership::class.java))
}
}
}
job.join()
return list
}
Thanks for your help !
You can use Tasks.await to get this done.
private fun getAllContainerships(): List<Containership> {
return try {
val taskResult = Tasks.await(db.collection("Containership").get(), 2, TimeUnit.SECONDS)
taskResult.mapTo(ArrayList()) { containership.toObject(Containership::class.java) }, null)
} catch (e: ExecutionException) {
//TODO handle exception
} catch (e: InterruptedException) {
//TODO handle exception
} catch (e: TimeoutException) {
//TODO handle exception
}
}
Make sure this is done in a background thread. Also, try to get rid of your GlobalScope.async as it is a code smell (to do this, you can use the MVVM pattern, put your data access code in a viewmodel and use viewModelScope from AndroidX lifecycle).