在©现在,我们已经在应用程序的屏幕上显示了Kanto地区的所有Pokâ©Mon(第一个151),以及它们的名称,id
s和类型。色情©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 Builder
执行任务并发布数据,Operator
将数据从一种格式转换为另一种格式,而Collector
收集了Flow Builder
发出的数据,这些数据由Operator
转换。
在我们的应用程序中,数据源和重新定位代表消费者和中间任务,而UI将是消费者。
实施解决方案
要实现我们的解决方案,我们需要创建一个Koud26,koud27,PagingData
koud10和一个名为koud30的广告适配器。
好的,但是这是什么字母汤?冷静下来,让我们一个一个一个。
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 class
。 sealed class
是必须在其内部或同一文件中开发其所有子类的类。我们的返回类具有反式子类:Error
,Invalid
和Page
。 Error
代表一个预期的错误(例如网络连接失败),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
,我们需要一个PageConfig
,Pager
的配置,在该配置中,我们在其中通知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 的第一个负载:Loading
,Error
和NotLoading
,仅在前两个中对我们感兴趣。如果此状态为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 。为此,我们分别使用Flow
和submitData()
的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:
tela de erro:
成功屏幕:
通过此实施,我们已经能够将加载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)
}
}
}
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 二。让我们开始过度发展PokedexAdapter
的getViewType()
函数:
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)
}
}
}
}
最后,我们今天的工作已经结束!让我们运行该应用程序,看看屏幕如何。
修改错误屏幕:
pokâ©twant with footer 充电:
pokâ©twith with footer 错误:
帖子
在下一篇文章中,我们将使用 navigation 和材料设计美化我们的pokâ©dex,我们还将使用 android Room从数据库中保存Pokage数据并使用 junit 和 mockk 。。
测试我们的应用程序感谢您的关注和下一个!
repo no github:
前邮: