Implement FirebaseUI AuthUI with Hilt and MVVM - android

So a few days into learning Kotlin, Android, and MVVM and I'm a little confused on how I'm supposed to handle the authentication flow with com.firebaseui:firebase-ui-auth:8.0.1
At the moment I have a AuthRepository which with my understanding should be responsible for calling to Firebase. So I have the following code:
class AuthRepositoryImpl(
private val auth: FirebaseAuth,
private val authUI: AuthUI,
) : AuthRepository {
override fun loginUser(result: (UiState<String>) -> Unit) {
val providers = arrayListOf(
AuthUI.IdpConfig.GoogleBuilder().build(),
)
// This is wrong, but what to do instead?
val signInIntent = authUI.createSignInIntentBuilder()
.setAvailableProviders(providers)
.build()
}
override fun logout(result: () -> Unit) {
TODO("Not yet implemented")
}
override fun getSession(result: (User?) -> Unit) {
val currentUser = auth.currentUser
if (currentUser == null) {
result.invoke(null)
} else {
result.invoke(User(currentUser.uid, currentUser.email))
}
}
}
I also have an AuthViewModel which should make use of the AuthRepository to trigger the login flow
#HiltViewModel
class AuthViewModel #Inject constructor(
private val repository: AuthRepository
) : ViewModel() {
private val _login = MutableLiveData<UiState<String>>()
val login: LiveData<UiState<String>> get() = _login
fun login() {
_login.value = UiState.Loading
repository.loginUser {
_login.value = it
}
}
fun getSession(result: (User?) -> Unit){
repository.getSession(result)
}
}
Finally I kind of have a LoginFragment with a mix of how I've seen it done in the docs and me trying to implement MVVM the best I can (I think I've got it correct around fetching already logged in users at least)
class LoginFragment : Fragment() {
private lateinit var binding: FragmentLoginBinding
private val viewModel: AuthViewModel by viewModels()
// This has to be either in a fragment or activity correct?
private val signInLauncher = registerForActivityResult(
FirebaseAuthUIActivityResultContract()
) { res ->
this.onSignInResult(res)
}
private fun onSignInResult(result: FirebaseAuthUIAuthenticationResult) {
val response = result.idpResponse
if (result.resultCode == RESULT_OK) {
// ??
} else {
// ???
}
}
fun observer() {
viewModel.login.observe(viewLifecycleOwner) { state ->
when (state) {
is UiState.Loading -> {}
is UiState.Failure -> {}
is UiState.Success -> {
findNavController().navigate(R.id.action_loginFragment_to_mainListingFragment)
}
}
}
}
override fun onStart() {
super.onStart()
viewModel.getSession { user ->
if (user != null) {
findNavController().navigate(R.id.action_loginFragment_to_mainListingFragment)
}
}
}
}
I'm trying to follow best practices while learning all this, so would really appreciate any code examples, github repositories, or documentation that explain a little on what I'm trying to do.

Related

Observer value cleared when using Dagger hilt

I am new to dagger hilt (DI), And I Implement dagger hilt (DI) in my project. Now I am trying to inject the ViewModel. It works fine. But in the API result, I am using mutableLivedata for updating value from the view model to view with the observer. The observer listens and fetches the value works fine. But I read the observed data value; it cleared the value. I don't know why it happened. Can anyone help me to find this?
Login Fragment
#AndroidEntryPoint
class LoginFragment : BaseFragment<FragmentLoginBinding>(FragmentLoginBinding::inflate) {
private val viewModel by viewModels<LoginViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewActionListeners()
setUpViewModelObserver()
}
private fun setUpViewModelObserver() {
viewModel.users.observe(viewLifecycleOwner) { response ->
AppLog.e("validateResponse", response.toString())
when (response.status) {
Status.SUCCESS -> {
binding.frmLayoutProgress.visible(false)
response.data?.let { users -> AppLog.e("AuthenticationMethod",users.clientResponse.authenticationMethod) }
}
Status.LOADING -> {
binding.frmLayoutProgress.visible(true)
}
Status.ERROR -> {
//Handle Error
binding.frmLayoutProgress.visible(false)
AppLog.e("Error:", response.message.toString())
}
}
}
}
}
If you comment the lineAppLog.e("AuthenticationMethod",users.clientResponse.authenticationMethod) }it return the value if uncomment response returns set to be null.
LoginViewModel
#HiltViewModel
class LoginViewModel #Inject constructor(
private val repository: LoginRepository,
private val networkHelper: NetworkHelper
) : ViewModel() {
val users: MutableLiveData<Resource<ResponseValidateUser>> = MutableLiveData()
fun loadValidateUser(email: String) {
viewModelScope.launch {
users.postValue(Resource.loading(null))
if (networkHelper.isNetworkConnected()) {
repository.getValidateUser(email).let {
if (it.isSuccessful) {
users.postValue(Resource.success(it.body()))
} else users.postValue(Resource.error(it.errorBody().toString(), null))
}
} else users.postValue(Resource.error("No internet connection", null))
}
}
}
LoginRepository
class LoginRepository #Inject constructor(private val apiHelper: AuthApiHelper) {
suspend fun getValidateUser(email: String) = apiHelper.getValidateUser(email)
}
AuthApiHelper
interface AuthApiHelper {
suspend fun getValidateUser(email: String): Response<ResponseValidateUser>
}
ResponseValidateUser
data class ResponseValidateUser(
#SerializedName("clientResponse") val clientResponse: ClientResponse,
#SerializedName("status") val status: String,
#SerializedName("errorMessage") val errorMessage: String
)

