Search code examples
androidkotlinrepository-patternandroid-databindingandroid-mvvm

How can I update the UI without skipping frames


So I am developing and android app in Kotlin with coroutines and no matter what change I make, I still keep getting the message:

I/Choreographer: Skipped 59 frames!  The application may be doing too much work on its main thread.

How can I get rid of it. I mean like I am only displaying nine photos... Below is my code

Model:

data class Food (
    val id: String,
    val name: String,
    val price: String,
    @Json(name = "img_url") val imgSrcUrl: String,
    val type: String,
    val description: String,
    val average_rating: String,
    val number_of_raters: String,
    val special_price: String
)
data class FoodCategory(
    val id: String,
    val title: String,
    val foods: List<Food>
)

ViewModel:

enum class NetworkStatus {LOADING, DONE, FAILED}

enum class FontFamily (@FontRes val fontRes: Int) {
    POPPINS_BOLD(R.font.poppins_bold),
    POPPINS(R.font.poppins)
}

class FoodOverviewViewModel(private val foodRepository: FoodRepository): ViewModel() {

    private lateinit var foodProducts: List<Food>

    //This is the data that is gonna be exposed to the viewmodel
    //It will be submitted to a ListAdapter
    private val _foodCategory = MutableLiveData<List<FoodCategory>>()
    val foodCategory: LiveData<List<FoodCategory>>
        get() = _foodCategory
    
    //Used to display a progress bar for network status
    private val _status = MutableLiveData<NetworkStatus>()
    val status: LiveData<NetworkStatus>
        get() = _status

    init {
        getOverviewProducts()
    }

    private fun getOverviewProducts() {
        viewModelScope.launch(Dispatchers.Default) {
            _status.postValue(NetworkStatus.LOADING)
            try {
                getUpdatedFood()
                Log.i("getOverviewProducts","I am running on tread: $coroutineContext")
                _status.postValue(NetworkStatus.DONE)

            }catch (e: Exception) {
                _status.postValue(NetworkStatus.FAILED)
            }
        }
    }

    private suspend fun getUpdatedFood() {
        //withContext(Dispatchers.Default) {
            val limiter = 6 //Number of items I want to get from the server
            val foodCategory = arrayListOf<FoodCategory>()
            Log.i("getUpdatedFood","I am running on tread: $coroutineContext")

            val getRecommended = foodRepository.getRecommendedFood(limiter.toString())
            foodCategory += FoodCategory(id = 0.toString(), title = "Recommended for you", foods = getRecommended)
            
            val getSpecials = foodRepository.getSpecials(limiter.toString())
            foodCategory += FoodCategory(id = 1.toString(), title = "Specials", foods = getSpecials)

            _foodCategory.postValue(foodCategory)
        //}
    }
}

Repository:

class FoodRepository {

    suspend fun getRecommendedFood(limiter: String) = withContext(Dispatchers.IO) {
        Log.i("Resp-getRecommended","I am running on tread: $coroutineContext")
        return@withContext ProductApi.retrofitService.getRecommended(limiter)
    }
    suspend fun getSpecials(limiter: String) = withContext(Dispatchers.IO) {
        Log.i("Resp-getSpecials","I am running on tread: $coroutineContext")
        return@withContext ProductApi.retrofitService.getSpecials(limiter)
    }
}




BindingAdapters:


//Load image using Glide (in Food item recycleview)
@BindingAdapter("imageUrl")
fun bindImage(imgView: ImageView , imgUrl: String?) {
    imgUrl?.let {
        val imgUri = imgUrl.toUri().buildUpon().scheme("http").build()
        Glide.with(imgView.context)
            .load(imgUri)
            .apply(
                RequestOptions()
                .placeholder(R.drawable.loading_animation)
                .error(R.drawable.ic_broken_image))
            .into(imgView)
    }
}

//set raters count (in Food item recycleview)
@BindingAdapter("ratersCount")
fun bindText(txtView: TextView, number_of_raters: String?) {
    number_of_raters?.let {
        val ratersCount = "(${number_of_raters})"
        txtView.text = ratersCount
    }
}

//update the progressbar visibilty (in outer-parent recycleview) 
@BindingAdapter("updateStatus")
fun ProgressBar.updateStatus(status: NetworkStatus?) {
    visibility = when (status) {
        NetworkStatus.LOADING -> View.VISIBLE
        NetworkStatus.DONE -> View.GONE
        else -> View.GONE
    }
}

//Hide or view an imageview based in the network Status. When network Error, an error image
//will show (in outer-parent recycleview)
@BindingAdapter("setNoInternet")
fun ImageView.setNoInternet(status: NetworkStatus?) {
    when(status) {
        NetworkStatus.LOADING -> {
            visibility = View.GONE
        }
        NetworkStatus.DONE -> {
            visibility = View.GONE
        }
        NetworkStatus.FAILED -> {
            visibility = View.VISIBLE
            setImageResource(R.drawable.ic_connection_error)
        }
    }
}

