How to make login viewmodel with mutable state - kotlin

My LoginRepository
interface LoginRepository {
suspend fun postLogin(
loginRequest: LoginRequest
): Resource<Login>
}
My LoginRepoImpl
#Singleton
class LoginRepositoryImpl #Inject constructor(
private val api:ApiClient,
):LoginRepository{
override suspend fun postLogin(loginRequest: LoginRequest): Resource<Login> {
return try {
val result = api.login(loginRequest)
Resource.Success(result.toLoginDomain())
}catch (e: IOException){
e.printStackTrace()
Resource.Error("Could not load login")
}catch (e:HttpException){
e.printStackTrace()
Resource.Error("Internet connection problem")
}
}
}
My ViewModel
#HiltViewModel
class LoginViewModel #Inject constructor(
private val useCase: LoginUseCase
) : ViewModel() {
private val _state = mutableStateOf<LoginState>(LoginState.InProgress)
val state: State<LoginState> get() = _state
fun login(username: String, password: String) = viewModelScope.launch {
val login = async { useCase.execute(loginRequest = LoginRequest(username,password)) }
_state = _state.value
}
Please help me, i have no idea, sorry i just startd my career as android developer 1 month ago. so i am new in android development.

You just need a small improvement in your viewModel:
#HiltViewModel
class LoginViewModel #Inject constructor(
private val useCase: LoginUseCase
) : ViewModel() {
private val _state = mutableStateOf<LoginState>(LoginState.InProgress)
val state: State<LoginState> get() = _state
fun login(username: String, password: String) = viewModelScope.launch {
val loginDeferred = async { useCase.execute(loginRequest = LoginRequest(username,password)) }
when (loginDeferred.await()) {
is Resource.Success -> _state.value = LoginState.Success
is Resource.Error -> _state.value = LoginState.Error
}
}
}
What have we done there? We are waiting to login result via await operator. After that we push new LoginState depending on result.

Related

DiffUtil (dispatchUpdatesTo) not updating Recycler View

I am implementing a recycler view with diff util, but the first time I send the list to the adapter, the recycler view is not notified about the new list. Only the second time I update the list does the recycler receive the notification. I tried several solutions I found in similar questions but none worked. If anyone can help, thanks in advance.
Here is my code:
Adapter:
class UserListAdapter : Adapter<UserListItemViewHolder>() {
private var mUsers = mutableListOf<User>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserListItemViewHolder =
UserListItemViewHolder.create(parent)
override fun onBindViewHolder(holder: UserListItemViewHolder, position: Int) {
holder.bind(mUsers[position])
}
override fun getItemCount(): Int = mUsers.size
fun updateUsersList(users: List<User>) {
val diffCallback = UserListDiffCallback(this.mUsers, users)
val diffResult = DiffUtil.calculateDiff(diffCallback)
this.mUsers.clear()
this.mUsers.addAll(users)
diffResult.dispatchUpdatesTo(this)
}
}
UsersListDiffCallback:
class UserListDiffCallback(
private val oldList: List<User>,
private val newList: List<User>
) : DiffUtil.Callback() {
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition].username == newList[newItemPosition].username
}
override fun getOldListSize(): Int {
return oldList.size
}
override fun getNewListSize(): Int {
return newList.size
}
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return true
}
}
ViewModel:
#HiltViewModel
class MainViewModel #Inject constructor(
private val getUsersUseCase: GetUsersUseCase
) : ViewModel() {
private val _usersResultStatus = MutableLiveData<ResultStatus>(NotLoading)
val usersResultStatus: LiveData<ResultStatus>
get() = _usersResultStatus
private val _users = MutableLiveData<MutableList<User>?>()
val users: LiveData<MutableList<User>?>
get() = _users
fun getUsers() {
viewModelScope.launch {
try {
_usersResultStatus.value = Loading
_users.value = getUsersUseCase()
} catch (error: Throwable) {
_usersResultStatus.value = Error(error)
} finally {
_usersResultStatus.value = NotLoading
}
}
}
}
And where I update the list in the activity:
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private var _binding: ActivityMainBinding? = null
private val binding: ActivityMainBinding get() = _binding!!
private val viewModel: MainViewModel by viewModels()
private val usersAdapter = UserListAdapter()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
initUsersAdapter()
observeStates()
viewModel.getUsers()
}
private fun initUsersAdapter() {
with(binding.recyclerView) {
setHasFixedSize(true)
adapter = usersAdapter
}
}
private fun observeStates() {
viewModel.usersResultStatus.observe(this) { resultStatus ->
setProgressbarVisibility(resultStatus is Loading)
if (resultStatus is Error) {
Toast.makeText(
this#MainActivity,
getString(R.string.error),
Toast.LENGTH_SHORT
).show()
}
}
viewModel.users.observe(this) { users ->
users?.let {
usersAdapter.updateUsersList(users.toMutableList())
}
}
}
private fun setProgressbarVisibility(show: Boolean) {
binding.progressBar.visibility = if (show) View.VISIBLE else View.GONE
}
}
Trying to add item range changed to your code works like this for me
fun updateUsersList(users: List<User>) {
val diffCallback = UserListDiffCallback(this.mUsers, users)
val diffResult = DiffUtil.calculateDiff(diffCallback)
this.mUsers.clear()
this.mUsers.addAll(users)
diffResult.dispatchUpdatesTo(this)
notifyItemRangeChanged(0, users.size)
}