StateFlow: Cancellation of Older Emitted State After Collecting

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.

Right place to offer or send Channel in MVI pattern

I load data in recycleView in advance. In order to do that I have following code in onCreate() of Activity :
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setupUI()
setupViewModel()
observeViewModel()
if (savedInstanceState == null) {
mainViewModel.userIntent.offer(MainIntent.FetchUser)
}
}
As you see I offer() when savedInstanceState is null, The problem is when we have process death ( you can simply create it by activating Do not keep activities in developer option), reload of data will not be triggered.
another option is to use it inside init block of ViewModel, but problem is I want to have bellow unit test which I can verify all three states :
#Test
fun givenServerResponse200_whenFetch_shouldReturnSuccess() {
runBlockingTest {
`when`(apiService.getUsers()).thenReturn(emptyList())
val apiHelper = ApiHelperImpl(apiService)
val repository = MainRepository(apiHelper)
val viewModel = MainViewModel(repository)
viewModel.state.asLiveData().observeForever(observer)
viewModel.userIntent.send(MainIntent.FetchUser)
}
verify(observer, times(3)).onChanged(captor.capture())
verify(observer).onChanged(MainState.Idle)
verify(observer).onChanged(MainState.Loading)
verify(observer).onChanged(MainState.Users(emptyList()))
}
If I use the init block option as soon as ViewModel initialized, send or offer will be called while observeForever did not be used for LiveData in the above unit test.
Here is my ViewModel class :
class MainViewModel(
private val repository: MainRepository
) : ViewModel() {
val userIntent = Channel<MainIntent>(Channel.UNLIMITED)
private val _state = MutableStateFlow<MainState>(MainState.Idle)
val state: StateFlow<MainState>
get() = _state
init {
handleIntent()
}
private fun handleIntent() {
viewModelScope.launch {
userIntent.consumeAsFlow().collect {
when (it) {
is MainIntent.FetchUser -> fetchUser()
}
}
}
}
private fun fetchUser() {
viewModelScope.launch {
_state.value = MainState.Loading
_state.value = try {
MainState.Users(repository.getUsers())
} catch (e: Exception) {
MainState.Error(e.localizedMessage)
}
}
}
}
What could be the solution for the above scenarios?
The only solution that I found is moving fetchUser method and another _state as MutableStateFlow to Repository layer and observeForever it in Repository for local unit test, as a result I can send or offer userIntent in init block off ViewModel.
I will have following _state in ViewModel :
val userIntent = Channel<MainIntent>(Channel.UNLIMITED)
private val _state = repository.state
val state: StateFlow<MainState>
get() = _state

NetworkOnMainThreadException when using rxandroid and mvvm design pattern

I have an issue with my code which is throwing NetworkOnMainThreadException. I am trying to connect to an Android app to Odoo using Android XML-RPC library.
Here is what I am doing.
class OdooServiceImpl : OdooService {
/* This is the only function doing network operation*/
override fun userAuthenticate(
host: String,
login: String,
password: String,
database: String
): Single<Int> {
val client = XMLRPCClient("$host/xmlrpc/2/common")
val result =
client.call("login", database, login, password)
return Single.just(result as Int)
}}
This class is called from a repository class.
The repository if called by the viewmodel class using rxandroid
class OdooViewModel(private val mainRepository: OdooRepository, private val context: Context) :
ViewModel() {
val host = "https://myodoo-domain.com"
private val user = MutableLiveData<OdooResource<Int>>()
private val compositeDisposable = CompositeDisposable()
init {
authUser()
}
private fun authUser(){
user.postValue(OdooResource.authConnecting(null))
compositeDisposable.add(
mainRepository.userAuthenticate(host,"mylogin","mypassword","mdb")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
if (it != null) {
user.postValue(OdooResource.authSuccess(it))
} else {
user.postValue(
OdooResource.authError(
null,
msg = "Something went wring while authenticating to $host"
)
)
}
}, {
server.postValue(
OdooResource.conError(
null,
msg = "Something went wring while authenticating to $host"
)
)
})
)
}
override fun onCleared() {
super.onCleared()
compositeDisposable.dispose()
}
fun getUser(): LiveData<OdooResource<Int>> {
return user
}
}
I have called this class from my activity as follow
class OdooActivity : AppCompatActivity() {
private lateinit var odooViewModel: OdooViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_odoo)
setupViewModel()
setupObserver()
}
private fun setupObserver() {
odooViewModel.getUser().observe(this, Observer {
Log.i("TAGGG", "Tests")
when (it.status) {
OdooStatus.AUTHENTICATION_SUCCESS -> {
progressBar.visibility = View.GONE
it.data?.let { server -> textView.setText(server.toString()) }
textView.visibility = View.VISIBLE
}
OdooStatus.AUTHENTICATION -> {
progressBar.visibility = View.VISIBLE
textView.visibility = View.GONE
}
OdooStatus.AUTHENTICATION_ERROR -> {
//Handle Error
progressBar.visibility = View.GONE
Toast.makeText(this, it.message, Toast.LENGTH_LONG).show()
}
else -> {
}
}
})
}
private fun setupViewModel() {
val viewModelFactory = OdooViewModelFactory(OdooApiHelper(OdooServiceImpl()), this)
odooViewModel = ViewModelProviders.of(this, viewModelFactory).get(OdooViewModel::class.java)
}
}
When I run the app this is a the line which is throwing the error
odooViewModel = ViewModelProviders.of(this, viewModelFactory).get(OdooViewModel::class.java)
What am I missing here??
The culprit is here:
val result = client.call("login", database, login, password)
return Single.just(result as Int)
The call to generate the result is executed, when setting up the Rx chain, which happens on the main thread. You have to make sure that the network-call is done when actually subscribing (on io()). One solution could be to return a Single.fromCallable:
return Single.fromCallable { client.call("login", database, login, password) as Int }

Cannot create login MVVM architecture

I decided to create a small application, which uses MVVM architecture. I need to create a login flow. I use fragment as view and here's a part of its code:
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
viewModel = ViewModelProvider(this, viewModelFactory).get(LoginViewModel::class.java)
viewModel.user.observe(viewLifecycleOwner, Observer {
when (it) {
Result.Loading -> {
println("Loading")
}
is Result.Success -> {
println(it.data)
}
Result.Error -> {
println("Error")
}
}
})
log_in_button.setOnClickListener {
viewModel.logInUser("Sergei", "12345")
}
}
Here I subscribe on the user updates and set button's listener.
Here's my ViewModel:
class LoginViewModel : ViewModel() {
private val loginRepository = LoginRepository()
private var login: String = ""
private var password: String = ""
fun logInUser(login: String, password: String) {
loginRepository.fetchUser(login, password)
}
val user: LiveData<Result<String>>
get() = loginRepository.fetchUser(login, password)
.asLiveData(viewModelScope.coroutineContext)
}
I did is as I saw in the tutorial, where the author advised to create a getter, which will call a repository's fetch user method. And here's my Repository class:
class LoginRepository {
fun fetchUser(login: String, password: String) = flow {
emit(Result.Loading)
if (login == "Sergei" && password == "12345") {
emit(Result.Success("Sergei"))
} else {
emit(Result.Error)
}
}
}
I don't know if it's correct to use flow here, as I don't have the flow of users, but I use it as I emit several Results. Please, tell me, if it's a wrong decision to use flow here.
So, and my problem that I don't understand how to update the user in ViewModel class after getting data from the Repository's fetch user method, please help me to solve this problem. I will appreciate any help, thanks in advance!
** UPD **
I modified my repository and viewmodel in the following way:
class LoginRepository(private val user: MutableLiveData<Result<User>>) {
fun fetchUser(login: String, password: String) = run {
if (login == "Sergei" && password == "12345") {
user.postValue(Result.Success(User(login, password)))
}
}
}
class LoginViewModel : ViewModel() {
var user: MutableLiveData<Result<User>> = MutableLiveData()
private val loginRepository = LoginRepository(user)
fun logInUser(login: String, password: String) {
loginRepository.fetchUser(login, password)
}
}
This implementation works correctly but are there any moments to improve?

Categories

Resources