//Submit the list of FoodCatergory item to the outer-parent recycleview
@BindingAdapter("listData")
fun bindRecyclerView(recyclerView: RecyclerView, data: List<FoodCategory>?) {
    (recyclerView.adapter as FoodCategoryAdapter).submitList(data)
}

//Submit list the the Food item recyclew view (child recycleView)
@BindingAdapter("setProducts")
fun RecyclerView.setProducts(foods: List<Food>?) {
    if (foods != null) {
        val foodAdapter = FoodItemAdapter()
        foodAdapter.submitList(foods)

        adapter = foodAdapter
    }
}

I have a Recycleview of Food Item and a Recycleview Pool of FoodCategory. If I comment out _foodCategory.postValue(foodCategory) in ViewModel: getUpdatedFood() than I do not get the message. However, when I submit the list to the outer recycleview (The one the hold the viewpool), than I get this answer. Please help. I been stuck on it for a while tryna get rid of that message.

Thank you..

Updated Below is the adapeters and its view holders

FoodItem layout

<layout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>

        <import type="android.view.View"/>
        <variable
                name="foodItem"
                type="com.example.e_commerceapp.models.Food"/>
        <variable
                name="font"
                type="com.example.e_commerceapp.products.overview.FontFamily"/>
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
            android:id="@+id/child_item_main_layout"
            android:background="@drawable/search_background"
            android:layout_marginTop="10dp"
            android:layout_marginStart="10dp"
            android:layout_marginBottom="10dp"
            android:layout_width="150dp"
            android:layout_height="250dp">

        <ImageView
                android:layout_width="120dp"
                android:layout_marginStart="10dp"
                android:layout_marginEnd="10dp"
                android:id="@+id/burger_image"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                android:layout_height="160dp"
                />
<!--        app:imageUrl="@{foodItem.imgSrcUrl}"-->

        <TextView
                android:layout_width="match_parent"
                android:layout_height="34dp"
                android:layout_marginStart="5dp"
                android:id="@+id/burger_title"
                app:layout_constraintStart_toStartOf="parent"
                android:layout_marginEnd="5dp"
                android:singleLine="true"
                android:textColor="#B4000000"
                app:layout_constraintTop_toBottomOf="@id/burger_image"
                android:text="@{foodItem.name}"
                android:textSize="12sp"
                android:fontFamily="@font/poppins"/>
        <TextView
                android:layout_width="wrap_content"
                android:layout_height="35dp"
                app:layout_constraintTop_toBottomOf="@id/burger_title"
                android:layout_marginEnd="5dp"
                android:gravity="center"
                android:id="@+id/burger_price"
                android:layout_marginStart="5dp"
                app:layout_constraintStart_toEndOf="@id/special_price"
                android:textColor="#D0000000"/>

<!--                app:price="@{foodItem.price}"-->
<!--                app:specialPrice="@{foodItem.special_price}"-->
<!--                app:fontRes="@{foodItem.special ? font.POPPINS : font.POPPINS_BOLD}"-->


        <TextView
                android:layout_width="wrap_content"
                android:layout_height="35dp"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@id/burger_title"
                android:layout_marginEnd="5dp"
                android:gravity="center_vertical"
                android:layout_marginStart="5dp"
                android:id="@+id/special_price"
                android:textColor="#D0000000"
                android:visibility="gone"
                android:textStyle="normal"
                android:fontFamily="@font/poppins_bold"/>
<!--        app:setSpecialPrice="@{foodItem.special_price}"-->


        <ImageView
                android:layout_width="15dp"
                android:layout_height="15dp"
                app:layout_constraintTop_toBottomOf="@id/burger_price"
                android:src="@drawable/ic_baseline_star_24"
                android:visibility="@{foodItem.hasRating ? View.GONE : View.VISIBLE}"
                android:id="@+id/rating_star"
                app:layout_constraintStart_toStartOf="parent"
                android:layout_marginStart="5dp"
                android:layout_marginBottom="10dp"
                app:layout_constraintBottom_toBottomOf="parent"/>
        <TextView
                android:layout_width="wrap_content"
                android:layout_height="15dp"
                android:layout_marginStart="5dp"
                android:gravity="center"
                android:textSize="12sp"
                android:visibility="@{foodItem.hasRating ? View.GONE : View.VISIBLE}"
                android:id="@+id/rating_count"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintTop_toBottomOf="@id/burger_price"
                android:text="@{foodItem.average_rating}"
                android:layout_marginBottom="10dp"
                app:layout_constraintStart_toEndOf="@id/rating_star"/>
        <TextView
                android:layout_width="wrap_content"
                android:layout_height="15dp"
                android:id="@+id/number_of_raters"
                android:textSize="12sp"
                android:visibility="@{foodItem.hasRating ? View.GONE : View.VISIBLE}"
                android:layout_marginStart="2dp"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toEndOf="@id/rating_count"
                app:ratersCount="@{foodItem.number_of_raters}"
                android:layout_marginBottom="10dp"
                app:layout_constraintTop_toBottomOf="@id/burger_price"/>

        <ImageView android:layout_width="20dp"
                   android:layout_height="20dp"
                   android:layout_marginEnd="10dp"
                   android:layout_marginTop="10dp"
                   app:layout_constraintEnd_toEndOf="parent"
                   app:layout_constraintTop_toTopOf="parent"/>


    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

