0

While writing an Android app, I encountered a problem with a stuttering animation. I use AHBottomNavigation for navigation, FragNav is for swapping fragments and FlexibleAdapter for RecyclerView. The application is built from one activity and five fragments. When I try to switch to the first fragment in the application, the BottomNavigation animation freez for a moment. It looks very unsightly. The second time I choose the same fragment, everything works smoothly. It seems to me that it is the fault to initialize the views in the fragment, but I have no idea how to do it differently.

AHBottomNavigation https://github.com/aurelhubert/ahbottomnavigation
FragNav https://github.com/ncapdevi/FragNav
FlexibleAdapter https://github.com/davideas/FlexibleAdapter

Fragment

class GradeFragment : BaseFragment(), GradeView {

@Inject
lateinit var presenter: GradePresenter

private val gradeAdapter = FlexibleAdapter<AbstractFlexibleItem<*>>(null, null, true)

companion object {
    fun newInstance() = GradeFragment()
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
    return inflater.inflate(R.layout.fragment_grade, container, false)
}

override fun onActivityCreated(savedInstanceState: Bundle?) {
    super.onActivityCreated(savedInstanceState)
    presenter.run {
        attachView(this@GradeFragment)
        loadData()
    }
}

override fun initView() {
    gradeAdapter.run {
        isAutoCollapseOnExpand = true
        isAutoScrollOnExpand = true
        setOnUpdateListener { presenter.onUpdateDataList(it) }
        setOnItemClickListener { position ->
            getItem(position).let {
                if (it is GradeItem) {
                    GradeDialog.newInstance(it.grade).show(fragmentManager, it.toString())
                }
            }
        }
    }
    gradeRecycler.run {
        layoutManager = SmoothScrollLinearLayoutManager(context)
        adapter = gradeAdapter
    }
    gradeSwipe.setOnRefreshListener { presenter.loadData(forceRefresh = true) }
}

override fun updateData(data: List<GradeHeader>) {
    gradeAdapter.updateDataSet(data, true)
}

override fun isViewEmpty(): Boolean = gradeAdapter.isEmpty

override fun showEmptyView(show: Boolean) {
    gradeEmpty.visibility = if (show) VISIBLE else GONE
}

override fun showProgress(show: Boolean) {
    gradeProgress.visibility = if (show) VISIBLE else GONE
}

override fun setRefresh(show: Boolean) {
    gradeSwipe.isRefreshing = show
}

Presenter

class GradePresenter @Inject constructor(
    private val errorHandler: ErrorHandler,
    private val schedulers: SchedulersManager,
    private val gradeRepository: GradeRepository,
    private val sessionRepository: SessionRepository) : BasePresenter<GradeView>(errorHandler) {

override fun attachView(view: GradeView) {
    super.attachView(view)
    view.initView()
}

fun loadData(forceRefresh: Boolean = false) {
    disposable.add(sessionRepository.getSemesters()
            .map { it.single { semester -> semester.current } }
            .flatMap { gradeRepository.getGrades(it, forceRefresh) }
            .map { it.groupBy { grade -> grade.subject } }
            .map { createGradeItems(it) }
            .subscribeOn(schedulers.backgroundThread())
            .observeOn(schedulers.mainThread())
            .doFinally { view?.setRefresh(false) }
            .doOnSuccess { if (it.isEmpty()) view?.showEmptyView(true) }
            .doOnError { view?.run { if (isViewEmpty()) showEmptyView(true) } }
            .subscribe({ view?.updateData(it) }) { errorHandler.proceed(it) })
}

private fun createGradeItems(items: Map<String, List<Grade>>): List<GradeHeader> {
    return items.map {
        val gradesAverage = calcAverage(it.value)
        GradeHeader().apply {
            subject = it.key
            average = view?.run {
                if (gradesAverage == 0f) emptyAverageString()
                else averageString().format(gradesAverage)
            }.orEmpty()
            number = view?.gradeNumberString(it.value.size).orEmpty()
            subItems = (it.value.map { item ->
                GradeItem().apply {
                    grade = item
                    weightString = view?.weightString().orEmpty()
                    valueColor = getValueColor(item.value)
                }
            })
        }
    }
}

fun onUpdateDataList(size: Int) {
    if (size != 0) view?.showProgress(false)
}
Faierbel
  • 51
  • 1
  • 5
  • Try to pin-point the call causing the lag by clamping them with time-measuring logs. Common offenders might be your `loadData` or adapters `onBindViewHolder` – Pawel Aug 29 '18 at 19:18
  • `loadData` is performed in a different thread and should not affect the ui thread. `onBindViewHolder` is very simple, I set only the already-tempered text, with no logic. I used Androi Profiller to see what happens when I click and here is the result https://ibb.co/cys0oU – Faierbel Aug 30 '18 at 09:24
  • After several days of searching for the cause, I probably found the culprit. The jam occurs when the android inflates the fragment view. The view is simple but it contains a Recycler View and it causes the jam. After removing it from xml, swapping fragments went smoothly. However, this is not a solution because I need a Recyler View in this view. – Faierbel Sep 01 '18 at 14:36

1 Answers1

0

After a few days, I managed to solve the problem by updating the SDK to version 28. RecyclerView no longer causes animation jams when inflating

Faierbel
  • 51
  • 1
  • 5