How to block a suspend function that is calling firebase - kotlin

In my AuthRepository I'm trying to make a login function that will run asynchronously in the main thread, I'm using firebase auth to do so and a sealed class AuthResult to handle the result state.
My problem is that when I try to return the AuthResult state it will be null because I return the var authResult: AuthResult = null variable before the .addOnCompleteListener is finished...
Here is my function:
override suspend fun login(email: String, password: String): AuthResult {
return try {
var authResult: AuthResult? = null
firebaseAuth.signInWithEmailAndPassword(email, password)
.addOnCompleteListener { task ->
if (task.isSuccessful) {
task.result.user?.let {
authResult = AuthResult.Success(it)
}
} else {
// exception
authResult = AuthResult.Error(message = "An error occurred", task.exception)
}
}
authResult ?: AuthResult.Error(message = "An error occurred null")
} catch (e: Exception) {
AuthResult.Error("An error has occurred", e)
}
}
I call this function using invoke usecase in my LoginViewModel:
private val _state = MutableStateFlow<AuthResult?>(null)
val state: StateFlow<AuthResult?> = _state
fun login(email: String, password: String) {
viewModelScope.launch(Dispatchers.IO) {
_state.value = loginUseCase(email, password)
}
}
so considering my problem state will be:
AuthResult.Error(message = "An error occurred null")
what im trying to accomplish is blocking the suspend fun login(...) until .addOnCompleteListener{} is finished...

Was missing a dependency so I couldn't use await():
add this:
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.6.0'
then add .await() after .addOnCompleteListener {}:
firebaseAuth.signInWithEmailAndPassword(email, password)
.addOnCompleteListener { task ->
...
}.await()

Related

What is the best way to get data from an API using retrofit when something needs to wait on that data?