FoodItem Adapter

class FoodItemAdapter: ListAdapter<Food ,
        FoodItemAdapter.ItemFoodViewHolder>(DiffCallback) {

    override fun onCreateViewHolder(parent: ViewGroup , viewType: Int): ItemFoodViewHolder {
        return ItemFoodViewHolder(
            FoodItemBinding.inflate(LayoutInflater.from(parent.context),
            parent, false))
    }

    override fun onBindViewHolder(holder: ItemFoodViewHolder , position: Int) {
        val currentFood = getItem(position)
        holder.bind(currentFood)
    }

    class ItemFoodViewHolder(private var binding: FoodItemBinding): RecyclerView.ViewHolder(binding.root) {
        fun bind(food: Food) {
            binding.foodItem = food
            binding.executePendingBindings()
        }
    }

    object DiffCallback: DiffUtil.ItemCallback<Food>() {
        override fun areItemsTheSame(oldItem: Food , newItem: Food): Boolean {
            return oldItem === newItem
        }

        override fun areContentsTheSame(oldItem: Food , newItem: Food): Boolean {
            return oldItem.id == newItem.id
        }
    }
}

FoodCategory layout

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools">
    <data>
        <variable
                name="foodCategory"
                type="com.example.e_commerceapp.models.FoodCategory"/>
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
            android:background="#fff"
            android:layout_marginTop="5dp"
            android:layout_marginBottom="5dp"
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

        <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginStart="16dp"
                android:id="@+id/category_title"
                android:layout_marginTop="16dp"
                android:text="@{foodCategory.title}"
                android:textColor="#2B2A2A"
                android:fontFamily="@font/poppins_bold"
                android:textSize="16sp"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"/>

        <androidx.recyclerview.widget.RecyclerView
                android:id="@+id/nestedRecyclerView"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="4dp"
                app:setProducts="@{foodCategory.foods}"
                android:orientation="horizontal"
                app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toBottomOf="@+id/category_title"
                tools:itemCount="4"
                tools:listitem="@layout/food_item"/>

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

FoodCategory Adapter

class FoodCategoryAdapter: ListAdapter<FoodCategory,
        FoodCategoryAdapter.CategoryFoodViewHolder>(Companion) {

    private val viewPool = RecyclerView.RecycledViewPool()

    override fun onCreateViewHolder(parent: ViewGroup , viewType: Int): CategoryFoodViewHolder {
        return CategoryFoodViewHolder(FoodCategoryBinding.inflate(LayoutInflater.from(parent.context),
            parent, false))
    }

    override fun onBindViewHolder(holder: CategoryFoodViewHolder , position: Int) {
        val currentFoodCategory = getItem(position)
        holder.bind(currentFoodCategory)
    }


    inner class CategoryFoodViewHolder(private var binding: FoodCategoryBinding): RecyclerView.ViewHolder(binding.root) {
        fun bind(currentFoodCategory: FoodCategory?) {
            binding.foodCategory = currentFoodCategory
            binding.nestedRecyclerView.setRecycledViewPool(viewPool)
            binding.executePendingBindings()
        }
    }

    companion object: DiffUtil.ItemCallback<FoodCategory>() {
        override fun areItemsTheSame(oldItem: FoodCategory , newItem: FoodCategory): Boolean {
            return oldItem === newItem
        }

        override fun areContentsTheSame(oldItem: FoodCategory, newItem: FoodCategory): Boolean {
            return oldItem.id == newItem.id
        }
    }
}

The parent recycleView

