如何在不跳帧的情况下更新 UI

How can I update the UI without skipping frames

所以我正在使用协程在 Kotlin 中开发和 android 应用程序,无论我做了什么更改,我仍然不断收到消息:

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

我怎样才能摆脱它。我的意思是我只显示九张照片...下面是我的代码

型号:

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)
        //}
    }
}

存储库:

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)
    }
}




绑定适配器:


//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
    }
}

我有一个 Food Item 的回收视图和一个 FoodCategory 的回收视图池。如果我注释掉 _foodCategory.postValue(foodCategory) ViewModel: getUpdatedFood() 中我没有收到消息。但是,当我将列表提交到外部回收视图(持有视图池的那个)时,我得到了这个答案。请帮忙。我坚持了一段时间,试图摆脱这条消息。

谢谢..

已更新 下面是适配器及其视图持有者

FoodItem 布局

<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 适配器

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
        }
    }
}

食品类别布局

<?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 适配器

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
        }
    }
}

父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>

跳帧可能与您 post 编写的代码无关:对我来说,这听起来像是 RecyclerViews / Adapters 配置错误。不过,您需要 post 使其代码更加清晰。

然而,即使您 post 编辑的可能不是罪魁祸首,您仍然可以优化您拥有的协程代码:

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
    }
}

这里的关键思想: