user login using viewmodel and retrofit
Asked Answered
Q

3

3

im trying to do login using retrofit and viewmodel

i have done successfully login with only retrofit...referred this tutorial--> https://www.youtube.com/watch?v=j0wH0m_xYLs

i havent found any tutorial related to login using viewmodel

found this stackoverflow question but it is still unanswered --> How to make retrofit API call request method post using LiveData and ViewModel

here is my call activity:--

class LoginActivity : BaseClassActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.login_activity)
    val button = findViewById<ImageView>(R.id.plusbutton)
    val forgotpassword=findViewById<TextView>(R.id.forgotpassword)
    button.setOnClickListener {
        val i = Intent(applicationContext, RegisterActivity::class.java)
        startActivity(i)
    }
    forgotpassword.setOnClickListener{
        val i = Intent(applicationContext, ForgotPassword::class.java)
        startActivity(i)
    }

    loginbtn.setOnClickListener {
        val email = loginuser.text.toString().trim()
        val password = loginpassword.text.toString().trim()

        if (email.isEmpty()) {
            Toast.makeText(
                applicationContext, "Data is missing",Toast.LENGTH_LONG
            ).show()
            loginuser.error = "Email required"
            loginuser.requestFocus()
            return@setOnClickListener
                    }


        if (password.isEmpty()) {
            loginpassword.error = "Password required"
            loginpassword.requestFocus()
            return@setOnClickListener
        }

        RetrofitClient.instance.userLogin(email, password)
            .enqueue(object : Callback<LoginResponse> {
                override fun onFailure(call: Call<LoginResponse>, t: Throwable) {
                    Log.d("res", "" + t)


                }

                override fun onResponse(
                    call: Call<LoginResponse>,
                    response: Response<LoginResponse>
                ) {
                    var res = response

                    Log.d("response check ", "" + response.body()?.status.toString())
                    if (res.body()?.status==200) {

                        SharedPrefManager.getInstance(applicationContext)
                            .saveUser(response.body()?.data!!)

                        val intent = Intent(applicationContext, HomeActivity::class.java)
                        intent.flags =
                            Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
                        showToast(applicationContext,res.body()?.message)
                        Log.d("kjsfgxhufb",response.body()?.status.toString())
                        startActivity(intent)
                        finish()


                    }

            else
                    {
                        try {
                            val jObjError =
                                JSONObject(response.errorBody()!!.string())

                            showToast(applicationContext,jObjError.getString("user_msg"))
                        } catch (e: Exception) {
                            showToast(applicationContext,e.message)
                            Log.e("errorrr",e.message)
                        }
                    }

                }
            })

    }
}}

Following is LoginResponse:-

data class LoginResponse(val status: Int, val data: Data, val message: String, val user_msg:String)

Data class:-

data class Data(

@SerializedName("id") val id: Int,
@SerializedName("role_id") val role_id: Int,
@SerializedName("first_name") val first_name: String?,
@SerializedName("last_name") val last_name: String?,
@SerializedName("email") val email: String?,
@SerializedName("username") val username: String?,
@SerializedName("profile_pic") val profile_pic: String?,
@SerializedName("country_id") val country_id: String?,
@SerializedName("gender") val gender: String?,
@SerializedName("phone_no") val phone_no: String,
@SerializedName("dob") val dob: String?,
@SerializedName("is_active") val is_active: Boolean,
@SerializedName("created") val created: String?,
@SerializedName("modified") val modified: String?,
@SerializedName("access_token") val access_token: String?
)

really need help desparately related to viewmodel for login

thanks in advance

Errors encountered while adding epicpandaforce answer:--

in loginviewmodel:--

enter image description here

in loginactivity:- 1-->

enter image description here

2-->

enter image description here

3-->

enter image description here

