通过第3页和流动API提高Pokage Dex的性能
#kotlin #android #paging #flow

在©现在,我们已经在应用程序的屏幕上显示了Kanto地区的所有Pokâ©Mon(第一个151),以及它们的名称,ids和类型。色情©m,我们的应用程序仍然非常慢:仅加载第一个口袋怪物的必要时间超过10秒。因此,在这篇文章中,我们将使用Koud1库3和Koud2提高pokâdex的性能。

什么是Paging 3

Paging是一个能够实现称为页面的机制的库,它涉及逐渐加载大量数据,减少网络使用和系统资源。陈述了3个库3,我们默认使用Kotlin Coroutines,这就是为什么我们将使用它。

Paging 3的优势:

  • 控制用于检索下一个和上一个p的键。
  • 当用户在已加载数据的末尾传输屏幕时,会自动提出以下正确页面的请求。
  • 确保不会同时发射最后一个。
  • 跟踪加载状态,并允许我们在RecyclerView中展示它,并为负载故障案例提供狂热的保留功能。
  • 允许在列表中使用常见操作,例如Koud7或Koud8。
  • 提供了一种实现列表分隔符的幻想,例如 footer ,我们将做。
  • 简化了数据缓存,确保我们不会在每次更改配置时执行数据转换。

正如我们所看到的,这是解决我们的宽符号问题的解决方案和pokémon的显示限制,在此库中,我们将能够显示所有小怪物ðð©

什么是Flow API

简而言之,在Coroutines中,Flow是一种可以顺序向suspend functions发出多个值的类型,该值仅返回一个值。在我们的情况下,我们将使用它来发出pokâ©mon数据流来创建我们的pokâ©dex。

从概念上讲,Flow是数据的(流)可以计算为异步方式。发行的值必须为相同的类型。例如,Flow<Int>是发出整个值的流。让我们看下面的示例:

import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.delay

suspend fun main() {
    val flow: Flow<Int> = flow {
        (0..10).forEach {
            delay(2000)
            emit(it)
        }
    }.map {
        it * it
    }

    flow.collect {
        println(it)
    }
}
0
1
4
9
16
25
36
49
64
81
100

我们可以将此示例视为Flow的A Hello World ,在其中,我们创建了每2(2)秒发出的整数的数据流。这些数据使用map函数进行了转换,然后使用collect函数收集以最终显示在控制台上。

在这个简单的示例中,我们可以看到并理解数据流中涉及的组件和实体:Koud18(生产者),Koud19(中级)和Collector(消费者)。

flow-entities.png

简而言之,Flow Builder执行任务并发布数据,Operator将数据从一种格式转换为另一种格式,而Collector收集了Flow Builder发出的数据,这些数据由Operator转换。

在我们的应用程序中,数据源和重新定位代表消费者和中间任务,而UI将是消费者。

实施解决方案

要实现我们的解决方案,我们需要创建一个Koud26,koud27,PagingData koud10和一个名为koud30的广告适配器。

paging-components

好的,但是这是什么字母汤?冷静下来,让我们一个一个一个。

PagingSource

a PagingSource是该页面数据源的定义,以及该数据将从源恢复的方式。 Koud26应该是存储库的一部分。

让我们突出getRefreshKey()函数,该功能为load()函数提供了钥匙,以及load() prom。有关后键和以前键的信息。这两个函数都是suspend functions,因此我们可以使用使申请的功能©api。

首先,我们将添加koud1库的因子到我们的gradle.build文件:

def pagingVersion = "3.1.1"

dependencies {
        ...

    // Paging
    implementation "androidx.paging:paging-runtime:$pagingVersion"

        ...
}

我们现在将继续进行Koud26的开发,我们将其称为Koud41。我们班级的隐私将收到一个PokemonApi实例,她将从Koud43继承,也就是说,她获得了整个钥匙,并从Poké©Mon中返回数据。我们将从母亲的建设开始©All getRefreshKey(),负责向母亲提供钥匙©All Koud35:

package br.com.pokedex.data.datasource.repository

import androidx.paging.PagingSource
import androidx.paging.PagingState
import br.com.pokedex.data.api.PokemonApi
import br.com.pokedex.domain.model.SinglePokemon

class PokedexPagingSource(
    private val api: PokemonApi
) : PagingSource<Int, SinglePokemon>() {

    override fun getRefreshKey(state: PagingState<Int, SinglePokemon>): Int? {
        return state.anchorPosition?.let { anchorPosition ->
            state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
                ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
        }
    }
}

该母亲的钥匙所有退货都必须使load()负载足够的项目填充 viewport 在访问的最后一个位置附近,从而使下一代可以透明地生成动漫。

可以通过state.anchorPosition恢复访问的最后一个位置,该位置通常是 fievorport 中最高或最低的项目,因为访问被项目在屏幕上滚动时发射。但是在我们的情况下,访问的最后一个位置还不够,我们需要知道钥匙/pânão最接近该位置,这就是为什么我们使用函数£o koud48。