I have a retrofit API call but am having trouble getting the data out of it:
This is in a class file that's not a viewModel or Fragment. It's called from the apps main activity view model. I need to be able to get the data from the API and wait for some processing to be done on it before returning the value back the view model. Newer to kotlin and struggling with all the watchers and async functions. The result of this an empty string is the app crashes, because it's trying to access data before it has a value.
From class getData which is not a fragment
private lateinit var data: Data
fun sync(refresh: Boolean = false): List<String> {
var info = emptyList<String>
try {
getData(::processData, ::onFailure)
info = data.info
} catch(e: Throwable){
throw Exception("failed to get data")
}
}
}
return info
}
fun getData(
onSuccess: KFunction1<ApiResponse>?, Unit>,
onFailed: KFunction1<Throwable, Unit>
) {
val client = ApiClient().create(Service.RequestData)
val request = client.getData()
request.enqueue(object : Callback<ApiResponse> {
override fun onResponse(
call: Call<ApiResponse>,
response: Response<ApiResponse>
) {
onSuccess(response.body())
}
override fun onFailure(call: Call<RegistryResponse<GlobalLanguagePack>>, t: Throwable) {
onFailed(Exception("failed to get data"))
}
})
}
private fun processData(body: ApiResponse?) {
requireNotNull(body)
data = body.data
}
```
From appViewModel.kt:
```
fun setUpStuff(context: Context, resources: AppResources) = viewModelScope.launch {
val stuff = try {
getData.sync()
} catch (e: Exception) {
return#launch
}
if (stuff.isEmpty()) return#launch
}
```

How to catch an error in Jetpack compose?

I tried to catch application errors in this way:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
try {
MyApp()
}
catch (t: Throwable) {
Text(t.message ?: "Error", fontSize = 24.sp)
Text(t.stackTraceToString(), fontFamily = FontFamily.Monospace)
}
}
}
}
But it does not compile:
Try catch is not supported around composable function invocations.
WTF?
What else should I use?
#Composable
fun MyScreen() {
val data = try {
loadData()
} catch (error: Throwable) {
// Handle the error here
null
}
if (data != null) {
// Render the data
} else {
// Render an error message or retry button
}
}
To use the catch operator, you can call it on a Composable function and pass in a lambda function to handle the error. The lambda function should take a single parameter of type Throwable, which represents the error that occurred. Inside the lambda function, you can perform any necessary error handling, such as displaying an error message or retrying the failed operation.

Kotlin Type mismatch: inferred type is String but Unit was expected

Im trying to get the return result to return a string, I believe this might be a noob question but I'm very new to kotlin so please don't be harsh. All solutions welcome!
private val client = OkHttpClient()
fun getRequest(id: String) {
val url = "http://localhost:8000/api/v1/discord?discordid=$id"
val request = Request.Builder()
.url(url)
.header("User-Agent", "OkHttp Bot")
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
println("Error: ${e.message}")
}
override fun onResponse(call: Call, response: Response) {
response.use {
if (!response.isSuccessful) throw IOException("Unexpected code $response")
var result = response.body!!.string()
return result
}
}
})
}
onResponse function has no return type. If you trying to return some value it will throw some error.
So if you want do somthing with the response, you can create a method and pass response body as paramter and do anything with that.
override fun onResponse(call: Call, response: Response) {
response.use {
if (!response.isSuccessful) throw IOException("Unexpected code $response")
var result = response.body?.string()?:""
someMethod(result)
}
}
fun someMethod(response:String){
//Update UI here
}

How i can use Flow on swift Kotlin multiplatform?

I am creating my first kotlin multiplataform project, and i'm having some difficulty to use the kotlin flow on swift. I created the models, server data requests as common file using kotlin flow and ktor, the the view model and ui layers i am creating as native. So, i have no experience with swift development, and beyond that i' having a lot of trouble to use flow on swift view model. Searching for an answer to my problem i found a class described as CommonFlow, which aims to be used as a common code for both languages(kotlin, swift, but i'm having an error that gives me little or no clue as to why it happens or, probably, it's just my lack of dominion with xcode and swift programming:
So this is the code part that xcode points the error:
Obs: sorry about the image i thought that maybe this time it would be more descriptive
And this its all i got from the error
Thread 1: EXC_BAD_ACCESS (code=2, address=0x7ffee5928ff8)
MY Ios ViewModel:
class ProfileViewModel: ObservableObject {
private let repository: ProfileRepository
init(repository: ProfileRepository) {
self.repository = repository
}
#Published var authentication: Authetication = Authetication.unauthenticated(false)
#Published var TokenResponse: ResponseDTO<Token>? = nil
#Published var loading: Bool = false
func authenticate(email: String, password: String) {
DispatchQueue.main.async {
if(self.isValidForm(email: email, password: password)){
self.repository.getTokenCFlow(email: email, password: password).watch{ response in
switch response?.status {
case StatusDTO.success:
self.loading = false
let token: String = response!.data!.accessToken!
SecretStorage().saveToken(token: token)
self.authentication = Authetication.authenticated
break;
case StatusDTO.loading:
self.loading = true
break;
case StatusDTO.error:
print("Ninja request error \(String(describing: response!.error!))}")
break;
default:
break
}
}
}
}
}
private func isValidForm(email: String, password: String) -> Bool {
var invalidFields = [Pair]()
if(!isValidEmail(email)){
invalidFields.append(Pair(first:"email invalido",second: "email invalido"))
}
if(password.isEmpty) {
invalidFields.append(Pair(first:"senha invalida",second: "senha invalida"))
}
if(!invalidFields.isEmpty){
self.authentication = Authetication.invalidAuthentication(invalidFields)
return false
}
return true
}
private func isValidEmail(_ email: String) -> Bool {
let emailRegEx = "[A-Z0-9a-z._%+-]+#[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}"
let emailPred = NSPredicate(format:"SELF MATCHES %#", emailRegEx)
return emailPred.evaluate(with: email)
}
}
class Pair {
let first: String
let second: String
init(first:String, second: String) {
self.first = first
self.second = second
}
}
enum Authetication {
case invalidAuthentication([Pair])
case authenticated
case persistentAuthentication
case unauthenticated(Bool)
case authenticationFailed(String)
}
The repository methods:
override fun getToken(email: String, password: String): Flow<ResponseDTO<Token>> = flow {
emit(ResponseDTO.loading<Token>())
try {
val result = api.getToken(GetTokenBody(email, password))
emit(ResponseDTO.success(result))
} catch (e: Exception) {
emit(ResponseDTO.error<Token>(e))
}
}
#InternalCoroutinesApi
override fun getTokenCFlow(email: String, password: String): CFlow<ResponseDTO<Token>> {
return wrapSwift(getToken(email, password))
}
The Class CFLOW:
#InternalCoroutinesApi
class CFlow<T>(private val origin: Flow<T>): Flow<T> by origin {
fun watch(block: (T) -> Unit): Closeable {
val job = Job()
onEach {
block(it)
}.launchIn(CoroutineScope(Dispatchers.Main + job))
return object: Closeable {
override fun close() {
job.cancel()
}
}
}
}
#FlowPreview
#ExperimentalCoroutinesApi
#InternalCoroutinesApi
fun <T> ConflatedBroadcastChannel<T>.wrap(): CFlow<T> = CFlow(asFlow())
#InternalCoroutinesApi
fun <T> Flow<T>.wrap(): CFlow<T> = CFlow(this)
#InternalCoroutinesApi
fun <T> wrapSwift(flow: Flow<T>): CFlow<T> = CFlow(flow)
There is an example of using flows in KampKit
https://github.com/touchlab/KaMPKit
I will paste an excerpt from NativeViewModel (iOS)
class NativeViewModel(
private val onLoading: () -> Unit,
private val onSuccess: (ItemDataSummary) -> Unit,
private val onError: (String) -> Unit,
private val onEmpty: () -> Unit
) : KoinComponent {
private val log: Kermit by inject { parametersOf("BreedModel") }
private val scope = MainScope(Dispatchers.Main, log)
private val breedModel: BreedModel = BreedModel()
private val _breedStateFlow: MutableStateFlow<DataState<ItemDataSummary>> = MutableStateFlow(
DataState.Loading
)
init {
ensureNeverFrozen()
observeBreeds()
}
#OptIn(FlowPreview::class)
fun observeBreeds() {
scope.launch {
log.v { "getBreeds: Collecting Things" }
flowOf(
breedModel.refreshBreedsIfStale(true),
breedModel.getBreedsFromCache()
).flattenMerge().collect { dataState ->
_breedStateFlow.value = dataState
}
}
This ViewModel is consumed in swift like this:
lazy var adapter: NativeViewModel = NativeViewModel(
onLoading: { /* Loading spinner is shown automatically on iOS */
[weak self] in
guard let self = self else { return }
if (!(self.refreshControl.isRefreshing)) {
self.refreshControl.beginRefreshing()
}
},
onSuccess: {
[weak self] summary in self?.viewUpdateSuccess(for: summary)
self?.refreshControl.endRefreshing()
},
onError: { [weak self] error in self?.errorUpdate(for: error)
self?.refreshControl.endRefreshing()
},
onEmpty: { /* Show "No doggos found!" message */
[weak self] in self?.refreshControl.endRefreshing()
}
)
In short, the flow is kept wrapped in kotlin mp land and leveraged in iOS by using traditional callback interfaces.

Kotlin coroutine immediately give an exception if last operation finished with exception

When I was try to login on my service via retrofit. When my service is off, 10 seconds after clicking the button I got an SocketTimeoutException exception.
So far everything is normal but again, I clicked the button again after the error gave the same error immediately. What's wrong?
interface LoginService {
#FormUrlEncoded
#POST("/login")
fun login(#Field("id") id: String, #Field("pw") pw: String): Deferred<Response<User>>
}
class LoginViewModel : ViewModel() {
private var job: Job = Job()
private val scope: CoroutineScope = CoroutineScope(Dispatchers.Main + job)
private val service by lazy { RetrofitApiFactory().create(LoginService::class.java) }
private val excHandler = CoroutineExceptionHandler { _, throwable ->
Timber.e(throwable);
}
fun doLogin(id: String, pw: String) {
scope.launch(excHandler) {
val response = service.login(id, pw).await()
if (response.isSuccessful) {
response.body()
?.let { user -> doOnSuccess(user) }
?: doOnError(InvalidUserException())
} else doOnError(Exception())
}
}
private fun CoroutineScope.doOnError(e: Throwable) {
excHandler.handleException(coroutineContext, e)
}
private fun doOnSuccess(user: User) {
...
}
override fun onCleared() {
job.cancel()
}
}
You need to change your CoroutineScope to not re-use the same Job. It's already considered as failed, so it will not even begin executing.
See related issue on github.