-
Notifications
You must be signed in to change notification settings - Fork 60
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Jetpack Compose state management sample (#12)
* Create compose state app * Create pokemon data repository + Unit tests * Create pokemon ViewModel + Unit tests * Set up app's UI * Add art to sample * Create README.md * Remove unused dependencies + example android test
- Loading branch information
1 parent
1cd8f1f
commit 804fc7e
Showing
39 changed files
with
1,609 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
*.iml | ||
.gradle | ||
/local.properties | ||
/.idea | ||
.DS_Store | ||
/build | ||
/captures | ||
.externalNativeBuild | ||
.cxx | ||
local.properties |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
# Jetpack Compose State | ||
|
||
Android sample app to learn about managing state when using Jetpack Compose. The app is a Pokedex that locally loads a list of pokemons and displays it on the UI, the data is paged, and the UI state is controlled by a [ViewModel](https://developer.android.com/topic/libraries/architecture/viewmodel). | ||
|
||
![Jetpack compose state - Sample](https://github.com/husaynhakeem/android-playground/blob/master/ComposeStateSample/art/android-jetpack-compose-state-sample.png) | ||
|
||
The sample mainly showcases: | ||
- Using a unidirectional data flow with ViewModel and Jetpack Compose | ||
- Using stateless Composables | ||
- Using stateful Composables via [`remember`](https://developer.android.com/reference/kotlin/androidx/compose/runtime/package-summary#remember) to internally store immutable values and handle state in a composable | ||
- Creating custom layouts/composables. E.g. Creating a custom [lazy grid layout](https://github.com/husaynhakeem/android-playground/blob/master/ComposeStateSample/app/src/main/java/com/husaynhakeem/composestatesample/widget/LazyGridForIndexed.kt), the equivalent of [RecyclerView](https://developer.android.com/reference/kotlin/androidx/recyclerview/widget/RecyclerView) + [GridLayoutManager](https://developer.android.com/reference/kotlin/androidx/recyclerview/widget/GridLayoutManager). | ||
- Loading paged data in a Composable and triggering recomposition. | ||
|
||
### Resources used in this sample | ||
- [Jetpack Compose - Managing state](https://developer.android.com/jetpack/compose/state) | ||
- [List pagination with Jetpack Compose](https://medium.com/schibsted-tech-polska/list-pagination-with-jetpack-compose-6c25da053858) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
/build |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
plugins { | ||
id "com.android.application" | ||
id "kotlin-android" | ||
} | ||
|
||
android { | ||
compileSdkVersion 30 | ||
|
||
defaultConfig { | ||
applicationId "com.husaynhakeem.composestatesample" | ||
minSdkVersion 21 | ||
targetSdkVersion 30 | ||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" | ||
} | ||
compileOptions { | ||
sourceCompatibility JavaVersion.VERSION_1_8 | ||
targetCompatibility JavaVersion.VERSION_1_8 | ||
} | ||
kotlinOptions { | ||
jvmTarget = "1.8" | ||
useIR = true | ||
} | ||
buildFeatures { | ||
compose true | ||
} | ||
composeOptions { | ||
kotlinCompilerExtensionVersion compose_version | ||
kotlinCompilerVersion "1.4.10" | ||
} | ||
} | ||
|
||
dependencies { | ||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" | ||
implementation "androidx.core:core-ktx:1.3.2" | ||
implementation "androidx.appcompat:appcompat:1.2.0" | ||
implementation "com.google.android.material:material:1.2.1" | ||
implementation "androidx.compose.ui:ui:$compose_version" | ||
implementation "androidx.compose.runtime:runtime-livedata:$compose_version" | ||
implementation "androidx.compose.material:material:$compose_version" | ||
implementation "androidx.ui:ui-tooling:$compose_version" | ||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-beta01" | ||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.0-beta01" | ||
implementation "dev.chrisbanes.accompanist:accompanist-coil:0.3.2" | ||
implementation "androidx.palette:palette-ktx:1.0.0" | ||
testImplementation "junit:junit:4.13.1" | ||
testImplementation "com.google.truth:truth:1.1" | ||
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.4.1' | ||
testImplementation "androidx.arch.core:core-testing:2.1.0" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
<?xml version="1.0" encoding="utf-8"?> | ||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" | ||
package="com.husaynhakeem.composestatesample"> | ||
|
||
<uses-permission android:name="android.permission.INTERNET" /> | ||
|
||
<application | ||
android:allowBackup="true" | ||
android:label="@string/app_name" | ||
android:supportsRtl="true" | ||
android:theme="@style/Theme.ComposeStateSample"> | ||
<activity | ||
android:name=".MainActivity" | ||
android:label="@string/app_name" | ||
android:theme="@style/Theme.ComposeStateSample.NoActionBar"> | ||
<intent-filter> | ||
<action android:name="android.intent.action.MAIN" /> | ||
|
||
<category android:name="android.intent.category.LAUNCHER" /> | ||
</intent-filter> | ||
</activity> | ||
</application> | ||
|
||
</manifest> |
47 changes: 47 additions & 0 deletions
47
ComposeStateSample/app/src/main/java/com/husaynhakeem/composestatesample/MainActivity.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package com.husaynhakeem.composestatesample | ||
|
||
import android.os.Bundle | ||
import androidx.appcompat.app.AppCompatActivity | ||
import androidx.compose.foundation.Text | ||
import androidx.compose.material.Scaffold | ||
import androidx.compose.material.TopAppBar | ||
import androidx.compose.ui.platform.setContent | ||
import androidx.compose.ui.unit.dp | ||
import androidx.lifecycle.ViewModelProvider | ||
import coil.Coil | ||
import coil.ImageLoader | ||
import coil.util.DebugLogger | ||
import com.husaynhakeem.composestatesample.ui.ComposeStateSampleTheme | ||
|
||
class MainActivity : AppCompatActivity() { | ||
override fun onCreate(savedInstanceState: Bundle?) { | ||
super.onCreate(savedInstanceState) | ||
|
||
// Set debug logger to monitor image loading logs | ||
Coil.setImageLoader( | ||
ImageLoader.Builder(this) | ||
.logger(DebugLogger()) | ||
.build() | ||
) | ||
|
||
// Set up viewModel and UI | ||
val factory = MainViewModel.Factory() | ||
val viewModel = ViewModelProvider(this, factory).get(MainViewModel::class.java) | ||
setContent { | ||
ComposeStateSampleTheme { | ||
Scaffold( | ||
topBar = { | ||
TopAppBar( | ||
title = { | ||
Text(text = "Pokedex - Jetpack Compose") | ||
}, | ||
elevation = 12.dp | ||
) | ||
} | ||
) { | ||
PokemonsLayout(viewModel) | ||
} | ||
} | ||
} | ||
} | ||
} |
67 changes: 67 additions & 0 deletions
67
ComposeStateSample/app/src/main/java/com/husaynhakeem/composestatesample/MainViewModel.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package com.husaynhakeem.composestatesample | ||
|
||
import androidx.lifecycle.* | ||
import com.husaynhakeem.composestatesample.data.Pokemon | ||
import com.husaynhakeem.composestatesample.data.PokemonRepository | ||
import com.husaynhakeem.composestatesample.data.impl.InMemoryPokemonContainer | ||
import com.husaynhakeem.composestatesample.data.impl.InternalPokemonRepository | ||
import kotlinx.coroutines.launch | ||
import java.util.concurrent.atomic.AtomicBoolean | ||
import java.util.concurrent.atomic.AtomicInteger | ||
|
||
class MainViewModel(private val pokemonRepository: PokemonRepository) : ViewModel() { | ||
|
||
private val _state = MutableLiveData(State()) | ||
val state: LiveData<State> = _state | ||
|
||
// Todo: Group inside pagination state | ||
private val currentPage = AtomicInteger(INITIAL_PAGE) | ||
private val totalPages = AtomicInteger(INITIAL_TOTAL_PAGES) | ||
private val isQueryInProgress = AtomicBoolean(false) | ||
|
||
fun loadPokemons() { | ||
// Do not load more pokemons if they have already all been loaded, or if a query is in | ||
// progress. | ||
if (currentPage.get() > totalPages.get() || isQueryInProgress.get()) { | ||
return | ||
} | ||
|
||
viewModelScope.launch { | ||
_state.postValue(State(getPokemonsFromState(), true)) | ||
isQueryInProgress.set(true) | ||
|
||
val pokemonResult = pokemonRepository.getPokemonResultFor(currentPage.get(), PAGE_SIZE) | ||
|
||
// Update current page and total pages from the response | ||
currentPage.set(pokemonResult.currentPage + 1) | ||
totalPages.set(pokemonResult.totalPages) | ||
|
||
// Add list of pokemons from response to current list of pokemons, then update state | ||
_state.postValue(State(getPokemonsFromState() + pokemonResult.items, false)) | ||
|
||
isQueryInProgress.set(false) | ||
} | ||
} | ||
|
||
private fun getPokemonsFromState() = _state.value!!.pokemons.toList() | ||
|
||
data class State(val pokemons: List<Pokemon> = emptyList(), val isLoading: Boolean = false) { | ||
|
||
fun isInitialState() = pokemons.isEmpty() && !isLoading | ||
} | ||
|
||
class Factory : ViewModelProvider.Factory { | ||
@Suppress("UNCHECKED_CAST") | ||
override fun <T : ViewModel?> create(modelClass: Class<T>): T { | ||
val container = InMemoryPokemonContainer() | ||
val repository = InternalPokemonRepository(container) | ||
return MainViewModel(repository) as T | ||
} | ||
} | ||
|
||
companion object { | ||
private const val INITIAL_PAGE = 1 | ||
private const val INITIAL_TOTAL_PAGES = Int.MAX_VALUE | ||
private const val PAGE_SIZE = 10 | ||
} | ||
} |
47 changes: 47 additions & 0 deletions
47
ComposeStateSample/app/src/main/java/com/husaynhakeem/composestatesample/PokemonList.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package com.husaynhakeem.composestatesample | ||
|
||
import androidx.compose.foundation.layout.Box | ||
import androidx.compose.foundation.layout.fillMaxSize | ||
import androidx.compose.foundation.layout.padding | ||
import androidx.compose.material.CircularProgressIndicator | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.getValue | ||
import androidx.compose.runtime.livedata.observeAsState | ||
import androidx.compose.ui.Alignment | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.unit.dp | ||
import androidx.ui.tooling.preview.Preview | ||
import com.husaynhakeem.composestatesample.ui.ComposeStateSampleTheme | ||
import com.husaynhakeem.composestatesample.widget.PokemonsGrid | ||
|
||
@Composable | ||
fun PokemonsLayout(viewModel: MainViewModel) { | ||
val observedState by viewModel.state.observeAsState() | ||
val state = observedState ?: return | ||
if (state.isInitialState()) { | ||
viewModel.loadPokemons() | ||
} | ||
|
||
Box(modifier = Modifier.fillMaxSize()) { | ||
|
||
// Pokemons | ||
PokemonsGrid(pokemons = state.pokemons, onLoadMorePokemons = { viewModel.loadPokemons() }) | ||
|
||
// Loading | ||
if (state.isLoading) { | ||
CircularProgressIndicator( | ||
Modifier | ||
.align(alignment = Alignment.Center) | ||
.padding(80.dp) | ||
) | ||
} | ||
} | ||
} | ||
|
||
|
||
@Preview | ||
@Composable | ||
fun PokemonListPreview() { | ||
ComposeStateSampleTheme { | ||
} | ||
} |
13 changes: 13 additions & 0 deletions
13
ComposeStateSample/app/src/main/java/com/husaynhakeem/composestatesample/data/Models.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package com.husaynhakeem.composestatesample.data | ||
|
||
data class PokemonResult( | ||
val currentPage: Int, | ||
val totalPages: Int, | ||
val items: List<Pokemon> | ||
) | ||
|
||
data class Pokemon( | ||
val id: Int, | ||
val name: String, | ||
val spriteUrl: String | ||
) |
6 changes: 6 additions & 0 deletions
6
...tateSample/app/src/main/java/com/husaynhakeem/composestatesample/data/PokemonContainer.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package com.husaynhakeem.composestatesample.data | ||
|
||
interface PokemonContainer { | ||
|
||
fun get(): List<Pokemon> | ||
} |
6 changes: 6 additions & 0 deletions
6
...ateSample/app/src/main/java/com/husaynhakeem/composestatesample/data/PokemonRepository.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
package com.husaynhakeem.composestatesample.data | ||
|
||
interface PokemonRepository { | ||
|
||
suspend fun getPokemonResultFor(page: Int, pageSize: Int): PokemonResult | ||
} |
18 changes: 18 additions & 0 deletions
18
...e/app/src/main/java/com/husaynhakeem/composestatesample/data/fake/FakePokemonContainer.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package com.husaynhakeem.composestatesample.data.fake | ||
|
||
import com.husaynhakeem.composestatesample.data.Pokemon | ||
import com.husaynhakeem.composestatesample.data.PokemonContainer | ||
|
||
class FakePokemonContainer : PokemonContainer { | ||
|
||
private val allPokemon: MutableList<Pokemon> = mutableListOf() | ||
|
||
override fun get(): List<Pokemon> { | ||
return allPokemon.toList() | ||
} | ||
|
||
fun setAllPokemon(allPokemon: List<Pokemon>) { | ||
this.allPokemon.clear() | ||
this.allPokemon.addAll(allPokemon) | ||
} | ||
} |
Oops, something went wrong.