例如,假设我们必须加载每个pânão的20个项目,加载两页将意味着使用1和2个键,并总共加载40个项目。在这种情况下,当我们在位置30中寻找项目时,Koud47将返回30,但这不是我们的钥匙,我们需要知道此项目是哪个页面,即,我们需要知道这是第2页。 minus(1)(少一个),也导致键2。

是时候实施了这么多的koudde35。您的标准收益是LoadResult<Key, Value>类型,在我们的情况下是LoadResult<Int, SinglePokemon>LoadResult类是sealed classsealed class是必须在其内部或同一文件中开发其所有子类的类。我们的返回类具有反式子类:ErrorInvalidPageError代表一个预期的错误(例如网络连接失败),Invalid表示PagingSource的任何未来请求无效,而Page代表成功的返回对象。

koud61类具有以下相关属性:

  • data:带电的数据
  • prevKey:上一页的键,如果存在
  • nextKey:如果存在的钥匙,则

Imparacha㧣它也是Fun -oload()

package br.com.pokedex.data.datasource.repository

import androidx.paging.PagingSource
import androidx.paging.PagingState
import br.com.pokedex.data.api.PokemonApi
import br.com.pokedex.data.datasource.Constants.LAST_OFFSET
import br.com.pokedex.data.datasource.Constants.LAST_POSITION
import br.com.pokedex.data.datasource.Constants.POKEMON_OFFSET
import br.com.pokedex.data.datasource.Constants.POKEMON_STARTING_OFFSET
import br.com.pokedex.data.mapper.toModel
import br.com.pokedex.domain.model.SinglePokemon
import okio.IOException
import retrofit2.HttpException

class PokedexPagingSource(
    private val api: PokemonApi
) : PagingSource<Int, SinglePokemon>() {

    // ...

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, SinglePokemon> {
        val position = params.key ?: POKEMON_STARTING_OFFSET
        return try {
            val response = api.getPokemon(
                if (position == LAST_POSITION) {
                    LAST_OFFSET
                } else {
                    position * POKEMON_OFFSET
                }
            )
            val pokemon = mutableListOf<SinglePokemon>()
            response.body()?.results?.map { result ->
                val singlePokemon = api.getSinglePokemon(result.name)
                singlePokemon.body()?.toModel()?.let { pokemon.add(it) }
            }
            LoadResult.Page(
                data = pokemon,
                prevKey = if (position == POKEMON_STARTING_OFFSET) null else position,
                nextKey = if (position == LAST_POSITION) null else position + 1
            )
        } catch (exception: IOException) {
            return LoadResult.Error(exception)
        } catch (exception: HttpException) {
            return LoadResult.Error(exception)
        }
    }
}

绕过Constants

package br.com.pokedex.data.datasource

object Constants {
    const val BASE_URL = "https://pokeapi.co/api/v2/"
    const val POKEMON_STARTING_OFFSET = 0
    const val POKEMON_OFFSET = 20
    const val LAST_OFFSET = 885
    const val LAST_POSITION = 45
    const val PAGE_SIZE = 20
}

也有必要用两个新妈妈修改PokemonApi:koud73,他返回一定数量的pokémon,根据a offset getSinglePokemon()姓名。此外,母亲现在的回归是Response<T>,对于使用Paging实施必不可少

package br.com.pokedex.data.api

import br.com.pokedex.data.api.dto.PokemonDTO
import br.com.pokedex.data.api.dto.SinglePokemonDTO
import retrofit2.Response
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query

interface PokemonApi {

    @GET("pokemon/")
    suspend fun getPokemon(
        @Query("offset") offset: Int?
    ): Response<PokemonDTO>

    @GET("pokemon/{name}")
    suspend fun getSinglePokemon(
        @Path("name") name: String?
    ) : Response<SinglePokemonDTO>

}

classe PokemonDTO

package br.com.pokedex.data.api.dto

import com.google.gson.annotations.SerializedName

data class PokemonDTO(
    @SerializedName("results") val results: List<PokemonResultDTO>
)

我们将逐步了解

我们使用koud78获得当前密钥的值,即我们使用getRefreshKey()函数,如果null,我们使用POKEMON_STARTING_OFFSET常数:

val position = params.key ?: POKEMON_STARTING_OFFSET

函数的返回是try-catch,我们尝试使用常数Koud82,Koud83和POKEMON_OFFSET恢复20个Pokémon的数据。缺点很简单:避免使用不一致的数据(id 905的Alês)获得pokémon,我们只通过 offset final final,如果是最后一个位置从当时的pokage获取数据。此外,我们将此请求的结果映射到SinglePokemon列表。最后,我们实例化了LoadResult,并给出了它们的属性:

  • 这是67接收到五个memberstionada
  • 的列表
  • 如果当前位置为0(零),则prevKey将接收null,如果未接收当前位置
  • nextKey接收Koud91,如果最后一个位置是(如果不是)接收到当前位置的更多1加1