Cannot create an instance of class error in Kotlin ViewModel

I am trying to create a ViewModel in my Kotlin app, but I am getting the following error: java.lang.RuntimeException: Cannot create an instance of class com.ri.movieto.presentation.home.HomeViewModel. Also, I am using Hilt.
And here is the code for my HomeFragment:
#AndroidEntryPoint
class HomeFragment : Fragment() {
private var _binding: FragmentHomeBinding? = null
private val homeViewModel: HomeViewModel by viewModels()
// This property is only valid between onCreateView and
// onDestroyView.
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentHomeBinding.inflate(inflater, container, false)
contentLayout = binding.contentLayout
dataLoading = binding.dataLoading
subscribeToObservables()
return binding.root
}
private fun subscribeToObservables() {
lifecycleScope.launchWhenStarted {
homeViewModel.state.collectLatest {
Log.e("++++", it.response?.page.toString())
}
}
}
}
Here is the code for my HomeViewModel:
#HiltViewModel
class HomeViewModel #Inject constructor(
private val getTrendingMoviesUseCase: GetTrendingMoviesUseCase
) : ViewModel() {
private val _state = MutableStateFlow(TrendingMoviesState())
val state = _state.asStateFlow()
init {
getTrendingMovies()
}
private fun getTrendingMovies() {
getTrendingMoviesUseCase().onEach { result ->
when (result) {
is Resource.Success -> {
_state.value = TrendingMoviesState(response = result.data)
}
is Resource.Error -> {
_state.value =
TrendingMoviesState(error = result.message ?: "An unexpected error occurred")
}
is Resource.Loading -> {
_state.value = TrendingMoviesState(isLoading = true)
}
}
}
}
}
GetTrendingMoviesUseCase.kt:
class GetTrendingMoviesUseCase #Inject constructor(
private val repository: MovieRepository,
private val movieResponseDtoToDomain: MovieResponseDtoToDomain
) {
operator fun invoke(): Flow<Resource<MovieResponse>> = flow {
try {
emit(Resource.Loading())
val movieResponseDto = repository.getTrendingMovies()
val movieResponse = movieResponseDtoToDomain.mapFrom(movieResponseDto)
emit(Resource.Success(movieResponse))
} catch (e: HttpException) {
emit(Resource.Error(e.localizedMessage ?: "Beklenmeyen bir hata oluştu"))
} catch (e: IOException) {
emit(Resource.Error("Lütfen internet bağlantınızı kontrol edin"))
}
}
}
MovieResponseDtoToDomain.kt:
#Singleton
class MovieResponseDtoToDomain #Inject constructor(private val itemDecider: MovieItemDecider) :
Mapper<MovieResponseDto, MovieResponse> {
override fun mapFrom(input: MovieResponseDto): MovieResponse {
return MovieResponse(
page = input.page,
movies = input.results.map { movie ->
MovieResponse.Movie(
release_year = itemDecider.provideReleaseYear(movie.release_date),
poster_path = itemDecider.providePosterPath(movie.poster_path),
backdrop_path = itemDecider.provideBackdropPath(movie.backdrop_path),
vote_average = itemDecider.provideRoundedAverage(movie.vote_average),
title = movie.title,
id = movie.id,
video = movie.video,
release_date = movie.release_date,
overview = movie.overview,
genre_ids = movie.genre_ids
)
},
total_pages = input.total_pages,
total_results = input.total_results
)
}
}

