使用ViewModel和Retrofit实现用户登录

3

我正在尝试使用Retrofit和ViewModel进行登录。

我已经成功地使用仅Retrofit进行了登录...参考了这篇教程-->https://www.youtube.com/watch?v=j0wH0m_xYLs

我还没有找到任何有关使用ViewModel进行登录的教程。

找到了这个stackoverflow问题,但是它仍未得到解答--> How to make retrofit API call request method post using LiveData and ViewModel

这是我的调用活动:

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)
                        }
                    }

                }
            })

    }
}}

以下是登录响应(LoginResponse):
data class LoginResponse(val status: Int, val data: Data, val message: String, val user_msg:String)

数据类:

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?
)

关于登录的viewmodel需要帮助。

先行致谢。

在添加epicpandaforce的答案时遇到了以下错误:

在 loginviewmodel 中:

enter image description here

在 loginactivity 中:

1-->

enter image description here

2-->

enter image description here

3-->

enter image description here


你了解MVVM吗? - shafayat hossain
是的,我仍在努力工作中... @shafayathossain... - apj123
2
那么@EpicPandaForce的答案会很有帮助。我认为这是一个好答案。 - shafayat hossain
3个回答

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)
                        }
                    }
                }
            })
    }
}

使用这个库(我写了这个库,因为没有其他库可以在不使用 SingleLiveEvent 或 Event wrapper 这种反模式的情况下正确解决这个问题)

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

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

编辑:一些缺失的代码块,以便使其能够编译:

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

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"

同时添加

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

  kotlinOptions {
    jvmTarget = "1.8"
  }
}

为了在 ViewModel 中访问 applicationContext,你需要使用 AndroidViewModel 替代 ViewModel
class LoginViewModel(
    private val application: Application,
    private val savedStateHandle: SavedStateHandle
): AndroidViewModel(application) {
    private val applicationContext = application

这样应该就解决了。

编辑: 显然在ktx中,"onTextChanged"是doAfterTextChanged,我使用的是这个:

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) {
        }
    })
}

3
虽然实现不错,但您还应将用于使SavedStateHandleby viewModels() Kotlin委托属性起作用的依赖项也放入其中。我认为您应该将以下依赖项放入其中:implementation "androidx.activity:activity-ktx:1.1.0",因为由于依赖项的数量非常庞大,现在的依赖项分裂情况并不是那么明显,很难找到它。 - Mariusz Brona
由于我的水晶球充电有限,我不知道那18个错误是什么。编辑问题可以包含新的相关信息,因为我编写的代码应该是有效的,错误可能来自丢失的导入或Gradle配置。但是我无法从远处判断到这一点,因为我的假设包括“项目已经添加了正确的KTX库,并带有Kotlin jvmTarget 1.8”。 - EpicPandaForce
这行代码仍然显示错误 --> loginuser.onTextChanged --> 在 it 处出现错误 @EpicPandaForce - apj123
1
这种方法仅在Activity保留范围内执行网络操作,但状态和逻辑仍在Activity中。VM工厂是反射的,即使它不需要,状态也由视图拥有。单击监听器在Activity中执行验证逻辑。结论:Activity是视图、模型和控制器。它不是MVVM,而是具有ViewModels的God-Activity(使用LiveData作为事件总线,由于LiveData记住成功,这会导致导航触发返回等问题)。 - EpicPandaForce
需要在这里寻求帮助 @EpicPandaForce https://stackoverflow.com/questions/66868657/how-to-store-images-from-serverapi-to-room-database - apj123
显示剩余11条评论

3

@EpicPandaForce,我们把讨论从评论区转移到这里:https://stackoverflow.com/a/64365692/2448589

OP 试图弄清楚你的代码中发生了什么,我想要澄清一下。我必须说的是,这段代码将 Activity 分成两部分:一个负责仅将数据从用户交互传递给 ViewModel 并观察结果的部分,这是正确组合代码的第一步。

@EpicPandaForce 使用了 Kotlin 委托属性 by viewModels(),它是一个不错的快捷方式,不需要使用 ViewModelProviders.of(...)

另外一件事是 API 调用在 ViewModel 中进行,这是另一个不错的步骤,但是我会通过 ViewModel 的构造函数来传递它,以使代码可测试,并最终实现依赖反转原则

我个人喜欢并且也采用的最后一件事是 sealed class LoginResult,它提高了某些情况下状态的可读性,并使我们能够轻松地向 Activity 传递一些有效载荷。

唯一缺少的是依赖项和 Gradle 配置,因为提供 by viewModels()SavedStateHandle 的库针对的是 Java 8 字节码。在您的app模块中的 build.gradle 文件中添加以下内容:

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"
  }
}

我将你的代码添加到我的Gradle中,减少了一个错误...谢谢。不过,我已经在问题帖子中添加了剩余的错误图片。 - apj123
尝试添加implementation 'androidx.core:core-ktx:1.3.2' - 这应该会添加必要的Kotlin扩展方法。我还会将SharedPreferences移动到ViewModel构造函数中,作为private val - Mariusz Brona
嘿,你能在这里帮忙吗?https://stackoverflow.com/q/66533461/14046751 - apj123

1

ViewModel 只是一个中介者。它具有自己的生命周期并保存数据。 如果您想要遵循 MVVM 模式,首先必须清理代码。 将数据源、ViewModel 和 View 分离,它们都有各自的任务。欲了解有关 MVVM 的更多信息,请访问此链接。 下面的代码可能对您有所帮助:

创建一个 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)
                            }
                        }
                    }
                })
    }
}

然后创建一个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 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)
            }
        })
    }
}

最终的Activity类:
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)
            }
        }
    }
}

你能在其中添加更多细节吗?例如为什么要分别使用ViewModelProvider.Factory和数据源...请添加更多解释。 - apj123
ViewModel 持有具有其自身生命周期的数据。您必须将其与视图(即片段、活动)绑定。ViewModelProvide 完成此工作。 在此答案中,LoginViewModel 在其构造函数中需要一个参数。ViewModelProvider.Factory 为 ViewModel 提供这些参数。这就是为什么您必须编写 ViewModelProvider.factory 的原因。如果 LoginViewModel 的构造函数中没有任何参数,则不需要使用此工厂。 - shafayat hossain
你为什么不同样使用 LoginDataSource?为什么不在 ViewModel 中传递 Retrofit 调用? - apj123
这是因为MVVM架构如此规定。请阅读我回答中提供的链接。 - shafayat hossain
1.) 没有理由反射性地调用viewModel的构造函数。 2.) 状态由视图拥有,而不是ViewModel拥有,因此这实际上不是MVVM。 - EpicPandaForce

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接