val response = api.getPokemon(
    if (position == LAST_POSITION) {
        LAST_OFFSET
    } else {
        position * POKEMON_OFFSET
    }
)
val pokemon = mutableListOf<SinglePokemon>()
response.body()?.results?.map { result ->
    val singlePokemon = api.getSinglePokemon(result.name)
    singlePokemon.body()?.toModel()?.let { pokemon.add(it) }
}
LoadResult.Page(
    data = pokemon,
    prevKey = if (position == POKEMON_STARTING_OFFSET) null else position,
    nextKey = if (position == LAST_POSITION) null else position + 1
)

最后,我们有catchs来保护我们的应用程序对Koud95(例如网络连接)和Koud96(带有不同2xx的响应):

catch (exception: IOException) {
    return LoadResult.Error(exception)
} catch (exception: HttpException) {
    return LoadResult.Error(exception)
}

ph!很多事情不是吗?但是冷静下来还有更多。

Cold27 E Cold98

现在,我们实施了一种处理远程数据源的方法,我们已经可以修改对此更改的重新定位。从那时起,我们的存储库将从Pager返回PagingData<SinglePokemon>数据流,但是什么是Pager

Pager不过是PagingData的反应流()的构建器,与Koud10一起处理,处理页面的所有状态,在成功的情况下返回一个对象,以防万一。

要实例化Pager,我们需要一个PageConfigPager的配置,在该配置中,我们在其中通知pamin的大小,20,也需要告知koud108,也就是说,这是如何数据Koud27将制造,即koud110我们一点一点地定义:

package br.com.pokedex.data.datasource.repository

import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import br.com.pokedex.data.api.PokemonApi
import br.com.pokedex.data.datasource.Constants.PAGE_SIZE
import br.com.pokedex.domain.model.SinglePokemon
import br.com.pokedex.domain.repository.PokemonRepository
import kotlinx.coroutines.flow.Flow

class PokemonRepositoryImpl(private val api: PokemonApi) : PokemonRepository {

    override fun getSinglePokemon(): Flow<PagingData<SinglePokemon>> {
        return Pager(
            config = PagingConfig(
                pageSize = PAGE_SIZE
            ),
            pagingSourceFactory = { PokedexPagingSource(api) }
        ).flow
    }
}

最后,我们使用flow Pager的属性表示将返回PagingData的流程,也就是说,在某些情况下将发出新的PagingData实例。

PokemonRepository接口修改:

package br.com.pokedex.domain.repository

import androidx.paging.PagingData
import br.com.pokedex.data.api.Resource
import br.com.pokedex.data.api.dto.SinglePokemonDTO
import br.com.pokedex.domain.model.SinglePokemon
import kotlinx.coroutines.flow.Flow

interface PokemonRepository {

    fun getSinglePokemon(): Flow<PagingData<SinglePokemon>>
}

PagingDataAdapter

当我们已经实现了存储库层时,我们将修改 viewModel 以反映这些更改。我们不会使用更多 livedata ,我们将使用Flow

package br.com.pokedex.presentation

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.PagingData
import androidx.paging.cachedIn
import br.com.pokedex.domain.interactor.GetSinglePokemonUseCase
import br.com.pokedex.domain.model.SinglePokemon
import kotlinx.coroutines.flow.Flow

class PokedexViewModel(
    private val useCase: GetSinglePokemonUseCase
) : ViewModel() {

    fun getPokemonFlow(): Flow<PagingData<SinglePokemon>> {
        return useCase.execute().cachedIn(viewModelScope)
    }

}

这要简单得多,不是吗?我们基本上将Koud118函数的返回类型(在称为Koud73)之前,将其称为Koud120,我们称为 usecase ,但由于我们应用了一个新的有趣呼叫:cachedIn()。这个功能有什么作用?

cachedIn()函数创建缓存PagingData Rude Access成员。这会导致koud111在范围处于活动状态时保持活跃,在我们的情况下, viewModel 的范围。因此,我们保证,无论配置的更改(例如屏幕旋转),新的活动都将接收现有数据,以便提出新请求以从头开始获取数据。

请注意, USECase代码没有更改,Kotlin的精益语法太多:

package br.com.pokedex.domain.interactor

import br.com.pokedex.domain.repository.PokemonRepository

class GetSinglePokemonUseCase(private val repository: PokemonRepository) {

    fun execute() = repository.getSinglePokemon()
}

好吧,我们已经可以创建PagingDataAdapter,这将使所有这些都与UI连接成为可能。基本上,我们将更改现有的适配器以反映我们所做的更改。 PokemonViewHolder将保持不变:

class PokemonViewHolder(binding: PokemonCardBinding) :
        RecyclerView.ViewHolder(binding.root) {
        private val image = binding.pokemonImage
        private val name = binding.pokemonName
        private val id = binding.pokemonId
        private val firstType = binding.firstPokemonType
        private val secondType = binding.secondPokemonType

        fun bind(singlePokemon: SinglePokemon) {
            loadPokemonImage(image, singlePokemon.imageUrl)
            name.text = singlePokemon.name
            id.text = singlePokemon.id.toString()
            firstType.text = singlePokemon.types.first().name
            secondType.text = singlePokemon.types.last().name
            secondType.apply {
                showIf(text.isNotEmpty())
            }
        }

        private fun loadPokemonImage(image: ImageView, imageUrl: String) {
            image.load(imageUrl)
        }
}