Quality answered 14/10, 2020 at 11:58 Comment(3)
Do you have knowledge on MVVM?Haemophilic
yes i still trying working on it.... @shafayathossain...Quality
Then the answer of @EpicPandaForce will be helpful. I think it's a good answer.Haemophilic
A
4
class LoginActivity : BaseClassActivity() {
    private val viewModel by viewModels<LoginViewModel>()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.login_activity)

        val button = findViewById<ImageView>(R.id.plusbutton)
        val forgotpassword = findViewById<TextView>(R.id.forgotpassword)

        button.setOnClickListener {
            val i = Intent(applicationContext, RegisterActivity::class.java)
            startActivity(i)
        }

        forgotpassword.setOnClickListener {
            val i = Intent(applicationContext, ForgotPassword::class.java)
            startActivity(i)
        }

        loginuser.onTextChanged {
            viewModel.user.value = it.toString()
        }

        loginpassword.onTextChanged {
            viewModel.password.value = it.toString()
        }

        loginbtn.setOnClickListener {
            viewModel.login()
        }

        viewModel.loginResult.observe(this) { result ->
            when (result) {
                UserMissing -> {
                    Toast.makeText(
                        applicationContext, "Data is missing", Toast.LENGTH_LONG
                    ).show()
                    loginuser.error = "Email required"
                    loginuser.requestFocus()
                }
                PasswordMissing -> {
                    loginpassword.error = "Password required"
                    loginpassword.requestFocus()
                }
                NetworkFailure -> {
                }
                NetworkError -> {
                    showToast(applicationContext, result.userMessage)
                }
                Success -> {
                    val intent = Intent(applicationContext, HomeActivity::class.java)
                    intent.flags =
                        Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
                    showToast(applicationContext, res.body()?.message)
                    Log.d("kjsfgxhufb", response.body()?.status.toString())
                    startActivity(intent)
                    finish()
                }
            }.safe()
        }
    }
}

class LoginViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
    sealed class LoginResult {
        object UserMissing : LoginResult(),

        object PasswordMissing : LoginResult(),

        class NetworkError(val userMessage: String) : LoginResult(),

        object NetworkFailure : LoginResult(),

        object Success : LoginResult()
    }

    val user: MutableLiveData<String> = savedStateHandle.getLiveData("user", "")
    val password: MutableLiveData<String> = savedStateHandle.getLiveData("password", "")

    private val loginResultEmitter = EventEmitter<LoginResult>()
    val loginResult: EventSource<LoginResult> = loginResultEmitter

    fun login() {
        val email = user.value!!.toString().trim()
        val password = password.value!!.toString().trim()

        if (email.isEmpty()) {
            loginResultEmitter.emit(LoginResult.UserMissing)
            return
        }


        if (password.isEmpty()) {
            loginResultEmitter.emit(LoginResult.PasswordMissing)
            return
        }

        RetrofitClient.instance.userLogin(email, password)
            .enqueue(object : Callback<LoginResponse> {
                override fun onFailure(call: Call<LoginResponse>, t: Throwable) {
                    Log.d("res", "" + t)
                    loginResultEmitter.emit(LoginResult.NetworkFailure)
                }

                override fun onResponse(
                    call: Call<LoginResponse>,
                    response: Response<LoginResponse>
                ) {
                    var res = response

                    Log.d("response check ", "" + response.body()?.status.toString())
                    if (res.body()?.status == 200) {
                        SharedPrefManager.getInstance(applicationContext).saveUser(response.body()?.data!!)
                        loginResultEmitter.emit(LoginResult.Success)
                    } else {
                        try {
                            val jObjError =
                                JSONObject(response.errorBody()!!.string())
                            loginResultEmitter.emit(LoginResult.NetworkError(jObjError.getString("user_msg")))
                        } catch (e: Exception) {
                            // showToast(applicationContext,e.message) // TODO
                            Log.e("errorrr", e.message)
                        }
                    }
                }
            })
    }
}

Using this library (that I wrote because there has not been and still isn't any other library that would solve this problem correctly without using the anti-pattern of SingleLiveEvent or the Event wrapper)

allprojects {
    repositories {
        // ...
        maven { url "https://jitpack.io" }
    }
    // ...
}

implementation 'com.github.Zhuinden:live-event:1.1.0'

EDIT: some missing blocks to actually make this compile:

fun <T> T.safe(): T = this // helper method

These dependencies in Gradle

implementation "androidx.core:core-ktx:1.3.2"
implementation "androidx.activity:activity-ktx:1.1.0"
implementation "androidx.fragment:fragment-ktx:1.2.5"
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.2.0"

Also add

android {
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }

  kotlinOptions {
    jvmTarget = "1.8"
  }
}

To access applicationContext in ViewModel, you'll need to use AndroidViewModel instead of ViewModel