<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        tools:context=".products.overview.FoodOverviewFragment">
    <data>
        <variable
                name="foodOverview"
                type="com.example.e_commerceapp.products.overview.FoodOverviewViewModel"/>

    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
            android:background="@color/grey"
            android:layout_width="match_parent"
            android:layout_height="match_parent">

        <RelativeLayout
                android:layout_width="match_parent"
                android:id="@+id/relative_layout"
                android:layout_height="105dp"
                android:elevation="8dp"
                android:layout_marginBottom="5dp"
                android:background="#fff"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintTop_toTopOf="parent"
                app:layout_constraintEnd_toEndOf="parent">

            <ImageView
                    android:layout_width="200dp"
                    android:layout_marginTop="10dp"
                    android:layout_height="35dp"
                    android:id="@+id/logo_and_name"
                    android:src="@drawable/compony_logo_and_name"
                    android:layout_alignParentStart="true"/>
            <ImageView
                    android:layout_width="wrap_content"
                    android:layout_height="35dp"
                    android:layout_marginTop="10dp"
                    android:id="@+id/notifications"
                    android:src="@drawable/ic_baseline_notifications_24"
                    android:layout_alignParentEnd="true"
                    android:paddingEnd="20dp"
                    android:paddingStart="20dp"/>
            <TextView
                    android:layout_width="match_parent"
                    android:id="@+id/search"
                    android:layout_marginTop="10dp"
                    android:layout_marginStart="20dp"
                    android:layout_marginEnd="20dp"
                    android:background="@drawable/search_background"
                    android:layout_below="@id/logo_and_name"
                    android:gravity="center_vertical"
                    android:layout_height="match_parent"
                    android:layout_marginBottom="10dp"
                    android:paddingEnd="10dp"
                    android:paddingStart="10dp"
                    android:text="@string/search_text"
                    tools:ignore="RtlSymmetry"
                    app:drawableEndCompat="@drawable/ic_baseline_search_24"/>
        </RelativeLayout>

        <ProgressBar
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:updateStatus="@{foodOverview.status}"
                app:layout_constraintTop_toBottomOf="@id/relative_layout"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                android:id="@+id/progressbar"/>
        <ImageView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                app:layout_constraintTop_toBottomOf="@id/relative_layout"
                app:layout_constraintBottom_toBottomOf="parent"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                android:id="@+id/noInternetImage"/>

        <androidx.recyclerview.widget.RecyclerView
                android:layout_width="0dp"
                android:id="@+id/foodCategory"
                android:clipToPadding="false"
                tools:itemCount="4"
                tools:listitem="@layout/food_category"
                app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
                android:layout_height="0dp"
                app:listData="@{foodOverview.foodCategory}"
                app:layout_constraintTop_toBottomOf="@id/relative_layout"
                app:layout_constraintStart_toStartOf="parent"
                app:layout_constraintEnd_toEndOf="parent"
                app:layout_constraintBottom_toBottomOf="parent"/>

    </androidx.constraintlayout.widget.ConstraintLayout>

</layout>

Solution

  • The skipping frames likely has nothing to do with the code you posted: it sounds like a misconfiguration of RecyclerViews / Adapters to me. You'll need to post that code for it be more clear though.

    However, even though what you posted likely isn't the culprit you can still optimize the coroutines code you have:

    class FoodOverviewViewModel(private val foodRepository: FoodRepository): ViewModel() {
    
        private lateinit var foodProducts: List<Food>
    
        private val _foodCategory = MutableLiveData<List<FoodCategory>>()
        val foodCategory: LiveData<List<FoodCategory>>
            get() = _foodCategory
        
        private val _status = MutableLiveData<NetworkStatus>()
        val status: LiveData<NetworkStatus>
            get() = _status
    
        init {
            getOverviewProducts()
        }
    
        private fun getOverviewProducts() {
            viewModelScope.launch { // <------- Don't apply a custom scope here
                _status.value = NetworkStatus.LOADING // <--- Don't call "postValue" here
                try {
                    val food = getUpdatedFood() // <------ This is already using a background dispatcher
                    _foodCategory.value = food // <------- Emit this value here
                    _status.value = NetworkStatus.DONE
                } catch (e: Exception) {
                    _status.value = NetworkStatus.FAILED
                }
            }
        }
    
        private suspend fun getUpdatedFood(): List<FoodCategory> { // <---- Return a value here
            val limiter = 6 //Number of items I want to get from the server
            val foodCategory = arrayListOf<FoodCategory>()
            Log.i("getUpdatedFood","I am running on tread: $coroutineContext")
    
            val getRecommended = foodRepository.getRecommendedFood(limiter.toString())
                foodCategory += FoodCategory(id = 0.toString(), title = "Recommended for you", foods = getRecommended)
                
            val getSpecials = foodRepository.getSpecials(limiter.toString())
            foodCategory += FoodCategory(id = 1.toString(), title = "Specials", foods = getSpecials)
            return foodCategories
        }
    }
    

    The key ideas here: