How can I handle the state value on change without using Composable in Kotlin? - android

I have a VM and a fragment. I want to handle the change of state.value in the fragment. How can I do that. Here is the fragment:
AndroidEntryPoint
class ImportFragment : Fragment() {
private val importVM by viewModels<ImportFragmentVM>()
lateinit var importButton: Button
lateinit var createButton: Button
lateinit var tab: TabLayout
lateinit var createItems: Group
lateinit var importItems: Group
lateinit var longUrl: EditText
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.import_fragment, container, false)
importButton = view.findViewById(R.id.importButton)
createButton = view.findViewById(R.id.createButton)
tab = view.findViewById(R.id.tab)
createItems = view.findViewById(R.id.createGroup)
importItems = view.findViewById(R.id.importGroup)
longUrl = view.findViewById(R.id.longurl)
createItems.isVisible = false
Timber.d("tab position:" + tab.selectedTabPosition)
tab.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
when (tab.position) {
0 -> {
createItems.isVisible = false
importItems.isVisible = true
Timber.d("position 0")
}
1 -> {
createItems.isVisible = true
importItems.isVisible = false
Timber.d("position 1")
}
}
}
override fun onTabUnselected(tab: TabLayout.Tab?) {
}
override fun onTabReselected(tab: TabLayout.Tab?) {
}
})
longUrl.addTextChangedListener(object : TextWatcher {
override fun afterTextChanged(s: Editable) {}
override fun beforeTextChanged(
s: CharSequence, start: Int,
count: Int, after: Int
) {
}
override fun onTextChanged(
s: CharSequence, start: Int,
before: Int, count: Int
) {
importVM.updateInput(longUrl.text.toString())
}
})
createButton.setOnClickListener {
val x = importVM.submit()
if (x is ImportFragmentVM.ImportUIState.Success)
Timber.d("assadads")
}
return view
}
}
And here is the VM for this fragment:
#HiltViewModel
class ImportFragmentVM #Inject constructor(
private val service: UrlService
) : ViewModel() {
sealed interface ImportUIState {
data class PendingUserInput(val longUrl: String) : ImportUIState
object Loading : ImportUIState
object Success : ImportUIState
data class PartialSuccess(val urlKey: UrlKey, val urlApiKey: UrlApiKey) : ImportUIState
object Error : ImportUIState
object Invalid : ImportUIState
}
val _state = MutableStateFlow<ImportUIState>(PendingUserInput(""))
val state: StateFlow<ImportUIState> = _state
fun updateInput(longUrl: String) {
check(_state.value is PendingUserInput) { "You can't be in update" }
Timber.d("longurl:" + longUrl)
_state.value = PendingUserInput(longUrl)
}
fun submit():ImportUIState {
val longUrl = state.value.run {
check(this is PendingUserInput) { "You can't be in update when submitting" }
longUrl
}
Timber.d("sunt in submit")
_state.value = Loading
Timber.d("value is" + state.value)
viewModelScope.launch(Dispatchers.IO) {
val result = service.shortenUrl(longUrl)
if (result is ShortenRequestOutcome.Failed)
_state.value = Error
if (result is ShortenRequestOutcome.Invalid)
_state.value = Invalid
if (result is ShortenRequestOutcome.Success)
_state.value = Success
}
return state.value
}
}
For example I want to show a dialog/toast based on the state.value, and I don't know how to find out in the fragment when the value has changed. Any ideas?

To collect your flow in a Fragment, you can use:
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
importVM.state.collect { data ->
//do something
}
}
}

Related

Problem with local database and remote database data when updating local data

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

Data From API Has Not Appeared Using Paging 3

I'm learning paging 3, but the data from the API doesn't appear. My code is like below:
interface PokeAPI {
#GET("pokemon")
fun getPokemonList() : Call<PokemonList>
#GET("pokemon")
fun getAllPokemon(
#Query("limit") limit: Int,
#Query("offset") offset: Int) : PokemonList
#GET("pokemon/{name}")
fun getPokemonInfo(
#Path("name") name: String
) : Call<Pokemon>
}
class PokePagingSource(private val apiService: PokeAPI): PagingSource<Int, Result>() {
private companion object {
const val INITIAL_PAGE_INDEX = 1
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Result> {
return try {
val position = params.key ?: INITIAL_PAGE_INDEX
val responseData = apiService.getAllPokemon(position, params.loadSize)
if (responseData.results.isEmpty()) {
Log.e("Response Succeed!", responseData.results.toString())
} else {
Log.e("Response Failed!", responseData.results.toString())
}
LoadResult.Page(
data = responseData.results,
prevKey = if (position == INITIAL_PAGE_INDEX) null else position - 1,
nextKey = if (responseData.results.isNullOrEmpty()) null else position + 1
)
} catch (exception: Exception) {
return LoadResult.Error(exception)
}
}
override fun getRefreshKey(state: PagingState<Int, Result>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
}
class PokemonRepository(private val apiService: PokeAPI) {
fun getAllPokemon(): LiveData<PagingData<Result>>{
return Pager(
config = PagingConfig(
pageSize = 10
),
pagingSourceFactory = {
PokePagingSource(apiService)
}
).liveData
}
}
object Injection {
private val api by lazy { RetrofitClient().endpoint }
fun provideRepository(): PokemonRepository {
return PokemonRepository(api)
}
}
class PokemonViewModel(pokemonRepository: PokemonRepository) : ViewModel() {
val allPokemonList: LiveData<PagingData<Result>> =
pokemonRepository.getAllPokemon().cachedIn(viewModelScope)
}
class ViewModelFactory : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(PokemonViewModel::class.java)) {
#Suppress("UNCHECKED_CAST")
return PokemonViewModel(Injection.provideRepository()) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
`class PokemonPagingAdapter(private val context: Context) :
PagingDataAdapter<Result, PokemonPagingAdapter.ViewHolder>(DIFF_CALLBACK) {
private var onItemClick: OnAdapterListener? = null
fun setOnItemClick(onItemClick: OnAdapterListener) {
this.onItemClick = onItemClick
}
class ViewHolder(val binding: AdapterPokemonBinding) : RecyclerView.ViewHolder(binding.root) {
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return ViewHolder(
AdapterPokemonBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val pokemonData = getItem(position)
if (pokemonData != null) {
holder.binding.apply {
val number = if (pokemonData.url.endsWith("/")) {
pokemonData.url.dropLast(1).takeLastWhile { it.isDigit() }
} else {
pokemonData.url.takeLastWhile { it.isDigit() }
}
val url = "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/${number}.png"
Glide.with(context)
.load(url)
.transition(DrawableTransitionOptions.withCrossFade())
.centerCrop()
.circleCrop()
.into(ivPokemon)
tvNamePokemon.text = pokemonData.name
btnDetail.setOnClickListener {
onItemClick?.onClick(pokemonData, pokemonData.name, url)
}
}
}
}
companion object {
val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Result>() {
override fun areItemsTheSame(
oldItem: Result,
newItem: Result
): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(
oldItem: Result,
newItem: Result
): Boolean {
return oldItem.name == newItem.name
}
}
}
interface OnAdapterListener {
fun onClick(data: Result, name: String, url: String)
}
}`
class FragmentPokemon: Fragment(R.layout.fragment_pokemon) {
private var _binding : FragmentPokemonBinding? = null
private val binding get() = _binding!!
private lateinit var dataPagingAdapter: PokemonPagingAdapter
private val viewModel: PokemonViewModel by viewModels {
ViewModelFactory()
}
private lateinit var comm: Communicator
override fun onStart() {
super.onStart()
getData()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
_binding = FragmentPokemonBinding.bind(view)
val toolBar = requireActivity().findViewById<View>(R.id.tool_bar)
toolBar.visibility = View.VISIBLE
val navBar = requireActivity().findViewById<BottomNavigationView>(R.id.bottom_navigation)
navBar.visibility = View.VISIBLE
comm = requireActivity() as Communicator
setupListPokemon()
}
private fun setupListPokemon(){
dataPagingAdapter = PokemonPagingAdapter(requireContext())
dataPagingAdapter.setOnItemClick(object: PokemonPagingAdapter.OnAdapterListener{
override fun onClick(data: Result, name: String, url: String) {
comm.passDataCom(name, url)
}
})
binding.apply {
rvPokemon.layoutManager = LinearLayoutManager(context)
rvPokemon.setHasFixedSize(true)
rvPokemon.adapter = dataPagingAdapter
}
}
private fun getData(){
viewModel.allPokemonList.observe(viewLifecycleOwner){
dataPagingAdapter.submitData(lifecycle, it)
binding.btnCoba.setOnClickListener { btn ->
if (it == null){
Log.e("ResponseFailed", it.toString())
} else Log.e("ResponseSucceed", it.toString())
}
}
}
}
What's the reason? I have followed the step by step implementation of paging 3 but the data still doesn't appear either.
I don't know the API you are using, but it seems that you are using it incorrectly. The getAllPokemon method has limit and offset parameters and you are calling it like apiService.getAllPokemon(position, params.loadSize), so you are using position as a limit and params.loadSize as an offset.
You should pass params.loadSize as a limit, rename INITIAL_PAGE_INDEX to INITIAL_OFFSET and set it to 0, since your API uses offsets instead of pages (at least it seems so from what you provided). The load function should then look something like this:
// get current offset
val offset = params.key ?: INITIAL_OFFSET
val responseData = apiService.getAllPokemon(limit = params.loadSize, offset = offset)
val prevKey = offset - params.loadSize
val nextKey = offset + params.loadSize

RecyclerView doesn't appear in a Fragment

Why doesn't RecyclerView appear in my fragment? I've added recyclerview adapter in the fragment, but it still didn't appear. Here are the codes:
FollowersFragment.kt
class FollowersFragment : Fragment() {
private lateinit var binding: FragmentFollowersBinding
companion object {
private const val TAG = "FollowersFragment"
const val ARG_NAME = "userName"
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.fragment_followers, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding = FragmentFollowersBinding.inflate(layoutInflater)
val username = arguments?.getString(ARG_NAME)
val layoutManager = LinearLayoutManager(requireActivity())
binding.rvFollowers.layoutManager = layoutManager
val itemDecoration = DividerItemDecoration(requireActivity(), layoutManager.orientation)
binding.rvFollowers.addItemDecoration(itemDecoration)
val client = ApiConfig.getApiService().getFollowers(username.toString(),"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")
client.enqueue(object : Callback<List<FollowsResponseItem>> {
override fun onResponse(
call: Call<List<FollowsResponseItem>>,
response: Response<List<FollowsResponseItem>>
) {
if(response.isSuccessful){
val responseBody = response.body()
if(responseBody!=null){
Log.d(TAG,responseBody.toString())
setUserData(responseBody)
}else{
Log.e(TAG,"onFailure: ${response.message()}")
}
}
}
override fun onFailure(call: Call<List<FollowsResponseItem>>, t: Throwable) {
Log.e(TAG, "onFailure: ${t.message}")
}
})
}
fun setUserData(item: List<FollowsResponseItem>){
val listUser = ArrayList<UserResponse>()
val executor = Executors.newSingleThreadExecutor()
executor.execute {
try {
for (i in 0..item.size-1) {
if(item.size>5 && i>5){
break
}
val client = ApiConfig.getApiService()
.getUser(item.get(i).login, "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")
client.enqueue(object : Callback<UserResponse> {
override fun onResponse(
call: Call<UserResponse>,
response: Response<UserResponse>
) {
if (response.isSuccessful) {
val responseBody = response.body()
if (responseBody != null) {
listUser.add(responseBody)
if(i==4 || item.get(i).login.equals(item.get(item.size-1).login)){
Log.d(TAG,"user : $listUser")
val adapter = ListUserAdapter(listUser)
binding.rvFollowers.adapter = adapter
Log.d(TAG,adapter.toString())
adapter.setOnItemClickCallback(object: ListUserAdapter.OnItemClickCallback{
override fun onItemClicked(data: UserParcelable) {
showSelectedUser(data)
}
})
}
} else {
Log.e(TAG, "onFailure: ${response.message()}")
}
}
}
override fun onFailure(call: Call<UserResponse>, t: Throwable) {
Log.e(TAG, "onFailure: ${t.message}")
}
})
}
} catch(e: InterruptedException) {
e.printStackTrace()
}
}
}
private fun showSelectedUser(data: UserParcelable) {
}
}
DetailActivity.kt
class DetailActivity : AppCompatActivity() {
private lateinit var binding: ActivityDetailBinding
private var getUserName: String ="sidiqpermana"
companion object{
const val EXTRA_DATA = "extra_data"
#StringRes
private val TAB_TITLES = intArrayOf(
R.string.tab_text_1,
R.string.tab_text_2
)
}
#SuppressLint("SetTextI18n")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
supportActionBar?.hide()
binding = ActivityDetailBinding.inflate(layoutInflater)
setContentView(binding.root)
val data = intent.getParcelableExtra<UserParcelable>(EXTRA_DATA) as UserParcelable
val sectionsPagerAdapter = SectionsPagerAdapter(this)
val viewPager: ViewPager2 = binding.viewPager
viewPager.adapter = sectionsPagerAdapter
val tabs: TabLayout = binding.tabs
sectionsPagerAdapter.userName = data.login
TabLayoutMediator(tabs,viewPager){ tab, position ->
tab.text = resources.getString(TAB_TITLES[position])
}.attach()
getUserName = data.login
showInfo(data)
}
fun getUserName() : String{
return getUserName
}
#SuppressLint("SetTextI18n")
private fun showInfo(data: UserParcelable){
Glide.with(this#DetailActivity)
.load(data.avatar_url)
.into(binding.detailPp)
if(data.name.equals("null")) binding.detailName.setText("No Name") else binding.detailName.setText(data.name)
binding.detailUsername.setText(data.login)
if(data.bio.equals("null")) binding.detailBio.setText("No Name") else binding.detailBio.setText(data.bio)
binding.detailFollowers.setText("${data.followers} Followers")
binding.detailFollowings.setText("${data.following} Following")
if(data.location.equals("null")) binding.detailLocation.setText("No Location") else binding.detailLocation.setText(data.location)
}
}
ListUserAdapter.kt (RecyclerView Adapter)
class ListUserAdapter (private val listUser: ArrayList<UserResponse>) : RecyclerView.Adapter<ListUserAdapter.ListViewHolder>() {
private lateinit var onItemClickCallback: OnItemClickCallback
fun setOnItemClickCallback(onItemClickCallback: OnItemClickCallback){
this.onItemClickCallback = onItemClickCallback
}
class ListViewHolder(var binding: ItemUsersRowBinding) : RecyclerView.ViewHolder(binding.root) {
}
override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ListViewHolder {
val binding = ItemUsersRowBinding.inflate(LayoutInflater.from(viewGroup.context),viewGroup,false)
return ListViewHolder(binding)
}
#SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
var (followers,avatar_url,following, name,bio, location, login) = listUser[position]
name = name ?: "No Name"
bio = bio ?: "No Bio"
location = location?: "No Location"
holder.apply {
Glide.with(itemView.getContext())
.load(avatar_url)
.into(binding.photoProfile)
binding.profileName.setText(name.toString())
binding.username.setText(login)
binding.followers.setText("$following Followers")
binding.followings.setText("$following Followings")
binding.location.setText(location.toString())
val detailUser = UserParcelable(followers,avatar_url,following,
name.toString(), bio.toString(), location.toString(), login)
itemView.setOnClickListener{ onItemClickCallback.onItemClicked(detailUser)}
}
}
override fun getItemCount(): Int {
return listUser.size
}
interface OnItemClickCallback {
fun onItemClicked(data: UserParcelable)
}
}
Help me solve this problem please.
There is no need to initialize adapter every time you want to update the list. Either make your ListUserAdapter extend ListAdapter and than use adapter.submitList(listUser) or if you want to extend RecyclerView.Adapter as you do, you can do the following :
class ListUserAdapter () : RecyclerView.Adapter<ListUserAdapter.ListViewHolder>() {
private val listUser: List<UserResponse>
fun submitList(newList: List<UserResponse>) {
listUser = newList
notifyDataSetChanged()
}
override fun onBindViewHolder(holder: ListViewHolder, position: Int) {
val listItem = listUser[position]
...
}
override fun getItemCount(): Int {
return listUser.size
}
}
I suggest you go with ListAdapter. Check if Log.d(TAG,"user : $listUser") is printed, if it is and listUser is not empty than call adapter.submitList(listUser) and RV should be populated.
You have missed to notify adapter about the changes, So after
binding.rvFollowers.adapter = adapter call adapter.notifyDataSetChanged()

Raise error if passwords don't match on sign up

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.

onDetach() not calling in moving from one fragment to another

When i get values back from FragB(A bottomsheetfragment launched from the toolbar of the ativity) to FragA(from viewpager of activity) and call the method of FragA via interface to hit the API's again with the new data it gives me the following exception:
java.lang.IllegalStateException: Can't create ViewModelProvider for detached fragment
Here after getting the data from FragB to activity i am calling method of FragA from activity by using:
val getFragment = pagerAdapter.getItem(viewPager.currentItem)
if(getFragment is GrowthStoryFragment){
getFragment.myfilterOptions(countryId, dateRange, specs)
So after reading from this SO thread it says go for a null check or re-initialize the viewModel in onAttach but the strange behavior here is that no lifecycle method of FragA is being called when i launch the bottomsheetfragment(FragB) from the toolbar of activity and when i dismiss() the FragB on a button click then only the lifecycle methods of FragB are being called and again no lifecycle method of viewpager fragment FragA are calling so when the fragment got detached and now from where i should re-initialize the viewModel and other instances?
Please help me to understand this scenario.
Update:
Here's the Activity code:
class ViewDetailsActivity : BaseActivity(), FilterOptionsDialogFragment.onApplyEventListener, BusinessUnitDialogFragment.afterDoneClick {
private lateinit var pagerAdapter: ViewDetailsFragmentAdapter
private var myFragmentFlag = 0
private var TAG = "ViewDetailsActivity"
override fun getContentView(): Int = R.layout.activity_view_details
override fun onViewReady(savedInstanceState: Bundle?, intent: Intent?) {
tabLayout!!.addTab(tabLayout!!.newTab().setText("Growth Story"))
tabLayout!!.addTab(tabLayout!!.newTab().setText("Share Story"))
tabLayout!!.addTab(tabLayout!!.newTab().setText("Purchase Dynamics"))
tabLayout!!.addTab(tabLayout!!.newTab().setText("Brand Health Track"))
tabLayout.tabMode = TabLayout.MODE_SCROLLABLE
tabLayout.tabGravity = Gravity.CENTER
tabLayout.setTabTextColors(Color.parseColor("#a1a1a1"), Color.parseColor("#ff8a00"))
pagerAdapter = ViewDetailsFragmentAdapter(supportFragmentManager, tabLayout!!.tabCount)
viewPager.adapter = pagerAdapter
viewPager.isFocusableInTouchMode = true
scrollListener()
viewPager!!.addOnPageChangeListener(TabLayout.TabLayoutOnPageChangeListener(tabLayout))
tabLayout!!.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
viewPager!!.currentItem = tab.position
when (tab.position) {
0 -> {
myFragmentFlag = 0
left_scroll.visibility = View.GONE
right_scroll.visibility = View.VISIBLE
}
1 -> {
myFragmentFlag = 1
right_scroll.visibility = View.VISIBLE
}
2 -> {
myFragmentFlag = 2
left_scroll.visibility = View.VISIBLE
right_scroll.visibility = View.VISIBLE
}
3 -> {
myFragmentFlag = 3
left_scroll.visibility = View.VISIBLE
right_scroll.visibility = View.GONE
}
}
}
override fun onTabUnselected(tab: TabLayout.Tab) {
}
override fun onTabReselected(tab: TabLayout.Tab) {
}
})
tvTitle.setTextSize(TypedValue.COMPLEX_UNIT_PX, getResources().getDimension(R.dimen._16ssp))
tvTitle.setText("Category Deep Dive")
ivBack.setOnClickListener(View.OnClickListener {
finish()
})
ivLogo.setOnClickListener(View.OnClickListener {
val addFrag = FilterOptionsDialogFragment.newInstance()
addFrag.show(supportFragmentManager, "add")
})
vd_edit_icon.setOnClickListener({
val buFrag = BusinessUnitDialogFragment.newInstance()
buFrag.show(supportFragmentManager, "add")
})
vd_edit_icon.visibility = View.VISIBLE
}
fun scrollListener() {
left_scroll.setOnClickListener(View.OnClickListener {
val itemnum = viewPager.currentItem
viewPager.currentItem = itemnum - 1
})
right_scroll.setOnClickListener(View.OnClickListener {
val itemnum = viewPager.currentItem
viewPager.currentItem = itemnum + 1
})
}
override fun someEvent(countryId: String?, dateRange: String?, specs: String?) {
val getFragment = pagerAdapter.getItem(viewPager.currentItem)
if(getFragment is GrowthStoryFragment){
getFragment.myfilterOptions(countryId, dateRange, specs)
}else if(getFragment is ShareStoryFragment){
getFragment.myfilterOptions(countryId, dateRange, specs)
}else if(getFragment is PurchaseDynamicsFragment){
getFragment.myfilterOptions(countryId, dateRange, specs)
}else if(getFragment is BrandHealthFragment){
getFragment.myfilterOptions(countryId, dateRange, specs)
}
}
override fun onDoneClicked(item: String) {
val getFragment = pagerAdapter.getItem(viewPager.currentItem)
if(getFragment is GrowthStoryFragment){
getFragment.getBuName(item)
}else if(getFragment is ShareStoryFragment){
getFragment.getBuName(item)
}else if(getFragment is PurchaseDynamicsFragment){
getFragment.getBuName(item)
}else if(getFragment is BrandHealthFragment){
getFragment.getBuName(item)
}
}
}
Fragment Adapter:
import android.support.v4.app.Fragment
import android.support.v4.app.FragmentManager
import android.support.v4.app.FragmentStatePagerAdapter
class ViewDetailsFragmentAdapter(supportFragmentManager: FragmentManager,internal var totalTabs: Int): FragmentStatePagerAdapter(supportFragmentManager) {
override fun getItem(position: Int): Fragment? {
when (position) {
0 -> return GrowthStoryFragment()
1 -> return ShareStoryFragment()
2 -> return PurchaseDynamicsFragment()
3 -> return BrandHealthFragment()
else -> return null
}
}
// this counts total number of tabs
override fun getCount(): Int {
return totalTabs
}
}
Fragment A:
class GrowthStoryFragment : Fragment() {
private val TAG = "GrowthStoryFragment"
private lateinit var disposable : Disposable
private lateinit var responseSpinner : List<RespCat>
private lateinit var responseFirstBarChart : List<RespBrand>
private lateinit var RespDon : List<RespDon>
private lateinit var responseSecondBarChart : List<RespDist>
companion object{
private lateinit var myApplicationContext : Context
private var countryID = "1"
private var date = "MAT TY"
private var spec = "val"
private var businessUnitID = "2"
private var category = "Fresh Milk"
private var firstReportTypeId = "1" //fixed for growth story and share story
private var isGroup = "false" //fixed to false
}
private lateinit var userModel : UserViewModel
private val backendApi = WinRetrofitHelper.winApiInstance()
override fun onAttach(context: Context?) {
super.onAttach(context)
Log.e(TAG, "OnAttach")
userModel = ViewModelProviders.of(this)[UserViewModel::class.java]
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?): View? {
Log.e(TAG, "OnCreateView")
return inflater.inflate(R.layout.fragment_growth_story, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Log.e(TAG, "OnViewCreated")
myApplicationContext = context!!.applicationContext
getSpinnerResponse(businessUnitID, isGroup,firstReportTypeId)
// getSuperRegionName(countryID, date,spec," ",businessUnitID, category, firstReportTypeId, isGroup)
growth_spinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener{
override fun onNothingSelected(parent: AdapterView<*>?) {
}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
val item = parent?.getItemAtPosition(position) as RespCat
category = item.nameValue
Log.e(TAG,"Category name is: " + category)
getSuperRegionName(countryID, date,spec," ",businessUnitID, category, firstReportTypeId, isGroup)
}
}
}
private fun getSpinnerResponse(businessUnitID: String, isGroup: String, firstReportTypeId: String){
userModel.getResponseGrowthSpinner(businessUnitID, isGroup, firstReportTypeId)
userModel.responseGrowthSpinner.observe(this,
Observer {
Utils.debugger("FRAG ", "$it")
growth_spinner.adapter = GrowthSpinnerAdapter(it)
})
}
private fun getSuperRegionName(countryID: String, date: String, spec: String, superMarket: String,businessUnitID: String, category: String, firstReportTypeId: String, isGroup: String) {
userModel.getResponseSuperRegion(countryID)
userModel.responseSuperRegion.observe(this,
Observer {
Utils.debugger("FRAG ", "$it")
getDataFromApi(countryID, date, spec, it!!.get(0).nameValue, businessUnitID, category, firstReportTypeId, isGroup)
})
}
private fun getColorID(position: Int): Int {
try {
val rnd = Random
when (position) {
0 -> return R.color.brand_almarai
1 -> return R.color.brand_alsafi
2 -> return R.color.brand_nadec
3 -> return R.color.brand_sadafco
4 -> return R.color.brand_nestle
5 -> return R.color.brand_amul
6 -> return R.color.brand_nada
}
return Color.argb(255, rnd.nextInt(256), rnd.nextInt(256), rnd.nextInt(256))
}catch (e :Exception){
e.printStackTrace()
}
return 1
}
fun myfilterOptions(countryId: String?, dateRange: String?, specs: String?){
userModel = ViewModelProviders.of(this)[UserViewModel::class.java]
getSuperRegionName(countryId!!,dateRange!!,specs!!.toLowerCase()," ",businessUnitID, category, firstReportTypeId, isGroup)
Log.e(TAG, "Growth Story Fragment:" +countryId!!+" "+dateRange!!+" "+specs!!.toLowerCase()+ " "+businessUnitID+ " "+category+ " "+firstReportTypeId+ " "+isGroup)
}
override fun onDestroy() {
super.onDestroy()
Log.e(TAG, "Ondestroy")
disposable.dispose()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.e(TAG, "OnCreate")
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
Log.e(TAG, "OnActivitycreated")
}
override fun onPause() {
super.onPause()
Log.e(TAG, "OnPause")
}
override fun onStart() {
super.onStart()
Log.e(TAG, "OnStart")
}
override fun onResume() {
super.onResume()
Log.e(TAG, "OnResume")
}
override fun onStop() {
super.onStop()
Log.e(TAG, "OnStop")
}
override fun onDestroyView() {
super.onDestroyView()
Log.e(TAG, "Ondestroyview")
}
override fun onDetach() {
super.onDetach()
Log.e(TAG, "OnDetach")
}
}
FragB code:
class FilterOptionsDialogFragment : BottomSheetDialogFragment(), View.OnClickListener {
private var myResp: List<RespBu>? = null
private lateinit var myView: View
private lateinit var customList: ArrayList<RespBu>
private var dateRange: String = ""
private var specrange: String = ""
private lateinit var onmyApplyEventListener: onApplyEventListener
private var TAG = "FilterOptionsDialogFragment"
val pref: AppPreference by lazy {
AppPreference.getInstance(context!!)
}
companion object {
fun newInstance(): FilterOptionsDialogFragment {
return FilterOptionsDialogFragment()
}
}
override fun onCreateView(inflater: LayoutInflater,
#Nullable container: ViewGroup?,
#Nullable savedInstanceState: Bundle?): View? {
myView = inflater.inflate(R.layout.filter_options_layout, container, false)
Log.e(TAG, "OnCreateView")
// get the views and attach the listener
val backendApi = WinRetrofitHelper.winApiInstance()
val request = backendApi.getBUCountry()
request.enqueue(object : Callback<List<RespBu>> {
override fun onFailure(call: Call<List<RespBu>>?, t: Throwable?) {
}
override fun onResponse(call: Call<List<RespBu>>?, response: Response<List<RespBu>>?) {
val spinner = myView.findViewById<Spinner>(R.id.filter_options_spinner)
spinner.adapter = Spinner_filter_options(getNewList(response?.body()))
if(pref.getString("BuID") != null && !pref.getString("BuID").equals("")){
if(pref.getBoolean(Site.BUSINESS_UNIT_FRONT)!=null && pref.getBoolean(Site.BUSINESS_UNIT_FRONT))
filter_options_spinner.setSelection(pref.getString("BuID").toInt()-1)
else
filter_options_spinner.setSelection(pref.getString("BuID").toInt()-1)
}
}
})
return myView
}
override fun onStart() {
super.onStart()
Log.e(TAG, "OnStart")
}
private fun getNewList(mylist: List<RespBu>?): List<RespBu> {
if(pref.getBoolean(Site.BUSINESS_UNIT_FRONT)!=null && pref.getBoolean(Site.BUSINESS_UNIT_FRONT))
return mylist!!
else{
customList = ArrayList()
customList.add(RespBu("0", 0, "Global", 0))
for (item in mylist.orEmpty()) {
customList.add(item)
}
return customList
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
try {
if (pref.getString("dateName") != null && !pref.getString("dateName").equals("")) {
if (pref.getString("dateName").equals("YTD"))
date_ytd.isChecked = true
else
date_mat.isChecked = true
}
if (pref.getString("specName") != null && !pref.getString("specName").equals("")) {
if (pref.getString("specName").equals("VAL"))
spec_val.isChecked = true
else
spec_vol.isChecked = true
}
val dateradiogroup = view.findViewById<RadioGroup>(R.id.date_radio_group)
val specradiogroup = view.findViewById<RadioGroup>(R.id.spec_radio_group)
view.findViewById<ImageView>(R.id.view_close).setOnClickListener(View.OnClickListener {
dismiss()
})
view.findViewById<Button>(R.id.apply).setOnClickListener(View.OnClickListener {
val dateRadioBtn = view.findViewById<RadioButton>(dateradiogroup.checkedRadioButtonId)
val specRadioBtn = view.findViewById<RadioButton>(specradiogroup.checkedRadioButtonId)
val respBu = view.findViewById<Spinner>(R.id.filter_options_spinner).selectedItem as RespBu
val buName = respBu.keyValue.toString()
val dateName = dateRadioBtn.text.toString()
val specName = specRadioBtn.text.toString()
pref.saveString("BuID", filter_options_spinner.selectedItemId.toString())
pref.saveString("dateName", dateName)
pref.saveString("specName", specName)
pref.saveString("BuName", respBu.keyValue.toString())
onmyApplyEventListener.someEvent(buName, dateName + " TY", specName)
Log.e("Filter item", respBu.nameValue + " " + dateRadioBtn.text)
dismiss()
})
view.findViewById<Spinner>(R.id.filter_options_spinner).onItemSelectedListener = object : OnItemSelectedListener {
override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
}
override fun onNothingSelected(parent: AdapterView<*>?) {
}
}
}catch (e: Exception){
e.printStackTrace()
}
}
override fun onClick(p0: View?) {
}
interface onApplyEventListener {
fun someEvent(countryId: String?, dateRange: String?, specs: String?)
}
override fun onAttach(activity: Activity?) {
super.onAttach(activity)
Log.e(TAG, "OnAttach")
onmyApplyEventListener = activity as onApplyEventListener
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Log.e(TAG, "onCreate")
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
Log.e(TAG, "OnActivitycreated")
}
// override fun onAttach(context: Context?) {
// super.onAttach(context)
// onmyApplyEventListener = context as onApplyEventListener
// Log.e(TAG, "OnAttach")
// }
override fun onPause() {
super.onPause()
Log.e(TAG, "OnPause")
}
override fun onResume() {
super.onResume()
Log.e(TAG, "OnResume")
}
override fun onStop() {
super.onStop()
Log.e(TAG, "OnStop")
}
override fun onDestroyView() {
super.onDestroyView()
Log.e(TAG, "Ondestroyview")
}
override fun onDestroy() {
super.onDestroy()
Log.e(TAG, "Ondestroy")
}
override fun onDetach() {
super.onDetach()
Log.e(TAG, "OnDetach")
}
}
Ok. I really think I have it this time:
ViewDetailsFragmentAdapter#getItem is returning a fresh instance every
time. When you later call #getItem, you're getting an un-initialized fragment instance that is also not currently attached to any Activity. As a result, nothing you do will get what you're looking for. By making sure you hand back the exact same instance each time for a given page type, you should be safe.
You have mentioned that FragmentManager#getFragments returns a list that has the fragment you had initialized earlier. You can use this to your advantage by getting the fragment you want by type from the fragments that the given FragmentManager knows about:
class ViewDetailsFragmentAdapter(supportFragmentManager: FragmentManager,internal var totalTabs: Int): FragmentStatePagerAdapter(supportFragmentManager) {
override fun getItem(position: Int): Fragment? {
return when (position) {
0 -> existing<GrowthStoryFragment>() ?: GrowthStoryFragment()
1 -> existing<ShareStoryFragment>() ?: ShareStoryFragment()
2 -> existing<PurchaseDynamicsFragment>() ?: PurchaseDynamicsFragment()
3 -> existing<BrandHealthFragment>() ?: BrandHealthFragment()
else -> return null
}
}
private inline fun <reified T> existing(): T? =
supportFragmentManager.getFragments().firstOrNull { it is T } as T?
SparseArray is just a Map<Int, ?> that's suggested by Android. You can instead track the list of fragments you've handed out inside your adapter instance in one. The upside here is that it's theoretically more performant, and you're keeping knowledge local. The theoretical downside is that you're holding onto framework-managed objects with a potentially different scope than the framework uses.
class ViewDetailsFragmentAdapter(supportFragmentManager: FragmentManager,internal var totalTabs: Int): FragmentStatePagerAdapter(supportFragmentManager) {
private val pages: SparseArray<Fragment> by lazy(:: { SparseArray(totalTabs) }
override fun getItem(position: Int): Fragment? {
return pages.get(position) ?:
when (position) {
0 -> GrowthStoryFragment()
1 -> ShareStoryFragment()
2 -> PurchaseDynamicsFragment()
3 -> BrandHealthFragment()
else -> null
}.also { pages.put(position, it) }
}
ViewModels should be initiated for application context. Like this:
someViewModel = activity.let { ViewModelProvider(requireActivity()).get(SomeViewModel::class.java) }
Although you did not paste proper part of code - I suspect you are tying ViewModel lifecycle to fragment - which you get by requireContext() inside fragment.
Try with requireActivity()
update since code is provided:
userModel = ViewModelProviders.of(this)[UserViewModel::class.java]
this - is reference to Fragment. What I wrote by blind guessing is correct. Use requireActivity()

Categories

Resources