Type mismatch: inferred type is CategoryQuran but MainAdapter.onSelectData

i am just trying to code for Get API, but when i write some code for that. it seems error, i took a few hours but i not get the point, can you guys help me?
this is the Activity (ActivityQuran)
class CategoryQuran : AppCompatActivity() {
var mainAdapter: MainAdapter? = null
var mProgressBar: ProgressDialog? = null
var modelMain: MutableList<ModelMain> = ArrayList()
private lateinit var adapter: MainAdapter
private lateinit var postArrayList: ArrayList<MainAdapter>
private lateinit var progressDialog: ProgressDialog
private val TAG = "MAIN_TAG"
private var isSearch = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_detail_artikel)
//setup progress dialog
progressDialog = ProgressDialog (this)
progressDialog.setTitle("Mohon Tunggu")
mProgressBar = ProgressDialog(this)
mProgressBar!!.setTitle("Mohon Tunggu")
mProgressBar!!.setCancelable(false)
mProgressBar!!.setMessage("Sedang menampilkan data...")
llAbout.setOnClickListener {
startActivity(Intent(this#CategoryQuran, AboutActivity::class.java)) }
llPP.setOnClickListener {
startActivity(Intent(this#CategoryQuran, PrivacyPolicyActivity::class.java)) }
llDisclaimer.setOnClickListener {
startActivity(Intent(this#CategoryQuran, DisclaimerActivity::class.java)) }
rvListArticles.setHasFixedSize(true)
rvListArticles.setLayoutManager(LinearLayoutManager(this))
//get data
listArticle
//search
searchBtn.setOnClickListener {
}
}
private val listArticle: Unit
private get() {
mProgressBar!!.show()
AndroidNetworking.get(BloggerApi.ListPost)
.setPriority(Priority.MEDIUM)
.build()
.getAsJSONObject(object : JSONObjectRequestListener {
override fun onResponse(response: JSONObject) {
try {
mProgressBar!!.dismiss()
val playerArray = response.getJSONArray("items")
for (i in 0 until playerArray.length()) {
val jsonObject1 = playerArray.getJSONObject(i)
val dataApi = ModelMain()
dataApi.title = jsonObject1.getString("title")
dataApi.content = jsonObject1.getString("content")
dataApi.labels = jsonObject1.getString("labels")
dataApi.url = jsonObject1.getString("url")
val datePost = jsonObject1.getString("published")
val inputFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss")
val outputFormat = SimpleDateFormat("dd-MM-yyyy")
val date = inputFormat.parse(datePost)
val datePostConvert = outputFormat.format(date)
dataApi.published = datePostConvert
val jsonObject2 = jsonObject1.getJSONObject("author")
val authorPost = jsonObject2.getString("displayName")
dataApi.author = authorPost
val jsonObject3 = jsonObject2.getJSONObject("image")
val authorImage = jsonObject3.getString("url")
dataApi.authorImage = Uri.parse("http:$authorImage").toString()
modelMain.add(dataApi)
showListArticle()
}
} catch (e: JSONException) {
e.printStackTrace()
Toast.makeText(this#CategoryQuran,
"Gagal menampilkan data!", Toast.LENGTH_SHORT).show()
} catch (e: ParseException) {
e.printStackTrace()
Toast.makeText(this#CategoryQuran,
"Gagal menampilkan data!", Toast.LENGTH_SHORT).show()
}
}
override fun onError(anError: ANError) {
mProgressBar!!.dismiss()
Toast.makeText(this#CategoryQuran,
"Tidak ada jaringan internet!", Toast.LENGTH_SHORT).show()
}
})
}
private fun showListArticle(){
mainAdapter = MainAdapter(this#CategoryQuran, modelMain, this )
rvListArticles!!.adapter = mainAdapter
}
private fun searchPosts(query: String) {
mainAdapter = MainAdapter(this#CategoryQuran, modelMain, this)
rvListArticles!!.adapter = mainAdapter
}
override fun onSelected(modelMain: ModelMain) {
val intent = Intent(this#CategoryQuran, DetailArtikelActivity::class.java)
intent.putExtra("detailArtikel", modelMain)
startActivity(intent)
}
}
i got error in 'this'
private fun showListArticle(){
mainAdapter = MainAdapter(this#CategoryQuran, modelMain, this )
rvListArticles!!.adapter = mainAdapter
}
private fun searchPosts(query: String) {
mainAdapter = MainAdapter(this#CategoryQuran, modelMain, this)
rvListArticles!!.adapter = mainAdapter
}
both show 'Type mismatch: inferred type is CategoryQuran but MainAdapter.onSelectData! was expected'
Can you guys help me? please
Ok so the problem is i am have to add OnSelectData in my Class, in my case what i have to do is add MainAdaper.OnSelectData
so Before it like :
class CategoryQuran : AppCompatActivity() {
and after it like :
class CategoryQuran : AppCompatActivity(), MainAdapter.OnSelectData {
thats my fault, im too rush when i coding it

My first observer called correctly but another was not called after inserted data to room database in kotlin android

In the application, I am fetching data from the web and from the observer change method, Insert that data to local db. that's fine. but after inserted to db, My second observer not called so my UI will not update.
ManActivity.class
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainViewModel
private lateinit var adapter: MainAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(layout.activity_main)
setupViewModel()
setupUI()
setupObservers()
setupObservers2()
}
private fun setupViewModel() {
viewModel = ViewModelProviders.of(
this,
ViewModelFactory(ApiHelper(RetrofitBuilder.apiService))
).get(MainViewModel::class.java)
}
private fun setupUI() {
recyclerView.layoutManager = LinearLayoutManager(this)
adapter = MainAdapter(arrayListOf())
recyclerView.addItemDecoration(
DividerItemDecoration(
recyclerView.context,
(recyclerView.layoutManager as LinearLayoutManager).orientation
)
)
recyclerView.adapter = adapter
}
private fun setupObservers() {
viewModel.getUsers().observe(this, Observer {
//viewModel.getUserFromWeb()
it?.let { resource ->
when (resource.status) {
SUCCESS -> {
Log.d("MYLOG","MyAPIChange success")
recyclerView.visibility = View.VISIBLE
progressBar.visibility = View.GONE
resource.data?.let {
users -> viewModel.setUserListToDB(this,users)
//sleep(1000)
}
}
ERROR -> {
recyclerView.visibility = View.VISIBLE
progressBar.visibility = View.GONE
Log.d("MYLOG","MyAPIChange error")
Toast.makeText(this, it.message, Toast.LENGTH_LONG).show()
}
LOADING -> {
Log.d("MYLOG","MyAPIChange loading")
progressBar.visibility = View.VISIBLE
recyclerView.visibility = View.GONE
}
}
}
})
}
private fun setupObservers2() {
viewModel.getUserFromDB(this).observe(this, Observer {
users -> retrieveList(users)
Log.d("MYLOG","..MyDBChange")
})
}
private fun retrieveList(users: List<User>) {
adapter.apply {
addUsers(users)
notifyDataSetChanged()
}
}
}
MyViewModel.class
class MainViewModel(private val mainRepository: MainRepository) : ViewModel() {
//lateinit var tempUser : MutableLiveData<List<User>>
fun getUsers() = liveData(Dispatchers.IO) {
emit(Resource.loading(data = null))
try {
emit(Resource.success(data = mainRepository.getUsers()))
} catch (exception: Exception) {
emit(Resource.error(data = null, message = exception.message ?: "Error Occurred!"))
}
//emit(mainRepository.getUsers()) //direct call
}
fun getUserFromDB(context: Context) = liveData(Dispatchers.IO) {
emit(mainRepository.getUserList(context))
}
fun setUserListToDB(context: Context, userList: List<User>) {
/*GlobalScope.launch {
mainRepository.setUserList(context, userList)
}*/
CoroutineScope(Dispatchers.IO).launch {
mainRepository.setUserList(context, userList)
}
}
}
MyRepository.class
class MainRepository(private val apiHelper: ApiHelper) {
suspend fun getUsers() = apiHelper.getUsers() // get from web
companion object {
var myDatabase: MyDatabase? = null
lateinit var userList: List<User>
fun initializeDB(context: Context): MyDatabase {
return MyDatabase.getDataseClient(context)
}
/*fun insertData(context: Context, username: String, password: String) {
myDatabase = initializeDB(context)
CoroutineScope(Dispatchers.IO).launch {
val loginDetails = User(username, password)
myDatabase!!.myDao().InsertData(loginDetails)
}
}*/
}
//fun getUserList(context: Context, username: String) : LiveData<LoginTableModel>? {
suspend fun getUserList(context: Context) : List<User> {
myDatabase = initializeDB(context)
userList = myDatabase!!.myDao().getUserList()
Log.d("MYLOG=", "DBREAD"+userList.size.toString())
return userList
}
fun setUserList(context: Context,userList: List<User>){
myDatabase = initializeDB(context)
/*CoroutineScope(Dispatchers.IO).launch {
myDatabase!!.myDao().InsertAllUser(userList)
Log.d("MYLOG","MyDBInserted")
}*/
myDatabase!!.myDao().InsertAllUser(userList)
Log.d("MYLOG","MyDBInserted")
/*val thread = Thread {
myDatabase!!.myDao().InsertAllUser(userList)
}
Log.d("MYLOG","MyDBInserted")
thread.start()*/
}
}
DAO class
#Dao
interface DAOAccess {
#Insert(onConflict = OnConflictStrategy.REPLACE)
fun InsertAllUser(userList: List<User>)
// #Query("SELECT * FROM User WHERE Username =:username")
// fun getLoginDetails(username: String?) : LiveData<LoginTableModel>
#Query("SELECT * FROM User")
suspend fun getUserList() : List<User>
}
RetrofitBuilder
object RetrofitBuilder {
private const val BASE_URL = "https://5e510330f2c0d300147c034c.mockapi.io/"
private fun getRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.build()
}
val apiService: ApiService = getRetrofit().create(ApiService::class.java)
}
Please can you know that what I am doing wrong here and why the second observer was not called after insert to db
Actually It was called when the screen launched but that time data not inserted so list size was 0 and after insert data this method will not call again. But Once I close app and again start then data will display bcoz at launch time, this method call and data got
I don't have enough reputation to commet, therefore I just bring a suggestion in this answer:
Suggestion/Solution
Room supports LiveData out of the box. So in your DAO you can change
suspend fun getUserList() : List<User>
to
suspend fun getUserList() : LiveData<List<User>>
Then in your repository adjust to
suspend fun getUserList(context: Context) : LiveData<List<User>> {
myDatabase = initializeDB(context)
userList = myDatabase!!.myDao().getUserList()
Log.d("MYLOG=", "DBREAD"+userList.value.size.toString())
return userList
}
and in the ViewModel
fun getUserFromDB(context: Context) = mainRepository.getUserList(context))
With these adjustments I think it should work.
Explaination
You used the liveData couroutines builder here
fun getUserFromDB(context: Context) = liveData(Dispatchers.IO) {
emit(mainRepository.getUserList(context))
}
As far as I understand this builder, it is meant to execute some asynchronous/suspend task and as soon as this task finishes the liveData you created will emit the result. That means that you only once receive the state of the user list an emidiately emit the list to the observer one single time and then this liveData is done. It does not observe changes to the list in the DB the whole time.
That is why it works perfectly for observing the API call (you want to wait until the call is finished and emit the response one single time), but not for observing the DB state(you want to observe the user list in the DB all the time and emit changes to the observer whenever the list is changed)

Test a view model with livedata, coroutines (Kotlin)

I've been trying to test my view model for several days without success.
This is my view model :
class AdvertViewModel : ViewModel() {
private val parentJob = Job()
private val coroutineContext: CoroutineContext
get() = parentJob + Dispatchers.Default
private val scope = CoroutineScope(coroutineContext)
private val repository : AdvertRepository = AdvertRepository(ApiFactory.Apifactory.advertService)
val advertContactLiveData = MutableLiveData<String>()
fun fetchRequestContact(requestContact: RequestContact) {
scope.launch {
val advertContact = repository.requestContact(requestContact)
advertContactLiveData.postValue(advertContact)
}
}
}
This is my repository :
class AdvertRepository (private val api : AdvertService) : BaseRepository() {
suspend fun requestContact(requestContact: RequestContact) : String? {
val advertResponse = safeApiCall(
call = {api.requestContact(requestContact).await()},
errorMessage = "Error Request Contact"
)
return advertResponse
}
}
This is my view model test :
#RunWith(JUnit4::class)
class AdvertViewModelTest {
private val goodContact = RequestContact(...)
private lateinit var advertViewModel: AdvertViewModel
private var observer: Observer<String> = mock()
#get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
#Before
fun setUp() {
advertViewModel = AdvertViewModel()
advertViewModel.advertContactLiveData.observeForever(observer)
}
#Test
fun fetchRequestContact_goodResponse() {
advertViewModel.fetchRequestContact(goodContact)
val captor = ArgumentCaptor.forClass(String::class.java)
captor.run {
verify(observer, times(1)).onChanged(capture())
assertEquals("someValue", value)
}
}
}
The method mock() :
inline fun <reified T> mock(): T = Mockito.mock(T::class.java)
I got this error :
Wanted but not invoked: observer.onChanged();
-> at com.vizzit.AdvertViewModelTest.fetchRequestContact_goodResponse(AdvertViewModelTest.kt:52)
Actually, there were zero interactions with this mock.
I don't understand how to retrieve the result of my query.
You would need to write a OneTimeObserver to observe livedata from the ViewModel
class OneTimeObserver<T>(private val handler: (T) -> Unit) : Observer<T>, LifecycleOwner {
private val lifecycle = LifecycleRegistry(this)
init {
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
}
override fun getLifecycle(): Lifecycle = lifecycle
override fun onChanged(t: T) {
handler(t)
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
}
}
After that you can write an extension function:
fun <T> LiveData<T>.observeOnce(onChangeHandler: (T) -> Unit) {
val observer = OneTimeObserver(handler = onChangeHandler)
observe(observer, observer)
}
Than you can check this ViewModel class class that I have from a project to check what's going on with your LiveData after you act (when) with invoking a method.
As for your error, it just says that the onChanged() method is not being called ever.