I'm newbie with RxJava2.
I have next code:
fun signIn(): Completable = getCredentials() // get saved token
.onErrorResumeNext { makeLockSignInRequest() } // if token not saved then get it
.flatMap { refreshToken(it) } // refresh token
.doOnSuccess { credentialsManager.saveCredentials(it) } // save updated token
.doFinally { lock?.onDestroy(context) }!!
.toCompletable()
private fun getCredentials() = Single.create(SingleOnSubscribe<Credentials> {
credentialsManager.getCredentials(object : BaseCallback<Credentials, CredentialsManagerException> {
override fun onSuccess(payload: Credentials?) = it.onSuccess(payload!!)
override fun onFailure(error: CredentialsManagerException?) = it.onError(error!!)
})
})
private fun makeLockSignInRequest() = Single.create(SingleOnSubscribe<Credentials> {
lock = Lock.newBuilder(auth0, object : AuthenticationCallback() {
override fun onAuthentication(credentials: Credentials?) = it.onSuccess(credentials!!)
override fun onCanceled() { }
override fun onError(error: LockException?) = it.onError(error!!)
})
.withScheme("demo")
.withScope("email openid offline_access")
.withAudience(ApiServiceProvider.DOMAIN + "/api/")
.closable(true)
.build(context)
context.startActivity(lock!!.newIntent(context))
})
private fun refreshToken(storedCredentials: Credentials) = Single.create(SingleOnSubscribe<Credentials> {
apiClient.renewAuth(storedCredentials.refreshToken!!)
.addParameter("scope", "openid email offline_access")
.start(object : BaseCallback<Credentials, AuthenticationException> {
override fun onSuccess(receivedCredentials: Credentials?) {
val newCredentials = Credentials(receivedCredentials!!.idToken, receivedCredentials.accessToken, receivedCredentials.type, storedCredentials.refreshToken, receivedCredentials.expiresAt, receivedCredentials.scope)
it.onSuccess(newCredentials)
}
override fun onFailure(error: AuthenticationException?) {
it.onError(Exception("Error refresh token: ${error!!.description!!}"))
}
})
})
This code gets saved token and refresh it.
Also if user just logged in it refresh token.
I want to add filter like follows:
fun signIn(): Completable = getCredentials()
.onErrorResumeNext { makeLockSignInRequest() }
.filter { OffsetDateTime.now(ZoneOffset.UTC).toEpochSecond() > it.expiresAt!!.time } // if token alive then do nothing
.flatMapSingle { refreshToken(it) }
.doOnSuccess { credentialsManager.saveCredentials(it) }
.doFinally { lock?.onDestroy(context) }!!
.toCompletable()
This code will fail with error: NoSuchElementException
So how can I filter token?
.filter changes your Single to Maybe. If there is no item in Maybe (because filter requirements are not met) after transforming it with flatMapSingle your code will return error with NoSuchElementException exception.
What I would do with it is:
fun signIn(): Completable = getCredentials()
.onErrorResumeNext { makeLockSignInRequest() }
.filter { OffsetDateTime.now(ZoneOffset.UTC).toEpochSecond() > it.expiresAt!!.time } // if token alive then do nothing
.flatMapCompletable { refreshToken(it).doAfterSuccess{credentialsManager.saveCredentials(it)}.toCompletable() }
.doFinally { lock?.onDestroy(context) }!!
Related
I'm new to Android development and trying to understand Coroutines and LiveData from various example projects. I have currently setup a function to call my api when the user has input a username and password. However after 1 button press, the app seems to jam and I can't make another api call as if its stuck on a pending process.
This is my first android app made with a mash of ideas so please let me know where I've made mistakes!
Activity:
binding.bLogin.setOnClickListener {
val username = binding.etUsername.text.toString()
val password = binding.etPassword.text.toString()
viewModel.userClicked(username, password).observe(this, Observer {
it?.let { resource ->
when (resource.status) {
Status.SUCCESS -> {
print(resource.data)
}
Status.ERROR -> {
print(resource.message)
}
Status.LOADING -> {
// loader stuff
}
}
}
})
}
ViewModel:
fun userClicked(username: String, password: String) = liveData(dispatcherIO) {
viewModelScope.launch {
emit(Resource.loading(data = null))
try {
userRepository.login(username, password).apply {
emit(Resource.success(null))
}
} catch (exception: Exception) {
emit(Resource.error(exception.message ?: "Error Occurred!", data = null))
}
}
}
Repository:
#WorkerThread
suspend fun login(
username: String,
password: String
): Flow<Resource<String?>> {
return flow {
emit(Resource.loading(null))
api.login(LoginRequest(username, password)).apply {
this.onSuccessSuspend {
data?.let {
prefs.apiToken = it.key
emit(Resource.success(null))
}
}
}.onErrorSuspend {
emit(Resource.error(message(), null))
}.onExceptionSuspend {
emit(Resource.error(message(), null))
}
}.flowOn(dispatcherIO)
}
API:
suspend fun login(#Body request: LoginRequest): ApiResponse<Auth>
You don't need to launch a coroutine in liveData builder, it is already suspend so you can call suspend functions there:
fun userClicked(username: String, password: String) = liveData(dispatcherIO) {
emit(Resource.loading(data = null))
try {
userRepository.login(username, password).apply {
emit(Resource.success(null))
}
} catch (exception: Exception) {
emit(Resource.error(exception.message ?: "Error Occurred!", data = null))
}
}
If you want to use LiveDate with Flow you can convert Flow to LiveData object using asLiveData function:
fun userClicked(username: String, password: String): LiveData<Resource<String?>> {
return userRepository.login(username, password).asLiveData()
}
But I wouldn't recommend to mix up LiveData and Flow streams in the project. I suggest to use only Flow.
Using only Flow:
// In ViewModel:
fun userClicked(username: String, password: String): Flow<Resource<String?>> {
return userRepository.login(username, password)
}
// Activity
binding.bLogin.setOnClickListener {
val username = binding.etUsername.text.toString()
val password = binding.etPassword.text.toString()
lifecycleScope.launch {
viewModel.userClicked(username, password).collect { resource ->
when (resource.status) {
Status.SUCCESS -> {
print(resource.data)
}
Status.ERROR -> {
print(resource.message)
}
Status.LOADING -> {
// loader stuff
}
}
}
}
}
Remove suspend keyword from the login function in Repository.
lifecycleScope docs.
here is my code, I am using it for logging in user with google,
This is my viewModel code
fun signInWithGoogle(account: GoogleSignInAccount): LiveData<Resource<Any>> {
val credential = GoogleAuthProvider.getCredential(account.idToken, null)
return liveData (IO){
authRepo.firebaseSignInWithGoogle(credential, object : FetchUser {
override suspend fun onUserDataFetch(user: User) {
this#liveData.emit(Resource.success(user))
}
override suspend fun onError(error: AppError?) {
this#liveData.emit(Resource.error(error, null))
}
})
}
}
This is my code authRepository where i am logging the user in and checking if user already exits in database or not according to that performing the work
suspend fun firebaseSignInWithGoogle(googleAuthCredential: AuthCredential, userCallBack: FetchUser) {
coroutineScope {
firebaseAuth.signInWithCredential(googleAuthCredential).await()
createUpdateUser(userCallBack)
}
}
private suspend fun createUpdateUser(userCallBack: FetchUser) {
val firebaseUser = firebaseAuth.currentUser
if (firebaseUser != null) {
userIntegrator.getUserById(firebaseUser.uid, object : OnDataChanged {
override suspend fun onChanged(any: Any?) {
if (any != null && any is User) {
any.isNew = false
userIntegrator.createUpdateUser(any, userCallBack)
} else {
val user = User()
user.id = firebaseUser.uid
user.name = firebaseUser.displayName
user.email = firebaseUser.email
user.isNew = true
userIntegrator.createUpdateUser(
user,
userCallBack
)
}
}
})
}
}
This is my last class where I am updating the user in database
suspend fun createUpdateUser(user: User, userCallBack: FetchUser) {
if (user.id.isNullOrEmpty()) {
userCallBack.onError(AppError(StatusCode.UnSuccess, ""))
return
}
val dp = databaseHelper.dataFirestoreReference?.collection(DatabaseHelper.USERS)?.document()
dp?.apply {
dp.set(user.toMap()).await().apply {
dp.get().await().toObject(User::class.java)?.let {
userCallBack.onUserDataFetch(it)
}?: kotlin.run {
userCallBack.onError(AppError(StatusCode.Exception,"Unable to add user at the moment"))
}
}
}
}
Now here whole thing is that, I am using a FetchUser interface which look like this
interface FetchUser {
suspend fun onUserDataFetch(user: User)
suspend fun onError(error: AppError?)
}
I just want to get rid of it and looking for something else in coroutines.
Also I just wanted to know the best practice here,
What should I do with it.
Also I want to make it unit testable
There are 2 ways, if you want to call and get result directly, you could use suspendCoroutine. Otherway, if you want to get stream of data like, loading, result, error,... you could try callbackFlow
Exp:
suspend fun yourMethod() = suspendCoroutine { cont ->
// do something
cont.resume(result)
}
suspend fun yourMethod() = callbackFlow {
val callbackImpl = object: yourInterace {
// your implementation
fun onSuccess() {
emit(your result)
}
fun onFailed() {
emit(error)
}
}
handleYourcallback(callbackImpl)
}
I use FirebaseAuth for registration new user
class FirebaseAuthenticationServiceImpl(): FirebaseAuthenticationService {
override fun registerWithEmailAndPassword(email: String, password: String): Boolean {
val registration = FirebaseAuth.getInstance().createUserWithEmailAndPassword(email, password)
.addOnSuccessListener {
println(it.additionalUserInfo?.providerId.toString())
}.addOnFailureListener {
println(it.message.toString())
}
return registration.isSuccessful
}
}
I call function above and every time I get false. After some time I get true
coroutineScope {
try {
if (firebaseService.registerWithEmailAndPassword(email, password)) {
openHomeActivity.offer(Unit)
} else {}
} catch (e: Exception) {}
}
How can I wait for uth result (success/failure) and afer that get that value?
Where is FirebaseAuthenticationService from? Do you need it? The official getting started guide just uses Firebase.auth. With this, you can authenticate using the await() suspend function instead of using the callback approach.
// In a coroutine:
val authResult = Firebase.auth.registerWithEmailAndPassword(email, password).await()
val user: FirebaseUser = authResult.user
if (user != null) {
openHomeActivity.offer(Unit)
} else {
// authentication failed
}
If you are using coroutines you can use suspendCoroutine which is perfect bridge between traditional callbacks and coroutines as it gives you access to the Continuation<T> object, example with a convenience extension function for Task<R> objects :
scope.launch {
val registrationResult = suspendCoroutine { cont -> cont.suspendTask(FirebaseAuth.getInstance().createUserWithEmailAndPassword(email, password) }
}
private fun <R> Continuation<R>.suspendTask(task: Task<R>) {
task.addOnSuccessListener { this.success(it) }
.addOnFailureListener { this.failure(it) }
}
private fun <R> Continuation<R>.success(r : R) = resume(r)
private fun <R> Continuation<R>.failure(t : Exception) = resumeWithException(t)
I am trying to write an Espresso test while I am using Paging library v2 and RxJava :
class PageKeyedItemDataSource<T>(
private val schedulerProvider: BaseSchedulerProvider,
private val compositeDisposable: CompositeDisposable,
private val context : Context
) : PageKeyedDataSource<Int, Character>() {
private var isNext = true
private val isNetworkAvailable: Observable<Boolean> =
Observable.fromCallable { context.isNetworkAvailable() }
override fun fetchItems(page: Int): Observable<PeopleWrapper> =
wrapEspressoIdlingResource {
composeObservable { useCase(query, page) }
}
override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Character>) {
if (isNext) {
_networkState.postValue(NetworkState.LOADING)
isNetworkAvailable.flatMap { fetchItems(it, params.key) }
.subscribe({
_networkState.postValue(NetworkState.LOADED)
//clear retry since last request succeeded
retry = null
if (it.next == null) {
isNext = false
}
callback.onResult(it.wrapper, params.key + 1)
}) {
retry = {
loadAfter(params, callback)
}
initError(it)
}.also { compositeDisposable.add(it) }
}
}
override fun loadInitial(
params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Character>,
) {
_networkState.postValue(NetworkState.LOADING)
isNetworkAvailable.flatMap { fetchItems(it, 1) }
.subscribe({
_networkState.postValue(NetworkState.LOADED)
if (it.next == null) {
isNext = false
}
callback.onResult(it.wrapper, null, 2)
}) {
retry = {
loadInitial(params, callback)
}
initError(it)
}.also { compositeDisposable.add(it) }
}
}
Here is my wrapEspressoIdlingResource :
inline fun <T> wrapEspressoIdlingResource(task: () -> Observable<T>): Observable<T> = task()
.doOnSubscribe { EspressoIdlingResource.increment() } // App is busy until further notice
.doFinally { EspressoIdlingResource.decrement() } // Set app as idle.
But it does not wait until data delivered from network. When I Thread.Sleep before data delivered, Espresso test will be passed, so it is related to my Idling Resource setup.
I believe it could be related to Paging library, since this method works perfectly fine for Observable types when I use them in other samples without Paging library.
Full source code is available at : https://github.com/AliRezaeiii/StarWarsSearch-Paging
What am I missing?
I needed to override the fetchDispatcher on the builder :
class BasePageKeyRepository<T>(
private val scheduler: BaseSchedulerProvider,
) : PageKeyRepository<T> {
#MainThread
override fun getItems(): Listing<T> {
val sourceFactory = getSourceFactory()
val rxPagedList = RxPagedListBuilder(sourceFactory, PAGE_SIZE)
.setFetchScheduler(scheduler.io()).buildObservable()
...
}
}
I am able to authenticate user by Firebase authentication but could not able to store data in Firebase database. I am not getting any error in logcat. I haven't changed any rules in Firebase database. My rules are set default. Here is my code:
fun authenticateUser(){
Observable.create(ObservableOnSubscribe<Task<AuthResult>>{
e: ObservableEmitter<Task<AuthResult>> -> run {
try {
firebaseAuth.createUserWithEmailAndPassword(email!!, password!!)
.addOnCompleteListener(this, object : OnCompleteListener<AuthResult> {
override fun onComplete(p0: Task<AuthResult>) {
e.onNext(p0)
e.onComplete()
}
})
}
catch (ex : Exception){
dialogs?.dismiss()
displayMessage("network problem..")
e.onError(ex)
}
}
}).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(object : Observer<Task<AuthResult>>{
override fun onError(e: Throwable) {
e.printStackTrace()
}
override fun onNext(t: Task<AuthResult>) {
if (t.isSuccessful) {
storeData()
}
else if (!t.isSuccessful){
dialogs?.dismiss()
displayMessage("some issues has came..")
}
}
override fun onComplete() {
}
override fun onSubscribe(d: Disposable) {
compositeDisposable?.add(d)
}
})
}
override fun storeData() {
var currentUser = firebaseAuth.currentUser
val databaseReference = firebaseDatabase.reference.child("Users_Information").child(currentUser?.uid)
val data = HashMap<String,String>()
data.put("name",nameString!!)
data.put("email_address",emailString!!)
data.put("image","def")
data.put("thumb_image","def")
databaseReference.setValue(data).addOnCompleteListener {
task -> if (task.isSuccessful){
dialogs?.dismiss()
displayMessage("user is created")
goToNextActivity()
}
else if (!task.isSuccessful){
dialogs?.dismiss()
displayMessage("Authentication failed, try again later")
}
else if (task.isComplete){
displayMessage("data not stored")
}
}
}
override fun goToNextActivity() {
var intents = Intent(this,MainActivity::class.java)
intents.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
intents.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK)
startActivity(intents)
finish()
}
Uid is generating but data is not storing in Firebase database.I debug the code but didn't find any issue. Where is the problem, please help...
Try to send Firebaseuser to storeData() and try to log Firebaseuser in storeData()
override fun onNext(t: Task<AuthResult>) {
if (t.isSuccessful) {
Log.d("FB user", t.getResult().getUser())
storeData(t.getResult().getUser())
}
else if (!t.isSuccessful){
dialogs?.dismiss()
displayMessage("some issues has came..")
}
}
....
..
override fun storeData(curentUser: Firebaseuser) {
Log.d("FB user", currentUser?.uid)
val databaseReference = firebaseDatabase.reference.child("Users_Information").child(currentUser?.uid)
...
..
}