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=j0
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)
}
}
}
}
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
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) {
}
})
}
@EpicPandaForce we moved the discussion from the comments from there: https://stackoverflow.com/a/64365692/2448589
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"
}
}