Skip to content

Commit

Permalink
Merge pull request #5 from BorgRancher/dev
Browse files Browse the repository at this point in the history
Data paging and UI enhancements building correctly
  • Loading branch information
ZASHMCD authored Jun 4, 2022
2 parents ca8ac64 + ae36ef1 commit 27d29e5
Show file tree
Hide file tree
Showing 30 changed files with 334 additions and 344 deletions.
3 changes: 2 additions & 1 deletion .idea/misc.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 23 additions & 6 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,21 @@ android {
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = '1.8'
jvmTarget = '11'
}
buildFeatures {
viewBinding true
}
namespace 'tech.borgranch.pokedex'

dependenciesInfo {
includeInApk true
includeInBundle true
}
}

dependencies {
Expand Down Expand Up @@ -78,9 +83,9 @@ dependencies {
implementation 'com.apollographql.apollo3:apollo-runtime:3.3.0'

// Kotlin Coroutines
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.2'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.2'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.2'

// Room for local storage
def room_version = "2.4.2"
Expand Down Expand Up @@ -135,7 +140,19 @@ dependencies {
kapt "com.github.bumptech.glide:compiler:$glide_version"
implementation "com.github.florent37:glidepalette:$glide_palette_version"

// WorkManager for background processing
implementation "androidx.work:work-runtime-ktx:2.7.1"

// Lottie for animations
def lottie_version = '5.2.0'
implementation "com.airbnb.android:lottie:$lottie_version"

// Paging 3 for taming pokemon
def paging_version = '3.0.0'
implementation "androidx.paging:paging-runtime:$paging_version"

// alternatively - without Android dependencies for tests
testImplementation "androidx.paging:paging-common:$paging_version"
}

apollo {
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
android:targetSdkVersion="23"
android:name=".PokedexApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:icon="@mipmap/pokedex_test"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:roundIcon="@mipmap/pokedex_test"
android:supportsRtl="true"
android:theme="@style/Theme.Pokedex">
<activity
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/java/tech/borgranch/pokedex/data/dao/ListDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ interface ListDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll(pokemonItems: List<PokemonItem>)

@Query("SELECT COUNT(*) FROM pokemon_item")
fun getCount(): Int

@Query("SELECT * FROM pokemon_item WHERE page = :page")
fun getPage(page: Int): List<PokemonItem>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package tech.borgranch.pokedex.data.repositories

import androidx.paging.PagingSource
import androidx.paging.PagingState
import com.apollographql.apollo3.ApolloClient
import com.apollographql.apollo3.api.Optional
import com.bumptech.glide.load.HttpException
import tech.borgranch.pokedex.data.dao.ListDao
import tech.borgranch.pokedex.data.dto.PokemonItem
import tech.borgranch.pokedex.graphql.PokemonsQuery
import java.io.IOException

class ListPagingSource(
private val pokeDexClient: ApolloClient,
private val itemDao: ListDao
) : PagingSource<Int, PokemonItem>() {

companion object {
const val POKEDEX_STARTING_PAGE = 1
}

override fun getRefreshKey(state: PagingState<Int, PokemonItem>): Int? {
// We need to get the previous key (or next key if previous is null) of the page
// that was closest to the most recently accessed index.
// Anchor position is the most recently accessed index
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}

/**
* Loading API for [PagingSource].
*
* Implement this method to trigger your async load (e.g. from database or network).
*/
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, PokemonItem> {
val page = params.key ?: POKEDEX_STARTING_PAGE
val query = PokemonsQuery(
limit = Optional.presentIfNotNull(params.loadSize),
offset = Optional.presentIfNotNull(page * params.loadSize)
)

try {
val response = pokeDexClient.query(query).execute()
val items = response.data?.pokemons?.results
val data = items?.mapNotNull {
PokemonItem(
name = it?.name!!,
url = it.url!!,
artwork = it.artwork!!,
image = it.image!!,
dreamworld = it.dreamworld!!,
page = page
)
}.orEmpty().toList()
val nextKey = if (data.isEmpty()) null else page + 1
return if (data.isEmpty()) {
LoadResult.Error(IOException("No data"))
} else {
itemDao.insertAll(data)
LoadResult.Page(
data = data,
prevKey = if (page == POKEDEX_STARTING_PAGE) null else page - 1,
nextKey = nextKey
)
}
} catch (e: IOException) {
return LoadResult.Error(e)
} catch (e: HttpException) {
return LoadResult.Error(e)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,23 @@
package tech.borgranch.pokedex.data.repositories

import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import androidx.annotation.WorkerThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.apollographql.apollo3.ApolloClient
import com.apollographql.apollo3.api.Optional
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
import tech.borgranch.pokedex.PokedexApp
import tech.borgranch.pokedex.data.converters.DataMappers.toPokemonItem
import tech.borgranch.pokedex.data.dao.ListDao
import tech.borgranch.pokedex.data.dto.PokemonItem
import tech.borgranch.pokedex.errors.PokeDexErrorHandler
import tech.borgranch.pokedex.graphics.BitmapUtils
import tech.borgranch.pokedex.graphql.PokemonsQuery
import timber.log.Timber
import javax.inject.Inject

class ListRepository @Inject constructor(
Expand All @@ -31,7 +27,7 @@ class ListRepository @Inject constructor(
) {

companion object {
const val POKEMON_LIMIT = 8
const val POKEMON_LIMIT = 20
}

private val loadingState: MutableLiveData<Boolean> = MutableLiveData<Boolean>(false)
Expand All @@ -46,41 +42,52 @@ class ListRepository @Inject constructor(

@WorkerThread
suspend fun fetchPokeDex(page: Int) {
pokemonList.postValue(fetchPokeDexAsync(page).await())
coroutineScope.launch(coroutineDispatcher) {
fetchPokeDexAsync(page)
}
}

fun getPagedPokemon(page: Int = 1): Flow<PagingData<PokemonItem>> {
Timber.d("New network page: $page")
return Pager(
config = PagingConfig(pageSize = POKEMON_LIMIT, enablePlaceholders = true),
pagingSourceFactory = { ListPagingSource(pokeApiClient, itemDao) }
).flow
}

@WorkerThread
private suspend fun fetchPokeDexAsync(page: Int): Deferred<List<PokemonItem>> = coroutineScope.async {
private suspend fun fetchPokeDexAsync(page: Int) {
loadingState.postValue(true)
val pokemons = itemDao.getPage(page)
val pokemons: List<PokemonItem> = itemDao.getPage(page)
if (pokemons.isNotEmpty()) {
// already in db
val data = itemDao.getCurrentPages(page)
pokemonList.postValue(data)
loadingState.postValue(false)
return@async data
}
return@async pokemons.ifEmpty {
fetchRemotePokemon(page)
itemDao.getCurrentPages(page)
} else {
fetchRemotePokemon(page).also {
val data = itemDao.getPage(page)
pokemonList.postValue(data)
loadingState.postValue(false)
}
}
}

@WorkerThread
private suspend fun fetchRemotePokemon(page: Int) {
try {
val incoming = pokeApiClient.query(
PokemonsQuery(
limit = Optional.presentIfNotNull(POKEMON_LIMIT),
offset = Optional.presentIfNotNull(POKEMON_LIMIT * page)
)
).execute()
val query = PokemonsQuery(
limit = Optional.presentIfNotNull(POKEMON_LIMIT),
offset = Optional.presentIfNotNull(POKEMON_LIMIT * page)
)
val incoming = pokeApiClient.query(query).execute()
if (!incoming.hasErrors()) {

incoming.data?.pokemons?.results?.mapNotNull { result ->
result?.let {
if (!itemDao.exists(it.name)) {
// Save image to local file system
saveArtwork(it.toPokemonItem(page))
// Push pokemon to db
itemDao.insert(it.toPokemonItem(page))
}
}
}
Expand All @@ -96,31 +103,4 @@ class ListRepository @Inject constructor(
loadingState.postValue(false)
}
}

private fun saveArtwork(it: PokemonItem) = coroutineScope.launch(coroutineDispatcher) {
Glide.with(PokedexApp.instance)
.asBitmap()
.load(it.artwork)
.into(object : CustomTarget<Bitmap>() {
override fun onLoadCleared(placeholder: Drawable?) {
}

override fun onLoadFailed(errorDrawable: Drawable?) {
super.onLoadFailed(errorDrawable)
errorState.postValue("Failed to load image")
}

override fun onResourceReady(
resource: Bitmap,
transition: Transition<in Bitmap>?
) {
coroutineScope.launch(coroutineDispatcher) {
val filePath =
BitmapUtils.saveBitmap(resource, PokedexApp.instance)
val updatedPokemon = it.copy(artwork = filePath)
itemDao.insert(updatedPokemon)
}
}
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class MainActivity : AppCompatActivity(), ListFragment.OnFragmentInteractionList

private var _binding: MainActivityBinding? = null
private val binding get() = _binding!!

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
_binding = MainActivityBinding.inflate(layoutInflater)
Expand Down
Loading

0 comments on commit 27d29e5

Please sign in to comment.