我们的班级将是PagingDataAdapter,但是,使用此类型的适配器才能实现Koude Class128。此类负责执行A 回调(将功能作为另一个函数传递),该函数计算列表中列表中两个项目之间的差异。换句话说,它可以验证两个对象表示同一项目,并检查两个项目是否具有相同的数据。

让我们以singleton class(一个类别的实例)来实现它。 Kotlin使用Koud130关键字开发了这种设计模式,并且由于此Koud129将在另一个关键字,它将是一个Koud132:

companion object {
    private val POKEMON_COMPARATOR = object
        : DiffUtil.ItemCallback<SinglePokemon>() {

        override fun areItemsTheSame(
            oldItem: SinglePokemon,
            newItem: SinglePokemon
        ): Boolean =
            oldItem.id == newItem.id

        override fun areContentsTheSame(
            oldItem: SinglePokemon,
            newItem: SinglePokemon
        ): Boolean =
            oldItem == newItem
    }
}

正如我们所看到的,我们一直在寻找Koud128:areItemsTheSame()和Koud135的两个妈妈。他们负责检查两个项目是否是同一项目,并且是否分别包含两个项目。

我们的实施非常简单:要检查两个项目是否是同一项目,只需比较pokâ©蒙蒙并检查两个项目是否相同。右边。

我们可以进入函数onBindViewHolder()onCreateViewHolder()的开发,不需要脱颖而出整个getItemCount()PagingDataAdapter将负责。

我们的onBindViewHolder()将是这样:

override fun onBindViewHolder(holder: PokemonViewHolder, position: Int) {
    val singlePokemon = getItem(position)
    singlePokemon?.let {
        holder.bind(it)
    }
}

在这里,我们使用PagingDataAdapter提供的整个getItem(),它返回我们所需的物品,因为返回可以为null,我们使用a safe call ,最后我们执行了bind()

,至于我们的母亲©All onCreateViewHolder(),没有任何变化:

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

完成了!我们有我们的适配器

package br.com.pokedex.presentation

import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.ImageView
import androidx.paging.PagingDataAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import br.com.pokedex.databinding.PokemonCardBinding
import br.com.pokedex.domain.model.SinglePokemon
import br.com.pokedex.util.showIf
import coil.load

class PokedexAdapter(
    private val context: Context
) : PagingDataAdapter<SinglePokemon, PokedexAdapter.PokemonViewHolder>(
    POKEMON_COMPARATOR
) {

    override fun onBindViewHolder(holder: PokemonViewHolder, position: Int) {
        val singlePokemon = getItem(position)
        singlePokemon?.let {
            holder.bind(it)
        }
    }

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

    inner class PokemonViewHolder(binding: PokemonCardBinding) :
        RecyclerView.ViewHolder(binding.root) {
        private val image = binding.pokemonImage
        private val name = binding.pokemonName
        private val id = binding.pokemonId
        private val firstType = binding.firstPokemonType
        private val secondType = binding.secondPokemonType

        fun bind(singlePokemon: SinglePokemon) {
            loadPokemonImage(image, singlePokemon.imageUrl)
            name.text = singlePokemon.name
            id.text = singlePokemon.id.toString()
            firstType.text = singlePokemon.types.first().name
            secondType.text = singlePokemon.types.last().name
            secondType.apply {
                showIf(text.isNotEmpty())
            }
        }

        private fun loadPokemonImage(image: ImageView, imageUrl: String) {
            image.load(imageUrl)
        }
    }

    companion object {
        private val POKEMON_COMPARATOR = object
            : DiffUtil.ItemCallback<SinglePokemon>() {

            override fun areItemsTheSame(
                oldItem: SinglePokemon,
                newItem: SinglePokemon
            ): Boolean =
                oldItem.id == newItem.id

            override fun areContentsTheSame(
                oldItem: SinglePokemon,
                newItem: SinglePokemon
            ): Boolean =
                oldItem == newItem
        }
    }

}

连接UI中的适配器

我们将继续在IU中进行 apapter 的连接。我们将构建的两个最重要的功能将如下:setUpAdapter()getPokemon()setUpAdapter()功能将负责配置我们的 apapter

private fun setUpAdapter() {
        pokedexAdapter.addLoadStateListener { loadState ->
            when(loadState.refresh) {
                is LoadState.Loading -> setUpLoadingView()
                is LoadState.Error -> setUpErrorView()
                else -> setUpSuccessView()
            }
        }
}

PagingDataAdapter为我们提供了一个出色的错误处理类,称为CombinedLoadStates,它提供了适配器的所有加载态。我们在这里感兴趣的是State Refresh ,它代表了Adapter 的第一个负载:LoadingErrorNotLoading,仅在前两个中对我们感兴趣。如果此状态为Loading,我们将IU配置为代表负载,如果是Erorr,我们将IU配置为表示错误,如果这两个都不是错误,我们认为它成功并呈现了Pokémon。 p>列表。

母亲©所有UI配置:

private fun setUpSuccessView() {
        binding.apply {
            pokedexRecyclerView.showView()
            pokedexCircularProgressIndicator.hideView()
            pokedexErrorMessage.hideView()
        }
}

private fun setUpErrorView() {
    binding.apply {
        pokedexRecyclerView.hideView()
        pokedexCircularProgressIndicator.hideView()
        pokedexErrorMessage.showView()
    }
}

private fun setUpLoadingView() {
    binding.apply {
        pokedexRecyclerView.hideView()
        pokedexCircularProgressIndicator.showView()
        pokedexErrorMessage.hideView()
    }
}

我们已经知道koud156,但是在这里,我们参考了其他两个 views :koud157和koud158。第一个是进度栏设计材料提供的围巾的形式,第二个是a textview ,它显示了错误的信息。以下是activity_main.xml的修改代码:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/pokedexRecyclerView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:listitem="@layout/pokemon_card"
        android:visibility="visible"/>

    <TextView
        android:id="@+id/pokedexErrorMessage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textAlignment="center"
        app:layout_constraintTop_toBottomOf="@id/pokedexRecyclerView"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        android:text="@string/error_message"
        android:visibility="gone"/>

    <com.google.android.material.progressindicator.CircularProgressIndicator
        android:id="@+id/pokedexCircularProgressIndicator"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminate="true"
        android:visibility="gone"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        app:layout_constraintBottom_toBottomOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

我们都留下了视图助手gone gone,并通过以下妈妈控制他们的可见性:

package br.com.pokedex.util

import com.google.android.material.progressindicator.CircularProgressIndicator

fun CircularProgressIndicator.showView() {
    visibility = CircularProgressIndicator.VISIBLE
}

fun CircularProgressIndicator.hideView() {
    visibility = CircularProgressIndicator.GONE
}
package br.com.pokedex.util

import android.widget.TextView

fun TextView.showIf(condition: Boolean) {
    if(condition) {
        visibility = TextView.VISIBLE
    }
}

fun TextView.showView() {
    visibility = TextView.VISIBLE
}

fun TextView.hideView() {
    visibility = TextView.GONE
}
package br.com.pokedex.util

import androidx.recyclerview.widget.RecyclerView

fun RecyclerView.showView() {
    visibility = RecyclerView.VISIBLE
}

fun RecyclerView.hideView() {
    visibility = RecyclerView.GONE
}

©m,我们更新了Koud161文件,其中包含我们项目的项目:

<resources>
    <string name="app_name">Pokédex</string>
    <string name="error_message">Something went wrong\nPlease, check your network connection and try reopen the app</string>
</resources>

现在让我们看一下整个Koud73。整个负责从 viewModel 收集数据,并将其提交给 apapter 。为此,我们分别使用FlowsubmitData()collectLatest函数:

private fun getPokemon() {
        lifecycleScope.launch {
            viewModel.getPokemonFlow().collectLatest { pokemon ->
                pokedexAdapter.submitData(pokemon)
            }
        }
}

最后,我们的koud167课程将是这样:

package br.com.pokedex.presentation

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.paging.LoadState
import androidx.recyclerview.widget.LinearLayoutManager
import br.com.pokedex.databinding.ActivityPokedexBinding
import br.com.pokedex.util.hideView
import br.com.pokedex.util.showView
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.viewModel

class PokedexActivity : AppCompatActivity() {

    private val binding by lazy {
        ActivityPokedexBinding.inflate(layoutInflater)
    }

    private val viewModel: PokedexViewModel by viewModel()
    private val pokedexAdapter by lazy {
        PokedexAdapter(this)
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        setUpAdapter()
        setUpPokedexRecyclerView()
        getPokemon()
    }

    private fun setUpAdapter() {
        pokedexAdapter.addLoadStateListener { loadState ->
            when(loadState.refresh) {
                is LoadState.Loading -> setUpLoadingView()
                is LoadState.Error -> setUpErrorView()
                else -> setUpSuccessView()
            }
        }
    }

    private fun setUpSuccessView() {
        binding.apply {
            pokedexRecyclerView.showView()
            pokedexCircularProgressIndicator.hideView()
            pokedexErrorMessage.hideView()
        }
    }

    private fun setUpErrorView() {
        binding.apply {
            pokedexRecyclerView.hideView()
            pokedexCircularProgressIndicator.hideView()
            pokedexErrorMessage.showView()
        }
    }

    private fun setUpLoadingView() {
        binding.apply {
            pokedexRecyclerView.hideView()
            pokedexCircularProgressIndicator.showView()
            pokedexErrorMessage.hideView()
        }
    }

    private fun setUpPokedexRecyclerView() {
        binding.pokedexRecyclerView.apply {
            layoutManager = LinearLayoutManager(context)
            adapter = pokedexAdapter
        }
    }

    private fun getPokemon() {
        lifecycleScope.launch {
            viewModel.getPokemonFlow().collectLatest { pokemon ->
                pokedexAdapter.submitData(pokemon)
            }
        }
    }

}

我们已经可以运行该应用并查看三种视图:

Tela de loading:

loading.png

tela de erro:

error-view.png

成功屏幕:

sucesso.png

