I'm doing a practise with the rick and morty api and I have two fragments, one with a recycleview with the characters and another one with the detail of the character, where also you can update some of the values.
My problem is that when I update a value, if I go back to the main fragment with the recycle view, that value is updated but when I go back again to the detail, the value is again the original one. I don't know how to fix it.
This is my detail fragment:
class GetCharacterDetail: Fragment() {
private var binding: CharacterDetailFragmentBinding by autoCleared()
private val viewModel: CharacterDetailViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = CharacterDetailFragmentBinding.inflate(inflater, container, false)
val edit = binding.editButton
val save = binding.saveBotton
changeStateOnEdit(edit, save)
save.setOnClickListener {
val gender = binding.characterGenderText.text.toString()
val status = binding.characterStatusText.text.toString()
val species = binding.characterSpeciesText.text.toString()
updateCharacterDetails(gender, status, species, edit, save)
}
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
arguments?.getInt("id")?.let { viewModel.start(it) }
setupObservers()
}
private fun setupObservers() {
viewModel.character.observe(viewLifecycleOwner, Observer {
when (it.status) {
Status.StatusEnum.SUCCESS -> {
bindCharacter(it.data!! as CharacterEntity)
binding.progressBar.visibility = View.GONE
binding.characterDetailLayout.visibility = View.VISIBLE
}
Status.StatusEnum.ERROR ->
Toast.makeText(activity, it.message, Toast.LENGTH_SHORT).show()
Status.StatusEnum.LOADING -> {
binding.progressBar.visibility = View.VISIBLE
binding.characterDetailLayout.visibility = View.GONE
}
}
})
}
private fun bindCharacter(character: CharacterEntity) {
if (character != null) {
binding.characterName.text = character.name
binding.characterSpeciesText.setText(character.species)
binding.characterStatusText.setText(character.status)
binding.characterGenderText.setText(character.gender)
Glide.with(binding.root)
.load(character.image)
.into(binding.characterImage)
}
}
private fun changeStateOnEdit(edit: ImageButton, save: MaterialButton) {
edit.setOnClickListener(View.OnClickListener {
edit.isVisible = false
binding.characterGender.isEnabled = true
binding.characterSpecies.isEnabled = true
binding.characterStatus.isEnabled = true
save.isVisible = true
})
}
private fun updateCharacterDetails(gender: String, status: String, species: String,edit: ImageButton, save: MaterialButton) {
viewModel.updateCharacterDetails(gender, status, species)
viewModel.character.observe(viewLifecycleOwner, Observer {
when (it.status) {
Status.StatusEnum.SUCCESS -> {
Toast.makeText(activity, "Personaje actualizado correctamente", Toast.LENGTH_SHORT).show()
edit.isVisible = true
binding.characterGender.isEnabled = false
binding.characterSpecies.isEnabled = false
binding.characterStatus.isEnabled = false
save.isVisible = false
bindCharacter(it.data!!)
}
Status.StatusEnum.ERROR ->
Toast.makeText(activity, it.message, Toast.LENGTH_SHORT).show()
Status.StatusEnum.LOADING -> {
binding.progressBar.visibility = View.VISIBLE
binding.characterDetailLayout.visibility = View.GONE
}
}
})
}
}
And this is my ViewModel:
class CharacterDetailViewModel #Inject constructor(
private val repository: CharacterRepository
) : ViewModel() {
private val idCharacter = MutableLiveData<Int>()
val character = idCharacter.switchMap { id ->
repository.getCharacter(id)
}
fun updateCharacterDetails(gender: String, status: String, species: String) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
val id = idCharacter.value ?: return#withContext
repository.updateCharacterDetail(id, gender, status, species)
}
}
}
fun start(id: Int) {
idCharacter.value = id
}
}
Herew is the repository:
class CharacterRepository #Inject constructor(
private val api : CharacterService,
private val characterDao: CharacterDao
) {
fun getAllCharacters() = getEntitiesOperation(
databaseQuery = { characterDao.getAllCharacters() },
networkCall = { api.getCharacters() },
saveCallResult = { characterDao.insertAll(it.results) }
)
fun getCharacter(id: Int) = getEntitiesOperation(
databaseQuery = { characterDao.getCharacter(id) },
networkCall = { api.getCharacter(id) },
saveCallResult = { characterDao.insert(it) }
)
fun deleteCharacter(id: Int) = characterDao.deleteCharacter(id)
fun updateCharacterDetail(id: Int, gender:String, status:String, species:String) =
characterDao.updateCharacterDetail(id, gender, status, species)
}
And the function I use to take the data from local database if there is data in it. Here is where I think it is the problem since I think that something has to be recovered wrong and that localData is null and then the method look for the data on the api
fun <T, A> getEntitiesOperation(databaseQuery: () -> LiveData<T>,
networkCall: suspend () -> Status<A>,
saveCallResult: suspend (A) -> Unit):
LiveData<Status<T>> = liveData(Dispatchers.IO) {
emit(Status.loading())
val source = databaseQuery.invoke().map { Status.success(it) }
emitSource(source)
val localData = source.value?.data
if (localData != null) return#liveData
val responseStatus = networkCall.invoke()
if (responseStatus.status == StatusEnum.SUCCESS) {
saveCallResult(responseStatus.data!!)
} else if (responseStatus.status == StatusEnum.ERROR) {
emit(Status.error(responseStatus.message!!))
emitSource(source)
}
}
I've been with this problem all day and I don't know what to do or how to fix it. Thank you in advance for the help
Related
I am almost new to android testing and following the official docs and Udacity course for learning purposes.
Coming to the issue I want to check when the task is completed or incompleted to be displayed properly or not, for this I wrote a few tests. Here I got the exception that toast can not be displayed on a thread that has not called Looper.prepare.
When I comment out the toast msg live data updating line of code then all tests work fine and pass successfully. I am new to android testing and searched out a lot but did not get any info to solve this issue. Any help would be much appreciated. A little bit of explanation will be much more helpful if provided.
Below is my test class source code along with ViewModel, FakeRepository, and fragment source code.
Test Class.
#ExperimentalCoroutinesApi
#MediumTest
#RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {
#get:Rule
var mainCoroutineRule = MainCoroutineRule()
#get:Rule
val rule = InstantTaskExecutorRule()
private lateinit var tasksRepository: FakeTasksRepository
#Before
fun setUp() {
tasksRepository = FakeTasksRepository()
ServiceLocator.taskRepositories = tasksRepository
}
#Test
fun addNewTask_addNewTaskToDatabase() = mainCoroutineRule.runBlockingTest {
val newTask = Task(id = "1", userId = 0, title = "Hello AndroidX World",false)
tasksRepository.addTasks(newTask)
val task = tasksRepository.getTask(newTask.id)
assertEquals(newTask.id,(task as Result.Success).data.id)
}
#Test
fun activeTaskDetails_DisplayedInUi() = mainCoroutineRule.runBlockingTest {
val newTask = Task(id = "2", userId = 0, title = "Hello AndroidX World",false)
tasksRepository.addTasks(newTask)
val bundle = TaskDetailFragmentArgs(newTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.Theme_ToDoWithTDD)
onView(withId(R.id.title_text)).check(matches(isDisplayed()))
onView(withId(R.id.title_text)).check(matches(withText("Hello AndroidX World")))
onView(withId(R.id.complete_checkbox)).check(matches(isDisplayed()))
onView(withId(R.id.complete_checkbox)).check(matches(isNotChecked()))
}
#Test
fun completedTaskDetails_DisplayedInUI() = mainCoroutineRule.runBlockingTest {
val newTask = Task(id = "2", userId = 0, title = "Hello AndroidX World",true)
tasksRepository.addTasks(newTask)
val bundle = TaskDetailFragmentArgs(newTask.id).toBundle()
launchFragmentInContainer <TaskDetailFragment>(bundle,R.style.Theme_ToDoWithTDD)
onView(withId(R.id.title_text)).check(matches(isDisplayed()))
onView(withId(R.id.title_text)).check(matches(withText("Hello AndroidX World")))
onView(withId(R.id.complete_checkbox)).check(matches(isDisplayed()))
onView(withId(R.id.complete_checkbox)).check(matches(isChecked()))
}
#After
fun tearDown() = mainCoroutineRule.runBlockingTest {
ServiceLocator.resetRepository()
}
}
FakeRepository class.
class FakeTasksRepository: TasksRepository {
var tasksServiceData: LinkedHashMap<String,Task> = LinkedHashMap()
private val observableTasks: MutableLiveData<Result<List<Task>>> = MutableLiveData()
private var shouldReturnError: Boolean = false
fun setReturnError(value: Boolean) {
shouldReturnError = value
}
override fun observeTasks(): LiveData<Result<List<Task>>> {
return observableTasks
}
override fun observeTask(taskId: String): LiveData<Result<Task>> {
runBlocking { fetchAllToDoTasks() }
return observableTasks.map { tasks ->
when(tasks) {
is Result.Loading -> Result.Loading
is Result.Error -> Result.Error(tasks.exception)
is Result.Success -> {
val task = tasks.data.firstOrNull() { it.id == taskId }
?: return#map Result.Error(Exception("Not found"))
Result.Success(task)
}
}
}
}
override suspend fun completeTask(id: String) {
tasksServiceData[id]?.completed = true
}
override suspend fun completeTask(task: Task) {
val compTask = task.copy(completed = true)
tasksServiceData[task.id] = compTask
fetchAllToDoTasks()
}
override suspend fun activateTask(id: String) {
tasksServiceData[id]?.completed = false
}
override suspend fun activateTask(task: Task) {
val activeTask = task.copy(completed = false)
tasksServiceData[task.id] = activeTask
fetchAllToDoTasks()
}
override suspend fun getTask(taskId: String): Result<Task> {
if (shouldReturnError) return Result.Error(Exception("Test Exception"))
tasksServiceData[taskId]?.let {
return Result.Success(it)
}
return Result.Error(Exception("Could not find task"))
}
override suspend fun getTasks(): Result<List<Task>> {
return Result.Success(tasksServiceData.values.toList())
}
override suspend fun saveTask(task: Task) {
tasksServiceData[task.id] = task
}
override suspend fun clearAllCompletedTasks() {
tasksServiceData = tasksServiceData.filterValues {
!it.completed
} as LinkedHashMap<String, Task>
}
override suspend fun deleteAllTasks() {
tasksServiceData.clear()
fetchAllToDoTasks()
}
override suspend fun deleteTask(taskId: String) {
tasksServiceData.remove(taskId)
fetchAllToDoTasks()
}
override suspend fun fetchAllToDoTasks(): Result<List<Task>> {
if(shouldReturnError) {
return Result.Error(Exception("Could not find task"))
}
val tasks = Result.Success(tasksServiceData.values.toList())
observableTasks.value = tasks
return tasks
}
override suspend fun updateLocalDataStore(list: List<Task>) {
TODO("Not yet implemented")
}
fun addTasks(vararg tasks: Task) {
tasks.forEach {
tasksServiceData[it.id] = it
}
runBlocking {
fetchAllToDoTasks()
}
}
}
Fragment class.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.loadTaskById(args.taskId)
setUpToast(this,viewModel.toastText)
viewModel.editTaskEvent.observe(viewLifecycleOwner, {
it?.let {
val action = TaskDetailFragmentDirections
.actionTaskDetailFragmentToAddEditFragment(
args.taskId,
resources.getString(R.string.edit_task)
)
findNavController().navigate(action)
}
})
binding.editTaskFab.setOnClickListener {
viewModel.editTask()
}
}
ViewModel class.
class TaskDetailViewModel(
private val tasksRepository: TasksRepository
) : ViewModel() {
private val TAG = "TaskDetailViewModel"
private val _taskId: MutableLiveData<String> = MutableLiveData()
private val _task = _taskId.switchMap {
tasksRepository.observeTask(it).map { res ->
Log.d("Test","res with value ${res.toString()}")
isolateTask(res)
}
}
val task: LiveData<Task?> = _task
private val _toastText = MutableLiveData<Int?>()
val toastText: LiveData<Int?> = _toastText
private val _dataLoading = MutableLiveData<Boolean>()
val dataLoading: LiveData<Boolean> = _dataLoading
private val _editTaskEvent = MutableLiveData<Unit?>(null)
val editTaskEvent: LiveData<Unit?> = _editTaskEvent
fun loadTaskById(taskId: String) {
if(dataLoading.value == true || _taskId.value == taskId) return
_taskId.value = taskId
Log.d("Test","loading task with id $taskId")
}
fun editTask(){
_editTaskEvent.value = Unit
}
fun setCompleted(completed: Boolean) = viewModelScope.launch {
val task = _task.value ?: return#launch
if(completed) {
tasksRepository.completeTask(task.id)
_toastText.value = R.string.task_marked_complete
}
else {
tasksRepository.activateTask(task.id)
_toastText.value = R.string.task_marked_active
}
}
private fun isolateTask(result: Result<Task?>): Task? {
return if(result is Result.Success) {
result.data
} else {
_toastText.value = R.string.loading_tasks_error
null
}
}
#Suppress("UNCHECKED_CAST")
class TasksDetailViewModelFactory(
private val tasksRepository: TasksRepository
): ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return (TaskDetailViewModel(
tasksRepository
) as T)
}
}
}
In this method in ViewModel when I comment out the below line of code all tests passed.
_toastText.value = R.string.loading_tasks_error
private fun isolateTask(result: Result<Task?>): Task? {
return if(result is Result.Success) {
result.data
} else {
_toastText.value = R.string.loading_tasks_error // Comment out this line then all test passed.
null
}
}
I'm stuck on a problem with my app.
I'm trying to upload some photos to a cloud and then, I'll show to the user a list of url to copy, in order to use them in a second time.
I'm using RxWorker for this.
When I enqueue my workers list, everything seems fine (one OneTimeWorkRequestBuilder for each image), but then, when I retrieve the result, seems that my observe is triggered too many times.
In order to make this step suspendable, I've two lists (image and worker ones).
When a work is finished (both failure or success) I cancel both image and worker from their own list.
Here's my code. Thanks to everyone!
My Fragment:
class ImagesUploadFragment : Fragment() {
companion object {
private const val TAG = "ImagesUploadFragment"
private const val PHOTOS_LIST = "PHOTOS_LIST"
private const val WORK_NAME = "PHOTO_UPLOAD"
fun newInstance(optionalData: String?): ImagesUploadFragment {
val imagesUploadFragment = ImagesUploadFragment()
val bundle = Bundle()
bundle.putString(PHOTOS_LIST, optionalData)
imagesUploadFragment.arguments = bundle
return imagesUploadFragment
}
}
private lateinit var imagesUploadBinding: FragmentImagesUploadBinding
private var stepListener: StepListener? = null
private val mDownloadUrlsList = mutableListOf<String>()
private val mWorkManger: WorkManager by lazy { WorkManager.getInstance(requireContext()) }
private val mWorkRequestList = mutableListOf<OneTimeWorkRequest>()
private var isUploadPaused = false
private val mImagesList = mutableListOf<String>()
private val gson: Gson by inject()
override fun onAttach(context: Context) {
super.onAttach(context)
stepListener = context as? StepListener
}
private val imagesUploadViewModel: ImagesUploadViewModel by viewModel()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
imagesUploadBinding = FragmentImagesUploadBinding.inflate(inflater, container, false)
return imagesUploadBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val imagesList = arguments?.getString(PHOTOS_LIST)
imagesList?.let {
startImagesUpload(it)
}
imagesUploadBinding.successfullyUpdated.text = String.format(getString(R.string.succeeded_updates), 0)
imagesUploadBinding.workActionIv.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_pause))
imagesUploadBinding.buttonContinue.setOnClickListener {
stepListener?.onStepChange(4, gson.toJson(mDownloadUrlsList))
}
mWorkManger.cancelAllWorkByTag(TAG)
mWorkManger.cancelAllWork()
mWorkManger.pruneWork()
imagesUploadBinding.workActionIv.setOnClickListener {
if (!isUploadPaused) {
isUploadPaused = true
pauseUpload()
imagesUploadBinding.workActionIv.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_play))
} else {
isUploadPaused = false
resumeUpload()
imagesUploadBinding.workActionIv.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_pause))
}
}
setupWorkManagerObserve()
with(imagesUploadViewModel) {
observe(imagesListResource,
success = { imagesList ->
imagesUploadBinding.uploadProgress.max = imagesList.first.size
mImagesList.addAll(imagesList.first.map { it.jsonValue })
setupWorkerList()
},
failure = {
Timber.e(it)
}
)
observe(uploadedFileResource,
success = {
if(it.isNotEmpty()) mDownloadUrlsList.add(it)
imagesUploadBinding.buttonContinue.isVisible = mImagesList.size == 0
imagesUploadBinding.workActionIv.isVisible = mImagesList.size > 0
imagesUploadBinding.horizontalSpace.isVisible = mImagesList.size > 0
},
failure = {
Timber.e(it)
}
)
}
}
private fun pauseUpload() {
mWorkManger.cancelAllWork()
}
private fun resumeUpload() {
setupWorkerList()
}
private fun startImagesUpload(imagesListJson: String) {
imagesUploadViewModel.getImagesFromUri(imagesListJson)
}
private fun setupWorkManagerObserve() {
mWorkManger
.getWorkInfosByTagLiveData(TAG)
.observe(viewLifecycleOwner) {
var successNumber = 0
var failureNumber = 0
it.forEach { work ->
when {
work.state == WorkInfo.State.SUCCEEDED && work.state.isFinished -> {
successNumber++
val image = work.outputData.getString(PHOTO)
val uploadedFileJson = work.outputData.getString(UPLOAD_RESPONSE)
mWorkRequestList.removeAll { it.id == work.id }
mImagesList.removeAll { it == image }
uploadedFileJson?.let {
imagesUploadViewModel.getDownloadUrlFromResponse(it)
}
imagesUploadBinding.uploadProgress.progress = successNumber + failureNumber
imagesUploadBinding.successfullyUpdated.text =
String.format(getString(R.string.succeeded_updates), successNumber)
}
work.state == WorkInfo.State.RUNNING -> {
}
work.state == WorkInfo.State.FAILED -> {
failureNumber++
val image = work.outputData.getString(PHOTO)
mWorkRequestList.removeAll { it.id == work.id }
mImagesList.removeAll { it == image }
imagesUploadBinding.uploadProgress.progress = successNumber + failureNumber
imagesUploadBinding.failuredUpdated.text =
String.format(getString(R.string.failed_updates), failureNumber)
imagesUploadViewModel.addFailure()
}
else -> {
}
}
}
}
}
private fun setupWorkerList() {
for (i in 0 until mImagesList.size) {
val work = OneTimeWorkRequestBuilder<ImagesUploadWorkManager>()
.addTag(TAG)
.setInputData(Data.Builder().putString(PHOTO, mImagesList[i])
.build())
.build()
mWorkRequestList.add(work)
}
mWorkManger.enqueue(mWorkRequestList)
}
}
My worker:
class ImagesUploadWorkManager(val context: Context, parameters: WorkerParameters) : RxWorker(context, parameters) {
companion object {
const val PHOTO = "PHOTO"
const val UPLOAD_RESPONSE = "UPLOAD_RESPONSE"
}
private val uploadFilesUseCase: UploadFilesUseCase by inject(UploadFilesUseCase::class.java)
private val gson: Gson by inject(Gson::class.java)
override fun createWork(): Single<Result> {
val imageStringUri = inputData.getString(PHOTO)
val imageUri = Uri.parse(imageStringUri)
return Single.fromObservable(
uploadFilesUseCase.buildObservable(UploadFilesUseCase.Params(imageUri))
.doOnError {
Timber.e(it)
}
.map {
Result.success(workDataOf(UPLOAD_RESPONSE to gson.toJson(it), PHOTO to imageStringUri))
}
.onErrorReturn {
Timber.e(it)
Result.failure(workDataOf(PHOTO to imageStringUri))
}
)
}
}
My UseCase:
class UploadFilesUseCase(
private val context: Context,
private val getServerUseCase: GetServerUseCase,
) : UseCase<UploadFilesUseCase.Params, GoFileDataEntity>() {
private val goFileApi: GoFileApi by inject(GoFileApi::class.java)
override fun buildObservable(param: Params): Observable<GoFileDataEntity> {
return Observable.just(param.imageUri)
.flatMap {
val file = createTempFile(it) ?: throw Exception()
Observable.just(file)
}.flatMap { imageFile ->
getServerUseCase.buildObservable(GetServerUseCase.Params())
.flatMap { server ->
goFileApi.uploadFile(
String.format(BuildConfig.api_gofile_upload, server.server),
MultipartBody.Part.createFormData("file", imageFile.name, createUploadRequestBody(imageFile, "image/jpeg"))
).toObservable()
}.map { it.goFileDataDTO.toEntity() }
}
}
private fun createUploadRequestBody(file: File, mimeType: String) =
file.asRequestBody(mimeType.toMediaType())
private fun createTempFile(uri: Uri): File? {
val tempFile = File.createTempFile(System.currentTimeMillis().toString().take(4), ".jpg", context.externalCacheDir)
val inputStream = context.contentResolver.openInputStream(uri) ?: return null
FileOutputStream(tempFile, false).use { outputStream ->
var read: Int
val bytes = ByteArray(DEFAULT_BUFFER_SIZE)
while (inputStream.read(bytes).also { read = it } != -1) {
outputStream.write(bytes, 0, read)
}
outputStream.close()
}
inputStream.close()
return tempFile
}
data class Params(val imageUri: Uri)
}
I had a similar problem, and it looks like the queue was full from previous Workmanager enqueuings.
I did following during application start and now it works fine:
WorkManager.getInstance(mContext).cancelAllWork();
I'm trying to get my sign up fragment to show an error if the passwords don't match but I only see the error that asks for alphanumeric characters (but even if I type that it doesn't seem to work).
What am I doing wrong?
This is the view model where I set up the conditions to raise an error if they aren't met:
class SignUpViewModel(private val signUpRepository: SignUpRepository) : ViewModel() {
private val _signUpForm = MutableLiveData<SignUpFormState>()
val signUpFormState: LiveData<SignUpFormState> = _signUpForm
fun signUp(name: String, email:String, password: String) {
}
fun signUpDataChanged(email:String, password: String, confirmPassword: String) {
if (!isEmailValid(email)) {
_signUpForm.value = SignUpFormState(emailError = R.string.invalid_email)
} else if (!isPasswordValid(password)) {
_signUpForm.value = SignUpFormState(passwordError = R.string.invalid_password_signUp)
} else if (!passwordsMatch(password,confirmPassword)){
_signUpForm.value = SignUpFormState(confirmPasswordError = R.string.mismatched_password)
}
else {
_signUpForm.value = SignUpFormState(isDataValid = true)
}
}
// A placeholder username validation check
private fun isEmailValid(email: String): Boolean {
return if (email.contains('#')) {
Patterns.EMAIL_ADDRESS.matcher(email).matches()
} else {
email.isNotBlank()
}
}
// A placeholder password validation check
private fun isPasswordValid(password: String): Boolean {
return password.contains("^(?=.*[a-zA-Z])(?=.*[0-9])[a-zA-Z0-9]+$")
}
//check if passwords are the same
private fun passwordsMatch(password: String, confirmPassword:String): Boolean{
return password == confirmPassword
}
}
This is the fragment:
class SignUpFragment : Fragment() {
private lateinit var binding: FragmentSignupBinding
private lateinit var signUpViewModel: SignUpViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentSignupBinding.inflate(inflater, container, false)
val nameInput = binding.etName
val emailInput = binding.etEmail
val passwordInput = binding.etPassword
val confirmPasswordInput = binding.etConfirmPassword
val signupButton = binding.btSignUp
signUpViewModel = ViewModelProvider(this, SignUpViewModelFactory())
.get(SignUpViewModel::class.java)
signUpViewModel.signUpFormState.observe(viewLifecycleOwner, Observer {
val signUpState = it ?: return#Observer
// disable sign up button unless fields are valid
signupButton.isEnabled = signUpState.isDataValid
if (signUpState.emailError != null) {
emailInput.error = getString(signUpState.emailError)
}
if (signUpState.passwordError != null) {
passwordInput.error = getString(signUpState.passwordError)
}
if (signUpState.confirmPasswordError != null) {
confirmPasswordInput.error = getString(signUpState.confirmPasswordError)
}
})
emailInput.afterTextChanged {
signUpViewModel.signUpDataChanged(
emailInput.text.toString(),
passwordInput.text.toString(),
confirmPasswordInput.text.toString()
)
}
passwordInput.afterTextChanged {
signUpViewModel.signUpDataChanged(
emailInput.text.toString(),
passwordInput.text.toString(),
confirmPasswordInput.text.toString()
)
}
confirmPasswordInput.apply {
afterTextChanged {
signUpViewModel.signUpDataChanged(
emailInput.text.toString(),
passwordInput.text.toString(),
confirmPasswordInput.text.toString()
)
}
setOnEditorActionListener { _, actionId, _ ->
when (actionId) {
EditorInfo.IME_ACTION_DONE ->
signUpViewModel.signUp(
emailInput.text.toString(),
passwordInput.text.toString(),
confirmPasswordInput.text.toString()
)
}
false
}
}
return binding.root
}
}
fun EditText.afterTextChanged(afterTextChanged: (String) -> Unit) {
this.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(editable: Editable?) {
afterTextChanged.invoke(editable.toString())
}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
})
}
String.contains matches substring not regex , for regex use
val regex = Regex("^(?=.*[a-zA-Z])(?=.*[0-9])[a-zA-Z0-9]+$")
assertTrue(regex.containsMatchIn("xabcdy"))
For your match function , try logging first then use String.trim() for unexpected spaces.
I have an application using databinding, livedata, room, kotlin koroutines, viewmodel, retrofit and koin. I have one activity, and two fragments.
UserListFragment: Show in a recyclerview a list of user items.
UserFullProfileFragment: Show the user item detail.
When the application is running, an external API is called to retrieve a list of users and display it in a recyclerview. Then, if I click on one item, an external API is called to get the detail of the current user whith its ID.
The problem is when I click on one item at the first, everything is going well but for following items, this is the detail of previous item which is displayed and so on and so forth.
Any ideas ?
UserRepository:
class UserRepositoryImpl (private val userApi: UserApi, private val userDao: UserDao, private val networkStateManager: NetworkStateManager) : UserRepository {
override suspend fun getUserList(): Result<List<UserListItem>> {
if (networkStateManager.hasNetWorkConnection()) {
return try {
// get user list from user API
val response = userApi.getUserList()
if (response.isSuccessful) {
Log.d("REPO", "get users from api")
response.body()?.let { userResponse ->
Log.d("REPO", "response:$response")
val userList = userResponse.data
// convert user API object to user entity
val entities = userList.map { it.toUserEntity() }
// save user list in database
withContext(Dispatchers.IO) { userDao.addUsers(entities) }
// convert user entity to user model
val userItemList = entities.map { it.toUserListItem() }
return Result.Success(userItemList)
} ?: handleFailure(response)
} else {
handleFailure(response)
}
} catch (e: Exception) {
return Result.Failure(e, e.localizedMessage)
}
} else {
// get user list from database if no network
val data = withContext(Dispatchers.IO) { userDao.findAllUsers() }
return if (data.isNotEmpty()) {
Log.d("REPO", "get users from db")
val userItemList = data.map { it.toUserListItem() }
Result.Success(userItemList)
} else {
Result.Failure(Exception("error"), "no network connection")
}
}
}
override suspend fun getUserFullProfile(userId: String): Result<UserFullProfile> {
if (networkStateManager.hasNetWorkConnection()) {
return try {
// get user from user API
val response = userApi.getUserFullProfile(userId)
if (response.isSuccessful) {
Log.d("REPO", "get users from api")
response.body()?.let { userResponse ->
Log.d("REPO", "response:$userResponse")
// convert user API object to user entity
val userEntity = userResponse.toUserEntity()
// save user data in database
withContext(Dispatchers.IO) { userDao.addUserFullProfile(userEntity) }
// convert user entity to user model
val user = userEntity.toUserFullProfile()
return Result.Success(user)
} ?: handleFailure(response)
} else {
handleFailure(response)
}
} catch (e: Exception) {
return Result.Failure(e, e.localizedMessage)
}
} else {
// get user from database if no network
val data = withContext(Dispatchers.IO) { userDao.getUserById(userId) }
return if (data != null) {
Log.d("REPO", "get users from db")
val user = data.toUserFullProfile()
Result.Success(user)
} else {
Result.Failure(Exception("error"), "no network connection")
}
}
}
UserViewModel:
getUserList and getUserFullProfile are use cases which call the repository
class UserViewModel (private val getUserList: GetUserList, private val getUserFullProfile: GetUserFullProfile) : ViewModel() {
val userList = MutableLiveData<List<UserListItem>>()
val userFullProfile = MutableLiveData<UserFullProfile>()
fun getUserList() {
viewModelScope.launch {
when (val result = getUserList.getUserList()) {
is Result.Success -> userList.value = result.successData
is Result.Failure -> result.exception.localizedMessage
}
}
}
fun getUserFullProfile(userId: String) {
viewModelScope.launch {
when (val result = getUserFullProfile.getUserFullProfile(userId)) {
is Result.Success -> userFullProfile.value = result.successData
is Result.Failure -> result.exception.localizedMessage
}
}
}
UserRecyclerAdaper:
class UserRecyclerAdapter(private val context: Context?, val clickListener: UserClickListener) : RecyclerView.Adapter<UserRecyclerAdapter.UserViewHolder>() {
var userList : List<UserListItem> = ArrayList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
val inflatedView: UserItemBinding = DataBindingUtil.inflate(LayoutInflater.from(parent.context), R.layout.user_item, parent, false)
return UserViewHolder(inflatedView)
}
override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
holder.bindUser(position)
}
override fun getItemCount() = userList.size
fun setUsers(users: List<UserListItem>) {
this.userList = users
notifyDataSetChanged()
}
inner class UserViewHolder(private val v: UserItemBinding) : RecyclerView.ViewHolder(v.root) {
fun bindUser(position: Int) {
val item = userList[position]
Log.d("ADAPTER", item.toString())
v.user = item
Picasso.get()
.load(item.picture)
.placeholder(R.drawable.ic_launcher_foreground)
.error(R.drawable.ic_launcher_background)
.into(v.picture)
v.userClickInterface = clickListener
v.root.setOnClickListener {
clickListener.onItemClick(item)
}
}
}
UserListFragment:
class UserListFragment : Fragment(), UserClickListener {
private val userViewModel by viewModel<UserViewModel>()
private lateinit var userAdapter: UserRecyclerAdapter
private lateinit var viewDataBinding: FragmentUserListBinding
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
viewDataBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_user_list, container, false)
viewDataBinding.lifecycleOwner = this
return viewDataBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
recyclerView.layoutManager = LinearLayoutManager(activity, LinearLayoutManager.VERTICAL, false)
userAdapter = UserRecyclerAdapter(context, this)
recyclerView.adapter = userAdapter
recyclerView.isNestedScrollingEnabled = false
viewDataBinding.viewModel = userViewModel
userViewModel.getUserList()
userViewModel.userList.observe(viewLifecycleOwner, { userList ->
if (userList.isNotEmpty() && userList != null) {
userAdapter.setUsers(userList)
}
})
}
override fun onItemClick(user: UserListItem) {
Log.d("FRAGMENT", user.toString())
userViewModel.getUserFullProfile(user.id)
userViewModel.userFullProfile.observe(viewLifecycleOwner, { userFullProfile ->
Log.d("UFP", userFullProfile.toString())
if (userFullProfile != null) {
(activity as MainActivity).replaceFragment(UserFullProfileFragment.newInstance(userFullProfile),
R.id.fragment_layout, "userFullProfile")
}
})
}
UserFullProfileFragment:
class UserFullProfileFragment : Fragment() {
companion object {
#JvmStatic
fun newInstance(user: UserFullProfile) = UserFullProfileFragment().apply {
arguments = Bundle().apply {
putParcelable("user", user)
}
}
}
private var user: UserFullProfile? = null
private lateinit var mViewDataBinding: FragmentUserFullProfileBinding
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mViewDataBinding.user = user
notify()
Picasso.get()
.load(mViewDataBinding.user?.picture)
.placeholder(R.drawable.ic_launcher_foreground)
.error(R.drawable.ic_launcher_background)
.into(mViewDataBinding.picture)
val dateOfBirth = parseDate(mViewDataBinding.user?.dateOfBirth)
mViewDataBinding.dateOfBirth.text = dateOfBirth
val registerDate = parseDate(mViewDataBinding.user?.registerDate)
mViewDataBinding.registerDate.text = registerDate
}
override fun onAttach(context: Context) {
super.onAttach(context)
user = arguments?.getParcelable("user")
Log.d("APP", user.toString())
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
mViewDataBinding = DataBindingUtil.inflate(inflater,
R.layout.fragment_user_full_profile, container, false)
mViewDataBinding.lifecycleOwner = this
return mViewDataBinding.root
}
Thank you :)
Finally, I found a solution :
I pass the user id argment from UserListFragment to UserFullProfileFragment instead of the current user object and I call the external API to get the current user in the UserFullProfileFragment.
This is the final code:
UserListFragment:
override fun onItemClick(user: UserListItem) {
val action = UserListFragmentDirections.actionUserListFragmentToUserFullProfileFragment(user.id)
findNavController().navigate(action)
}
UserFullProfileFragment:
class UserFullProfileFragment : Fragment() {
private lateinit var userID: String
private var mViewDataBinding: FragmentUserFullProfileBinding? = null
private val binding get() = mViewDataBinding!!
private val userViewModel by viewModel<UserViewModel>()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.viewModel = userViewModel
userID = UserFullProfileFragmentArgs.fromBundle(requireArguments()).userArgs
userViewModel.getUserFullProfile(userID)
userViewModel.userFullProfile.observe(viewLifecycleOwner, { userFullProfile ->
if (userFullProfile != null) {
binding.user = userFullProfile
Picasso.get()
.load(binding.user?.picture)
.placeholder(R.drawable.ic_launcher_foreground)
.error(R.drawable.ic_launcher_background)
.into(binding.picture)
val dateOfBirth = parseDate(binding.user?.dateOfBirth)
binding.dateOfBirth.text = dateOfBirth
val registerDate = parseDate(binding.user?.registerDate)
binding.registerDate.text = registerDate
}
})
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
mViewDataBinding = DataBindingUtil.inflate(inflater,
R.layout.fragment_user_full_profile, container, false)
binding.lifecycleOwner = this
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
mViewDataBinding = null
}
}
I am trying to display data from IconFinder API. It seems to be ItemKeyedDataSource for me and I used Paging3 to display the data as it's mentioned in the official docs.
Here is code, please check if there're any issues with the implementation I have done and where is the mistake.
IconSetsRemoteMediator
#OptIn(ExperimentalPagingApi::class)
class IconSetsRemoteMediator(
private val query: String?,
private val database: IconsFinderDatabase,
private val networkService: IconFinderAPIService
) : RemoteMediator<Int, IconSetsEntry>() {
private val TAG: String? = IconSetsRemoteMediator::class.simpleName
private val iconSetsDao = database.iconSetsDao
private val remoteKeysDao = database.remoteKeysDao
override suspend fun initialize(): InitializeAction {
// Load fresh data when ever the app is open new
return InitializeAction.LAUNCH_INITIAL_REFRESH
}
override suspend fun load(
loadType: LoadType,
state: PagingState<Int, IconSetsEntry>
): MediatorResult {
val iconSetID = when (loadType) {
LoadType.REFRESH -> {
null
}
LoadType.PREPEND -> {
return MediatorResult.Success(
endOfPaginationReached = true
)
}
LoadType.APPEND -> {
Log.d(TAG, "LoadType.APPEND")
val lastItem = state.lastItemOrNull()
if (lastItem == null) {
return MediatorResult.Success(
endOfPaginationReached = true
)
}
// Get the last item from the icon-sets list and return its ID
lastItem.iconset_id
}
}
try {
// Suspending network load via Retrofit.
val response = networkService.getAllPublicIconSets(after = iconSetID)
val iconSets = response.iconsets
val endOfPaginationReached = iconSets == null || iconSets.isEmpty()
database.withTransaction {
if (loadType == LoadType.REFRESH) {
// Delete the data in the database
iconSetsDao.deleteAllIconSets()
//remoteKeysDao.deleteRemoteKeys()
}
Log.d(TAG, "iconSets = ${iconSets?.size}")
Log.d(TAG, "endOfPaginationReached = $endOfPaginationReached")
Log.d(TAG, "state.anchorPosition = ${state.anchorPosition}")
Log.d(TAG, "state.pages = ${state.pages.size}")
val time = System.currentTimeMillis()
/*val remoteKeys = iconSets!!.map {
RemoteKeysEntry(it.iconset_id, time)
}*/
// Insert new IconSets data into database, which invalidates the current PagingData,
// allowing Paging to present the updates in the DB.
val data = iconSets!!.mapAsIconSetsEntry()
iconSetsDao.insertAllIconSets(data)
// Insert the remote key values which set the time at which the data is
// getting updated in the DB
//remoteKeysDao.insertRemoteKeys(remoteKeys)
}
return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached)
} catch (exception: IOException) {
return MediatorResult.Error(exception)
} catch (exception: HttpException) {
return MediatorResult.Error(exception)
}
}
}
IconFinderRepository
class IconFinderRepository(
private val service: IconFinderAPIService,
private val database: IconsFinderDatabase
) {
private val TAG: String? = IconFinderRepository::class.simpleName
fun getPublicIconSets(): Flow<PagingData<IconSetsEntry>> {
Log.d(TAG, "New Icon Sets query")
val pagingSourceFactory = { database.iconSetsDao.getIconSets() }
#OptIn(ExperimentalPagingApi::class)
return Pager(
config = PagingConfig(pageSize = NUMBER_OF_ITEMS_TO_FETCH, enablePlaceholders = false),
remoteMediator = IconSetsRemoteMediator(
query = null,
database,
service
),
pagingSourceFactory = pagingSourceFactory
).flow
}
companion object {
const val NUMBER_OF_ITEMS_TO_FETCH = 20
}
}
IconSetViewHolder
class IconSetViewHolder private constructor(val binding: RecyclerItemIconSetBinding) :
RecyclerView.ViewHolder(binding.root) {
fun bind(iconSetsEntry: IconSetsEntry?) {
if (iconSetsEntry == null) {
//Show the Loading UI
} else {
binding.model = iconSetsEntry
binding.executePendingBindings()
}
}
companion object {
fun from(parent: ViewGroup): IconSetViewHolder {
val layoutInflater = LayoutInflater.from(parent.context)
val binding = RecyclerItemIconSetBinding.inflate(layoutInflater, parent, false)
return IconSetViewHolder(binding)
}
}
}
IconSetAdapter
class IconSetAdapter : PagingDataAdapter<UiModel.IconSetDataItem, ViewHolder>(UI_MODEL_COMPARATOR) {
companion object {
private val UI_MODEL_COMPARATOR =
object : DiffUtil.ItemCallback<UiModel.IconSetDataItem>() {
override fun areContentsTheSame(
oldItem: UiModel.IconSetDataItem,
newItem: UiModel.IconSetDataItem
): Boolean {
return oldItem.iconSetsEntry.iconset_id == newItem.iconSetsEntry.iconset_id
}
override fun areItemsTheSame(
oldItem: UiModel.IconSetDataItem,
newItem: UiModel.IconSetDataItem
): Boolean =
oldItem == newItem
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return if (viewType == R.layout.recycler_item_icon_set) {
IconSetViewHolder.from(parent)
} else {
IconSetViewHolder.from(parent)
}
}
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is UiModel.IconSetDataItem -> R.layout.recycler_item_icon_set
null -> throw UnsupportedOperationException("Unknown view")
else -> throw UnsupportedOperationException("Unknown view")
}
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val uiModel = getItem(position)
uiModel.let {
when (uiModel) {
is UiModel.IconSetDataItem -> (holder as IconSetViewHolder).bind(uiModel.iconSetsEntry)
}
}
}
}
HomeFragmentViewModel
class HomeFragmentViewModel(application: Application) : AndroidViewModel(application) {
private val TAG: String? = HomeFragmentViewModel::class.simpleName
private val repository: IconFinderRepository = IconFinderRepository(
IconFinderAPIService.create(),
IconsFinderDatabase.getInstance(application)
)
private var iconSetsQueryResult: Flow<PagingData<UiModel.IconSetDataItem>>? = null
fun iconSetsQuery(): Flow<PagingData<UiModel.IconSetDataItem>> {
val newResult: Flow<PagingData<UiModel.IconSetDataItem>> = repository.getPublicIconSets()
.map { pagingData -> pagingData.map { UiModel.IconSetDataItem(it) } }
.cachedIn(viewModelScope)
iconSetsQueryResult = newResult
return newResult
}
/**
* Factory for constructing HomeFragmentViewModel
*/
class Factory(private val application: Application) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(HomeFragmentViewModel::class.java)) {
return HomeFragmentViewModel(application) as T
}
throw IllegalArgumentException("Unable to construct ViewModel")
}
}
}
sealed class UiModel {
data class IconSetDataItem(val iconSetsEntry: IconSetsEntry) : UiModel()
}
IconSetFragment: This is one of the fragments implemented as part of ViewPager. Its parent is a Fragment in an Activity.
class IconSetFragment : Fragment() {
private val TAG: String = IconSetFragment::class.java.simpleName
/**
* Declaring the UI Components
*/
private lateinit var binding: FragmentIconSetBinding
private val viewModel: HomeFragmentViewModel by viewModels()
private val adapter = IconSetAdapter()
private var job: Job? = null
companion object {
fun newInstance(): IconSetFragment {
return IconSetFragment()
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Get a reference to the binding object
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_icon_set, container, false)
Log.d(TAG, "onCreateView")
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initAdapter()
job?.cancel()
job = viewLifecycleOwner.lifecycleScope.launch {
viewModel.iconSetsQuery().collectLatest {
adapter.submitData(it)
Log.d(TAG, "collectLatest $it")
}
}
}
private fun initAdapter() {
binding.rvIconSetList.adapter = adapter
/*.withLoadStateHeaderAndFooter(
header = LoadStateAdapter(), // { adapter.retry() },
footer = LoadStateAdapter { adapter.retry() }
)*/
}
}
IconSetsDao
#Dao
interface IconSetsDao {
#Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAllIconSets(iconSets: List<IconSetsEntry>)
#Query("SELECT * FROM icon_sets_table")
fun getIconSets(): PagingSource<Int, IconSetsEntry>
#Query("DELETE FROM icon_sets_table")
suspend fun deleteAllIconSets()
}
This is the Logcat screenshot, the load() method is being invoked without any scrolling action.
I have the similar issue, seems the recursive loading issue is fixed by setting the recyclerView.setHasFixedSize(true)