Thursday, 25 August 2022

What could be causing Android (Kotlin) Fragments visual glitchs (fragments overlap on nagivate)?

I have a fragments displaying lists of items and it's forms to populate the lists. When i go to the list fragment to the form fragment using findNavController().navigate() the form fragment is shown only on the 50% of the screen, after a couple seconds it finishs showing the other 50%. If i scroll the screen the fragments shows it´s 100% instantly, if i idle and do not touch the screen it takes several seconds. In some of the fragment transacction the glitch is not random, meaning i can reproduce it every time i make that findNavController().navigate(). But there are other cases where the glitch happens some times and some other times it does´t.

This is happening in emulator and in real devices also.

This is how it looks like (this is the one i can reproduce) here Another example 3rd example

Top half is my form fragment. Bottom half is the list fragment that haven´t updated, the form fragment should be 100% of the container i am not spliting this into 2 fragments in the same container one top of another.

This is how i nav from list fragment to form fragment. Backwards i use the generated backwards button at the left top corner.

    findNavController().navigate(
        R.id.nav_form_automovil,
        args = b
    )

I may add that i am using abstract generic clases that i created to reutilize code. The glitchs started happening around that time.

/*
* Params:
*  id: layout res
* Generics:
*  T: data entity
*  B: ViewBinding
*  F: Field Validator
* */
abstract class BaseListItemFragment<T, B : ViewBinding, F : BaseFormState>(@LayoutRes id: Int) :
    BaseItemFragment<T, B, F>(id) {
    private var _recyclerView: RecyclerView? = null
    private var _adapter: BaseAdapter<T>? = null

    fun initRecycler(recyclerView: RecyclerView, adapter: BaseAdapter<T>) {
        _recyclerView = recyclerView
        _recyclerView?.layoutManager =
            LinearLayoutManager(
                MainApplication.instance.applicationContext,
                LinearLayoutManager.VERTICAL,
                false
            )
        _adapter = adapter
        _recyclerView?.adapter = _adapter
    }

    private fun observeItemList() =
        viewModel?.itemList?.observe(viewLifecycleOwner, Observer {
            val itemList = it ?: return@Observer
            viewModel?.updateIsLoading(false)
            if (itemList.isNotEmpty()) {
                _adapter?.updateModels(itemList)
                _recyclerView?.smoothScrollToPosition(0)
            }
        })

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        observeItemList()
    }

    override fun onResume() {
        super.onResume()
        viewModel?.fetchAndObserve()
    }
}


abstract class BaseItemFragment<T, B : ViewBinding, F : BaseFormState>(@LayoutRes id: Int) :
    Fragment(id) {
    private var _binding: B? = null
    val binding get() = _binding
    var viewModel: BaseItemViewModel<T, F>? = null

    fun inflateLayout(viewBinding: B) {
        _binding = viewBinding
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        provideViewModels()
        return super.onCreateView(inflater, container, savedInstanceState)
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        enableLoadingScreen()
        observeLogoutFlag()
        setUpListeners(view)
    }

    private fun observeLogoutFlag() =
        viewModel?.refreshTokenExpired?.observe(viewLifecycleOwner, Observer {
            val isExpired = it ?: return@Observer
            if (isExpired) {
                activity?.finish()
            }
        })

    abstract fun enableLoadingScreen()
    abstract fun setUpListeners(view: View)
    abstract fun provideViewModels()

    //**  Extension functions start here  **//
    /**
     * Extension function to simplify setting an afterTextChanged action to EditText components.
     */
    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) {}
        })
    }

    fun Int.toDp(context: Context): Int = TypedValue.applyDimension(
        TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), context.resources.displayMetrics
    ).toInt()
}

Provided a viewModel instance my fragments usually performs 1 or 2 or 3 network operations with IO dispatcher, most common case

override fun onResume() {
    super.onResume()
    viewModel?.fetchAndObserve()
}

Implementations look like this.

class AutomovilListFragment() :
    BaseListItemFragment<Automovil, FragmentListAutomovilBinding, AutomovilFormState>(
        R.layout.fragment_list_automovil
    ) {
override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        super.onCreateView(inflater, container, savedInstanceState)
        inflateLayout(FragmentListAutomovilBinding.inflate(inflater, container, false))
        initRecycler(
            binding!!.listAuto,
            AutomovilAdapter(ArrayList(), WeakReference(this))
        )
        return binding!!.root
    }

    override fun setUpListeners(view: View) {

    }

    override fun enableLoadingScreen() {
        viewModel?.isLoading?.observe(viewLifecycleOwner, Observer {
            val isLoading = it ?: return@Observer
            if (isLoading) binding!!.pBarListAutomovil.visibility = View.VISIBLE
            else binding!!.pBarListAutomovil.visibility = View.INVISIBLE
        })
    }

    override fun provideViewModels() {
        viewModel =
            ViewModelProvider(
                requireActivity(),
                AutomovilViewModelFactory()
            )[AutomovilViewModel::class.java]
    }
}

Do anyone see what could be the root cause of this and what route take to find a solution?



from What could be causing Android (Kotlin) Fragments visual glitchs (fragments overlap on nagivate)?

No comments:

Post a Comment