class LoginViewModel(
    private val application: Application,
    private val savedStateHandle: SavedStateHandle
): AndroidViewModel(application) {
    private val applicationContext = application

And that should fix it

EDIT: apparently the "onTextChanged" is doAfterTextChanged in ktx, what I use is this:

inline fun EditText.onTextChanged(crossinline textChangeListener: (String) -> Unit) {
    addTextChangedListener(object : TextWatcher {
        override fun afterTextChanged(editable: Editable) {
            textChangeListener(editable.toString())
        }

        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
        }

        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
        }
    })
}
Aide answered 14/10, 2020 at 12:59 Comment(16)
Although the implementation is good, you should also put the dependencies you used to get SavedStateHandle and by viewModels() Kotlin delegated property working. I think you should put this dependency: implementation "androidx.activity:activity-ktx:1.1.0" because it's not that super obvious to find since the dependencies split nowadays is really hugeTaverner
@Aide sorry for late reply... i got one error in LoginViewModel and after adding dependencies of user mariusz it reduce one error in loginactivity now total 18 error in loginactivityQuality
@EpicPandaForace if shafayat answer is not mvvm why don't you help me solving the errors of your solution?? 😥😫😭Quality
Because my crystal ball has limited charges (😉) , and I didn't know what those 18 errors are. Editing the question helps to include the new relevant information, as the code I wrote should be valid, and the errors probably come from missing imports, or missing Gradle configuration. But I can't tell that from a distance, as my assumption included "the project has the right KTX libraries already added, with Kotlin jvmTarget 1.8"Aide
@Aide have you seen the 4 images in question? That is only giving 18 errors in code... 1.image) how do I access applicationcontext in viewmodel activity--> as it is showing redQuality
In loginactivity 1:keyword 'it' is showing error 2: result.usermessage is having error 3: res.body is showing error because I have to print server side message 4: 'when' is showing error because it needs else blockQuality
Where do I have put helper method? N why this is needed ...Quality
Anywhere. I usually just put it in a Utils.kt and is global to the project. It's to make sure that the when is used in an assignment and therefore non-exhaustive when becomes not a warning, but an error. Kotlin trick.Aide
this line still showing error --> loginuser.onTextChanged -->error at it @AideQuality
@Aide Also this line still showing error --> LoginViewModel.LoginResult.NetworkError saying that clasifier networkerror does not have companion object and must be initialiszed hereQuality
The exhaustive when should be working correctly. I don't see why it shouldn't, so I can only tell if another update is made to the original post.Aide
@Aide the only error is left is now see image 2 please.... Loginuser.ontextchanged error.... I'm not getting option of onTextChanged... Please guideQuality
added the missing extension funcAide
@Aide just wanted to confirm with you that above shafayat answer is not an mvvm?Quality
That approach merely executes the network op in the Activity retained scope, but the state and the logic is still in the Activity. The VM factory is reflective even tho it doesn't need to be, and state is owned by the views. The click listener executes the validation logic, in the Activity. Conclusion: the Activity is the view, the model, and the controller. It's not MVVM, it's God-Activity with ViewModels (that uses LiveData as an event bus, which has its own problems due to LiveData remembering success, which would cause navigation to trigger on back and stuff).Aide
need help here @Aide #66869157Quality
T
3

@EpicPandaForce we moved the discussion from the comments from there: https://mcmap.net/q/541403/-list-is-not-getting-in-recyclerview-using-retrofit

OP is trying to figure out what is going on in your code and I want to clarify a bit. What I must say is that this code split between Activity that is in charge of just passing the data from the user interactions to ViewModel and is observing the results is the first good step in composing the code properly.

@EpicPandaForce has used Kotlin delegated property by viewModels() which is a nice shortcut that doesn't require to use ViewModelProviders.of(...).

Another thing is that the API call is made in the ViewModel, which is another good step, however I would pass it via the constructor of ViewModel to make the code testable and it will be a good step to eventually have Dependency Inversion Principle fulfilled.

Last thing I like and do on my own is the sealed class LoginResult which improves readability of the State in which we are in certain situations and gives us ability to easily pass some payloads to the Activity.

The one thing that is missing are the dependencies and the gradle configuration, because the library that provide by viewModels() and SavedStateHandle is targeting Java 8 bytecode. Add this to your build.gradle file in app module:

android {
  ...
  // Configure only for each module that uses Java 8
  // language features (either in its source code or
  // through dependencies).
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
  // For Kotlin projects
  kotlinOptions {
    jvmTarget = "1.8"
  }
}
Taverner answered 15/10, 2020 at 6:45 Comment(3)
i added your code to my gradle ..it reduced one error ...thanks though....i have added remaining error images in question postQuality
Try adding also implementation 'androidx.core:core-ktx:1.3.2' - it should add necessary Kotlin extensions methods. I would also move the SharedPreferences to ViewModel constructor as private valTaverner
Hey can you help here stackoverflow.com/q/66533461/14046751Quality
H
1

ViewModel is nothing but a mediator. It just hold data with it's own lifecycle. If you are trying to follow MVVM, you have to clean your code first. Make data source, viewmodel and view seperate. All of them has seperate task to do. For better understanding about MVVM please follow this link Following code may help you:

Create a LoginDataSource

class LoginDataSource(private val context: Context) {

    interface LoginCallBack {
        fun onSuccess();
        fun onError(message: String?)
    }

    fun login(email: String, password: String, loginCallBack: LoginCallBack) {
        RetrofitClient.instance.userLogin(email, password)
                .enqueue(object : Callback<LoginResponse> {
                    override fun onFailure(call: Call<LoginResponse>, t: Throwable) {
                        loginCallBack.onError(t.localizedMessage)
                    }

                    override fun onResponse(
                            call: Call<LoginResponse>,
                            response: Response<LoginResponse>
                    ) {
                        var res = response
                        if (res.body()?.status==200) {

                            SharedPrefManager.getInstance(context)
                                    .saveUser(response.body()?.data!!)
                            loginCallBack.onSuccess()
                        } else {
                            try {
                                val jObjError = JSONObject(response.errorBody()!!.string())
                                loginCallBack.onError(jObjError.getString("user_msg"))

                            } catch (e: Exception) {
                                loginCallBack.onError(e.message)
                            }
                        }
                    }
                })
    }
}

Then create a ViewModelFactory

class LoginViewModelFactory(val loginDataSource: LoginDataSource) : ViewModelProvider.Factory {

    override fun <T : ViewModel?> create(modelClass: Class<T>): T {
        return modelClass.getConstructor(LoginDataSource::class.java)
                .newInstance(loginDataSource)
    }
}

ViewModel class:

class LoginViewModel(private val loginDataSource: LoginDataSource) : ViewModel() {

    val loginSuccess = MutableLiveData<Boolean>()
    val loginFailedMessage = MutableLiveData<String?>()

    fun login(email: String, password: String) {
        loginDataSource.login(email, password, object: LoginDataSource.LoginCallBack {
            override fun onSuccess() {
                loginSuccess.postValue(true)
            }

            override fun onError(message: String?) {
                loginSuccess.postValue(false)
                loginFailedMessage.postValue(message)
            }
        })
    }
}

Finally Activity class:

class LoginActivity : BaseClassActivity() {

    private lateinit var viewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.login_activity)

        val dataSource = LoginDataSource(applicationContext)
        viewModel = ViewModelProvider(this, LoginViewModelFactory(dataSource)).get(LoginViewModel::class.java)

        val button = findViewById<ImageView>(R.id.plusbutton)
        val forgotpassword = findViewById<TextView>(R.id.forgotpassword)

        viewModel.loginSuccess.observe(this, Observer {
            if(it) {
                val intent = Intent(applicationContext, HomeActivity::class.java)
                intent.flags =
                        Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
                startActivity(intent)
                finish()
            }
        })

        viewModel.loginFailedMessage.observe(this, Observer {
            showToast(applicationContext, it)
        })

        button.setOnClickListener {
            val i = Intent(applicationContext, RegisterActivity::class.java)
            startActivity(i)
        }
        forgotpassword.setOnClickListener {
            val i = Intent(applicationContext, ForgotPassword::class.java)
            startActivity(i)
        }

        loginbtn.setOnClickListener {
            val email = loginuser.text.toString().trim()
            val password = loginpassword.text.toString().trim()

            if (email.isEmpty()) {
                Toast.makeText(
                        applicationContext, "Data is missing", Toast.LENGTH_LONG
                ).show()
                loginuser.error = "Email required"
                loginuser.requestFocus()
                return@setOnClickListener
            } else if (password.isEmpty()) {
                loginpassword.error = "Password required"
                loginpassword.requestFocus()
                return@setOnClickListener
            } else {
                viewModel.login(email, password)
            }
        }
    }
}
Haemophilic answered 16/10, 2020 at 4:0 Comment(5)
can you add more details in it?like why u use ViewModelProvider.Factory and datasource differently ...please add more explanationQuality
ViewModel holds data which has it's own lifecycle. You have to bind that with your view (ie fragment, activity). ViewModelProvide do this job. In this answer LoginViewModel expected an argument in it's constructor. ViewModelProvider.Factory provides these to the viewmodel. That's why you've to write ViewModelProvider.factory. If LoginViewModel doesn't have any argument in it's constructor then this factory will not required.Haemophilic
why you use logindatasource differently? why you not pass retrofit call inside viewmodel?Quality
It's because MVVM architecture says that. Please read through the link I've given in my answer.Haemophilic
1.) there is no reason to reflectively call constructor of the viewModel, 2.) the state is owned by the views and not the ViewModel, so this isn't actually MVVM.Aide

© 2022 - 2024 — McMap. All rights reserved.