Skip to content

Commit

Permalink
Add simulated async loading
Browse files Browse the repository at this point in the history
  • Loading branch information
zsoltk committed Jan 16, 2020
1 parent 5a9ca3e commit f556839
Show file tree
Hide file tree
Showing 6 changed files with 167 additions and 10 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# compose-pokedex
![License-MIT](https://img.shields.io/badge/License-MIT-red.svg)

Android Pokedex on Jetpack Compose
Android Pokedex on Jetpack Compose. Single-Activity, no Fragments.

## Install

Expand All @@ -27,6 +27,7 @@ You can build the project with Android Studio 4.0 or [download the apk directly]

- [compose](https://developer.android.com/jetpack/compose)
- [compose-router](https://github.com/zsoltk/compose-router)
- [livedata](https://developer.android.com/topic/libraries/architecture/livedata)

## License

Expand Down
5 changes: 4 additions & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,14 +39,17 @@ dependencies {

implementation fileTree(dir: "libs", include: ["*.jar"])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'androidx.core:core-ktx:1.1.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.1.0'
implementation "androidx.lifecycle:lifecycle-livedata:2.1.0"
implementation 'androidx.ui:ui-framework:0.1.0-dev03'
implementation 'androidx.ui:ui-layout:0.1.0-dev03'
implementation 'androidx.ui:ui-foundation:0.1.0-dev03'
implementation 'androidx.ui:ui-material:0.1.0-dev03'
implementation 'androidx.ui:ui-tooling:0.1.0-dev03'
implementation 'com.github.zsoltk:compose-router:0.3.1'
implementation "io.reactivex.rxjava2:rxjava:2.2.17"
implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
Expand Down
33 changes: 33 additions & 0 deletions app/src/main/java/com/github/zsoltk/pokedex/common/Observe.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.github.zsoltk.pokedex.common

import androidx.annotation.CheckResult
import androidx.compose.effectOf
import androidx.compose.memo
import androidx.compose.onCommit
import androidx.compose.state
import androidx.lifecycle.LiveData
import androidx.lifecycle.Observer


sealed class AsyncState<T> {
class Initialised<T>: AsyncState<T>()
class Loading<T>: AsyncState<T>()
data class Error<T>(val error: Throwable): AsyncState<T>()
data class Result<T>(val result: T): AsyncState<T>()
}

/**
* Based on https://medium.com/swlh/android-mvi-with-jetpack-compose-b0890f5156ac
*/
@CheckResult(suggest = "+")
fun <T> observe(data: LiveData<T>) = effectOf<T?> {
var result by +state { data.value }
val observer = +memo { Observer<T> { result = it } }

+onCommit(data) {
data.observeForever(observer)
onDispose { data.removeObserver(observer) }
}

result
}
34 changes: 34 additions & 0 deletions app/src/main/java/com/github/zsoltk/pokedex/entity/PokemonApi.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.github.zsoltk.pokedex.entity

import androidx.annotation.FloatRange
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import java.lang.RuntimeException
import java.util.concurrent.TimeUnit
import kotlin.random.Random

object PokemonApi {

/**
* Just for demonstration purposes.
* You could do the same with a proper API.
* You could also do the same with LiveData if you wish.
*/
fun loadPokemon(): Observable<List<Pokemon>> =
Observable
.just(pokemons)
.delay(200, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
.map {
if (Random.nextFloat() < randomFailureChance) throw FakeApiException()
it
}


/**
* Feel free to try different values to test error screen
*/
@FloatRange(from = 0.0, to = 1.0)
val randomFailureChance: Float = 0.1f

class FakeApiException : RuntimeException("Test exception, please ignore")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.github.zsoltk.pokedex.entity

import androidx.lifecycle.MutableLiveData
import com.github.zsoltk.pokedex.common.AsyncState
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable

class PokemonLiveData : MutableLiveData<AsyncState<List<Pokemon>>>() {

init {
reload()
}

private var disposable: Disposable? = null

fun reload() {
disposable?.dispose()
disposable = PokemonApi
.loadPokemon()
.observeOn(AndroidSchedulers.mainThread())
.map { AsyncState.Result(it) as AsyncState<List<Pokemon>> }
.startWith(AsyncState.Loading())
.onErrorReturn { AsyncState.Error(it) }
.subscribe { value = it }
}
}
76 changes: 68 additions & 8 deletions app/src/main/java/com/github/zsoltk/pokedex/pokedex/PokemonList.kt
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package com.github.zsoltk.pokedex.pokedex

import androidx.compose.Composable
import androidx.compose.memo
import androidx.compose.unaryPlus
import androidx.ui.core.Alignment
import androidx.ui.animation.Crossfade
import androidx.ui.core.Opacity
import androidx.ui.core.Text
import androidx.ui.core.dp
Expand All @@ -14,29 +15,41 @@ import androidx.ui.foundation.shape.corner.RoundedCornerShape
import androidx.ui.graphics.Color
import androidx.ui.layout.Column
import androidx.ui.layout.Container
import androidx.ui.layout.ExpandedHeight
import androidx.ui.layout.ExpandedWidth
import androidx.ui.layout.Gravity
import androidx.ui.layout.Height
import androidx.ui.layout.Padding
import androidx.ui.layout.Spacing
import androidx.ui.layout.Stack
import androidx.ui.layout.StackChildren
import androidx.ui.material.Button
import androidx.ui.material.MaterialTheme
import androidx.ui.material.ripple.Ripple
import androidx.ui.material.surface.Surface
import androidx.ui.res.colorResource
import androidx.ui.res.imageResource
import androidx.ui.text.ParagraphStyle
import androidx.ui.text.TextStyle
import androidx.ui.text.font.FontFamily
import androidx.ui.text.font.FontWeight
import androidx.ui.text.style.TextAlign
import androidx.ui.tooling.preview.Preview
import com.github.zsoltk.pokedex.R
import com.github.zsoltk.pokedex.common.AsyncState.Error
import com.github.zsoltk.pokedex.common.AsyncState.Initialised
import com.github.zsoltk.pokedex.common.AsyncState.Loading
import com.github.zsoltk.pokedex.common.AsyncState.Result
import com.github.zsoltk.pokedex.common.PokeBallBackground
import com.github.zsoltk.pokedex.common.PokeBallSmall
import com.github.zsoltk.pokedex.common.PokemonTypeLabels
import com.github.zsoltk.pokedex.common.RotateIndefinitely
import com.github.zsoltk.pokedex.common.TableRenderer
import com.github.zsoltk.pokedex.common.Title
import com.github.zsoltk.pokedex.common.TypeLabelMetrics.Companion.SMALL
import com.github.zsoltk.pokedex.common.observe
import com.github.zsoltk.pokedex.entity.Pokemon
import com.github.zsoltk.pokedex.entity.PokemonApi
import com.github.zsoltk.pokedex.entity.PokemonLiveData
import com.github.zsoltk.pokedex.entity.color
import com.github.zsoltk.pokedex.entity.pokemons
import com.github.zsoltk.pokedex.lightThemeColors
Expand All @@ -46,19 +59,66 @@ interface PokemonList {
companion object {
@Composable
fun Content(onPokemonSelected: (Pokemon) -> Unit) {
Surface {
Stack {
PokeBallBackground()
PokedexContent(onPokemonSelected)
// You could lift this out to higher scope to survive this screen and avoid
// loading every time. Kept here for demonstration purposes only.
val liveData = +memo { PokemonLiveData() }
val asyncState = +observe(liveData)

Stack(modifier = ExpandedHeight wraps ExpandedWidth) {
PokeBallBackground()

expanded {
Crossfade(current = asyncState) {
when (it) {
is Initialised,
is Loading -> LoadingView()
is Error -> ErrorView(onRetryClicked = { liveData.reload() })
is Result -> ContentView(onPokemonSelected)
}
}
}
}
}
}
}

@Composable
private fun StackChildren.PokedexContent(onPokemonSelected: (Pokemon) -> Unit) {
aligned(Alignment.TopLeft) {
private fun LoadingView() {
Container(expanded = true) {
RotateIndefinitely(durationPerRotation = 400) {
Container(width = 50.dp, height = 50.dp) {
PokeBallSmall(tint = +colorResource(R.color.poke_light_red))
}
}
}
}

@Composable
private fun ErrorView(onRetryClicked: () -> Unit) {
val errorRatio = "%.0f".format(PokemonApi.randomFailureChance * 100)

Container(expanded = true) {
Column {
Text(
text = "There's a $errorRatio% chance of a simulated error.\nNow it happened.",
style = (+MaterialTheme.typography()).body1.copy(
color = +colorResource(R.color.poke_red)
),
paragraphStyle = ParagraphStyle(textAlign = TextAlign.Center),
modifier = Spacing(bottom = 16.dp)
)
Button(
modifier = Gravity.Center,
text = "Retry",
onClick = onRetryClicked
)
}
}
}

@Composable
private fun ContentView(onPokemonSelected: (Pokemon) -> Unit) {
Container(expanded = true) {
VerticalScroller {
Padding(padding = 32.dp) {
Column {
Expand Down

0 comments on commit f556839

Please sign in to comment.