通过此实施,我们已经能够将加载pokâmon加载到不到5秒的等待时间,改善了50%以上!祝贺我们ðð

,但我们仍然有问题:尽管我们在屏幕的第一个负载中处理可能的错误,但我们以后不这样做。也就是说,如果移动互联网由于某种原因停止工作,那么享受将不知道该怎么办。我们需要改善这一经验。我们该怎么做?添加页脚

提高页脚的经验

Paging库提供了几种改善其实施体验的工具,其中之一是 footer ,它不过是杆,文本,botan£o或图像,它将始终留在屏幕的末端。我们将使用此页脚显示进度栏加载或错误消息和一个按钮,以尝试再次加载剩余的数据。

首先,我们将为此页脚创建一个布局,它将由koud169组成,以表示加载,koud170,错误文本和MaterialButton,按钮,按钮重试。所有这些都有可见性gone默认情况下:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
        android:id="@+id/loadStateErrorMessage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/error_message"
        android:layout_gravity="center"
        android:textAlignment="center"
        android:visibility="gone"/>

    <com.google.android.material.button.MaterialButton
        android:id="@+id/loadStateTryAgainButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/try_again"
        android:layout_gravity="center"
        android:visibility="gone"
        android:layout_marginTop="16dp"/>

    <com.google.android.material.progressindicator.CircularProgressIndicator
        android:id="@+id/loadStateProgressBar"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminate="true"
        android:layout_gravity="center" />

</LinearLayout>

strings.xml文件修改:

<resources>
    <string name="app_name">Pokédex</string>
    <string name="error_message">Something went wrong\nCheck your network connection and try again</string>
    <string name="try_again">Try again</string>
    <string name="load_error_message">Something went wrong\nTry again</string>
</resources>

要实现此页脚,我们还需要创建一个 apapter LoadStateAdapter和此适配器需要 viever 。我们叫你koud175:

package br.com.pokedex.presentation

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.paging.LoadState
import androidx.recyclerview.widget.RecyclerView
import br.com.pokedex.R
import br.com.pokedex.databinding.PokedexLoadStateFooterBinding
import br.com.pokedex.util.hideView
import br.com.pokedex.util.showView

class PokedexLoadStateViewHolder(
    private val binding: PokedexLoadStateFooterBinding,
    tryAgain: () -> Unit
) : RecyclerView.ViewHolder(binding.root) {

    init {
        binding.loadStateTryAgainButton.setOnClickListener { tryAgain.invoke() }
    }

    fun bind(loadState: LoadState) {
        when(loadState) {
            is LoadState.Loading -> {
                binding.loadStateProgressBar.showView()
                binding.loadStateErrorMessage.hideView()
                binding.loadStateTryAgainButton.hideView()
            }
            is LoadState.Error -> {
                binding.loadStateErrorMessage.showView()
                binding.loadStateTryAgainButton.showView()
                binding.loadStateProgressBar.hideView()
            }
            is LoadState.NotLoading -> {
                // Do nothing
            }
        }
    }

    companion object {
        fun create(parent: ViewGroup, tryAgain: () -> Unit) : PokedexLoadStateViewHolder {
            val view = LayoutInflater.from(parent.context)
                .inflate(R.layout.pokedex_load_state_footer, parent, false)
            val binding = PokedexLoadStateFooterBinding.bind(view)
            return PokedexLoadStateViewHolder((binding), tryAgain)
        }
    }
}
pplispãpædion)jeee视频sumplides 171:deguogram?

package br.com.pokedex.util

import com.google.android.material.button.MaterialButton

fun MaterialButton.showView() {
    visibility = MaterialButton.VISIBLE
}

fun MaterialButton.hideView() {
    visibility = MaterialButton.GONE
}

我们的 viewholder < /em>在您的构建器中接收两件事:binding属性,其中包含对我们布局的引用,a challback < /em>从单击时将执行的函数中将执行按钮重试。在这里,我们还使用A 初始化器块,它是每当创建类时将执行的方式的块。在其中,我们通知您,点击点击操作将调用函数回调 tryAgain(),即将执行它。

class PokedexLoadStateViewHolder(
    private val binding: PokedexLoadStateFooterBinding,
    tryAgain: () -> Unit
) : RecyclerView.ViewHolder(binding.root) {

    init {
        binding.loadStateTryAgainButton.setOnClickListener { tryAgain.invoke() }
    }

    // ...

}

之后,我们创建了bind()函数,该功能将打开 apapter Viewholder 。简而言之,她使用loadStates使IU反应性,使她对成功,错误和加载状态做出反应:

fun bind(loadState: LoadState) {
        when(loadState) {
            is LoadState.Loading -> {
                binding.loadStateProgressBar.showView()
                binding.loadStateErrorMessage.hideView()
                binding.loadStateTryAgainButton.hideView()
            }
            is LoadState.Error -> {
                binding.loadStateErrorMessage.showView()
                binding.loadStateTryAgainButton.showView()
                binding.loadStateProgressBar.hideView()
            }
            is LoadState.NotLoading -> {
                // Do nothing
            }
        }
    }

最后,我们开发了一个函数,该函数返回了companion object中此 viewholder 的实例,它接收了a viewGroup ,我们称之为parent,并且function 回调 tryAgain()。 koud182是其他布局的布局 - 即koud159,而koud178是单击“再次尝试”按钮时将执行的函数:

companion object {
    fun create(parent: ViewGroup, tryAgain: () -> Unit) : PokedexLoadStateViewHolder {
        val view = LayoutInflater.from(parent.context)
            .inflate(R.layout.pokedex_load_state_footer, parent, false)
        val binding = PokedexLoadStateFooterBinding.bind(view)
        return PokedexLoadStateViewHolder((binding), tryAgain)
    }
}

我们的适配器的方式将非常简单:我们脱颖而出©All Koud137和Koud138。在第一个中,我们称为 viewholder 的koud144函数,第二个我们称为create()

package br.com.pokedex.presentation

import android.view.ViewGroup
import androidx.paging.LoadState
import androidx.paging.LoadStateAdapter

class PokedexLoadStateAdapter(
    private val tryAgain: () -> Unit
) : LoadStateAdapter<PokedexLoadStateViewHolder>() {

    override fun onBindViewHolder(holder: PokedexLoadStateViewHolder, loadState: LoadState) {
        holder.bind(loadState)
    }

    override fun onCreateViewHolder(
        parent: ViewGroup,
        loadState: LoadState
    ): PokedexLoadStateViewHolder {
        return PokedexLoadStateViewHolder.create(parent, tryAgain)
    }

}

请注意,koud191接收一个函数,这是单击“再次尝试试验”时执行的函数。自UI以来,我们一直在传播她的 viewholder 通过母亲的create()功能©All All Koud138。

我们几乎结束了,但是首先我们需要在PokedexAdapter中进行更改。我们开发页脚的方式将以A span recyclerview 的大小显示,以及我们计划如何展示两个-column的pokémon网格,我们必须修复它。

羊毛如下:如果查看项目是pokâmon,则此项目的大小为 span 一个(如果不是),则大小为 span 二。让我们开始过度发展PokedexAdaptergetViewType()函数:

override fun getItemViewType(position: Int): Int {
    return if (position == itemCount) {
        NETWORK_VIEW_TYPE
    } else {
        POKEMON_VIEW_TYPE
    }
}

如果当前位置等于最后一个位置,即等于itemCount,则查看项目的类型将为NETWORK_VIEW_TYPE,也就是说,它将是一个将显示 progress bar 的项目或错误。如果没有,它将是POKEMON_VIEW_TYPE项目,换句话说,该项目将显示一个pokâ©Mon。

object Constants修改:

package br.com.pokedex.util

object Constants {
    const val BASE_URL = "https://pokeapi.co/api/v2/"
    const val POKEMON_STARTING_OFFSET = 0
    const val POKEMON_OFFSET = 20
    const val LAST_OFFSET = 885
    const val LAST_POSITION = 45
    const val PAGE_SIZE = 20
    const val NETWORK_VIEW_TYPE = 2
    const val POKEMON_VIEW_TYPE = 1
}

好吧,我们可以开始连接 adpter âii。主要修改将在母亲©All setUpPokedexRecyclerView()中。在其中,我们将实现 span 的大小:

package br.com.pokedex.presentation

import ...

private const val SPAN_COUNT = 2

class PokedexActivity : AppCompatActivity() {

    //...    

    private fun setUpPokedexRecyclerView() {
        binding.pokedexRecyclerView.apply {
            val gridLayoutManager = GridLayoutManager(context, SPAN_COUNT)
            gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
                override fun getSpanSize(position: Int): Int {
                    val viewType = pokedexAdapter.getItemViewType(position)
                    return if (viewType == POKEMON_VIEW_TYPE) ONE_SPAN_SIZE
                    else TWO_SPANS_SIZE
                }
            }
            layoutManager = gridLayoutManager
            adapter = pokedexAdapter.withLoadStateFooter(
                footer = PokedexLoadStateAdapter { pokedexAdapter.retry() }
            )
        }
    }

    //...

}

object Constants修改:

package br.com.pokedex.util

object Constants {
    const val BASE_URL = "https://pokeapi.co/api/v2/"
    const val POKEMON_STARTING_OFFSET = 0
    const val POKEMON_OFFSET = 20
    const val LAST_OFFSET = 885
    const val LAST_POSITION = 45
    const val PAGE_SIZE = 20
    const val NETWORK_VIEW_TYPE = 2
    const val POKEMON_VIEW_TYPE = 1
    const val ONE_SPAN_SIZE = 1
    const val TWO_SPANS_SIZE = 2
}

首先,我们创建了一个GridLayoutManager span_count 等于两个。然后,我们将新的SpanSizeLookUp类实例化为singleton类,我们突出了整个getSpanSizeLookUp()。在这个母亲中,如果您没有跨度大小二,那么我们将获得当前物品的viewType,如果您的跨度将具有一个尺寸。这,我们定义了我们配置的gridLayout将是 recyclerview 的koud209,并且在 apapter的定义中,我们在课程中进行了修改:我们通知您,我们知道我们是我们的。现在通知您pokedexAdapter将拥有页脚PokedexLoadStateAdapter。请注意,在这里,我们终于传递了单击按钮重试时将执行的功能:pokedexAdapter.retry(),此功能试图补充页面上失败的任何请求。

为了在初始加载中提供retry()的此功能,我们修改了活动的布局,插入MaterialButton,当单击时,MaterialButton执行retry()

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/pokedexRecyclerView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:listitem="@layout/pokemon_card" />

    <TextView
        android:id="@+id/pokedexErrorMessage"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/error_message"
        android:textAlignment="center"
        android:visibility="gone"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/pokedexRecyclerView" />

    <com.google.android.material.button.MaterialButton
        android:id="@+id/pokedexTryAgainButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="@string/try_again"
        android:visibility="gone"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/pokedexErrorMessage" />

    <com.google.android.material.progressindicator.CircularProgressIndicator
        android:id="@+id/pokedexCircularProgressIndicator"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:indeterminate="true"
        android:visibility="gone"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

记住Koud161的方式:

<resources>
    <string name="app_name">Pokédex</string>
    <string name="error_message">Something went wrong\nCheck your network connection and try again</string>
    <string name="try_again">Try again</string>
    <string name="load_error_message">Something went wrong\nTry again</string>
</resources>

所以,我们的PokedexActivity课程将是这样:

package br.com.pokedex.presentation

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import androidx.paging.LoadState
import androidx.recyclerview.widget.GridLayoutManager
import br.com.pokedex.databinding.ActivityPokedexBinding
import br.com.pokedex.util.Constants.NETWORK_VIEW_TYPE
import br.com.pokedex.util.Constants.ONE_SPAN_SIZE
import br.com.pokedex.util.Constants.POKEMON_VIEW_TYPE
import br.com.pokedex.util.Constants.TWO_SPANS_SIZE
import br.com.pokedex.util.hideView
import br.com.pokedex.util.showView
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.viewModel

private const val SPAN_COUNT = 2

class PokedexActivity : AppCompatActivity() {

    private val binding by lazy { ActivityPokedexBinding.inflate(layoutInflater) }
    private val pokedexAdapter by lazy { PokedexAdapter(this) }
    private val viewModel: PokedexViewModel by viewModel()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        setUpAdapter()
        setUpTryAgainButton()
        setUpPokedexRecyclerView()
        getPokemon()
    }

    private fun setUpAdapter() {
        pokedexAdapter.addLoadStateListener { loadState ->
            when(loadState.refresh) {
                is LoadState.Loading -> setUpLoadingView()
                is LoadState.Error -> setUpErrorView()
                else -> setUpSuccessView()
            }
        }
    }

    private fun setUpSuccessView() {
        binding.apply {
            pokedexRecyclerView.showView()
            pokedexCircularProgressIndicator.hideView()
            pokedexErrorMessage.hideView()
            pokedexTryAgainButton.hideView()
        }
    }

    private fun setUpErrorView() {
        binding.apply {
            pokedexRecyclerView.hideView()
            pokedexCircularProgressIndicator.hideView()
            pokedexErrorMessage.showView()
            pokedexTryAgainButton.showView()
        }
    }

    private fun setUpLoadingView() {
        binding.apply {
            pokedexRecyclerView.hideView()
            pokedexCircularProgressIndicator.showView()
            pokedexErrorMessage.hideView()
            pokedexTryAgainButton.hideView()
        }
    }

    private fun setUpTryAgainButton() {
        binding.pokedexTryAgainButton.setOnClickListener {
            pokedexAdapter.refresh()
        }
    }

    private fun setUpPokedexRecyclerView() {
        binding.pokedexRecyclerView.apply {
            val gridLayoutManager = GridLayoutManager(context, SPAN_COUNT)
            gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
                override fun getSpanSize(position: Int): Int {
                    val viewType = pokedexAdapter.getItemViewType(position)
                    return if (viewType == POKEMON_VIEW_TYPE) ONE_SPAN_SIZE
                    else TWO_SPANS_SIZE
                }
            }
            layoutManager = gridLayoutManager
            adapter = pokedexAdapter.withLoadStateFooter(
                footer = PokedexLoadStateAdapter { pokedexAdapter.retry() }
            )
        }
    }

    private fun getPokemon() {
        lifecycleScope.launch {
            viewModel.getPokemonFlow().collectLatest { pokemon ->
                pokedexAdapter.submitData(pokemon)
            }
        }
    }

}

最后,我们今天的工作已经结束!让我们运行该应用程序,看看屏幕如何。

修改错误屏幕:

erro_tela_inicial.png

pokâ©twant with footer 充电:

loading_pokemon.png

pokâ©twith with footer 错误:

erro_pokemon.png

帖子

在下一篇文章中,我们将使用 navigation 材料设计美化我们的pokâ©dex,我们还将使用 android Room从数据库中保存Pokage数据并使用 junit mockk

测试我们的应用程序

感谢您的关注和下一个!

repo no github:

GitHub logo ronaldocoding / pokedex

简单的粉末

Pokédex

A simple Pokédex

前邮: