Share
Embed
Google Tech Talk DevLibrary - Exploring an Android Open Source Project : Pokedex
In this tech talk, Google Dev Library contributor Jaewoong Eum discusses Pokedex, which follows Google's official Android architecture guidance and demonstrates modern Android development with Hilt, Coroutines, Flow, Jetpack (Room, ViewModel), and Material Design based on MVVM architecture. The repository includes the app's layout, features, functionality, and documentation on how to implement and utilize its resources.
Jaewoong
February 21, 2023
Transcript
57 slides
-
Pokedex Main Screen • Displays a list of Pokemon. Details
Screen • Displays information about a selected Pokemon. -
Tech Stacks • Material Components • Data Binding (Bindables) •
RecyclerView • Glide, Palette • TransformationLayout • AndroidRibbon, ProgressView, Rainbow User Interface 01 -
Tech Stacks • Material Components • Data Binding (Bindables) •
RecyclerView • Glide, Palette • TransformationLayout • AndroidRibbon, ProgressView, Rainbow User Interface 01 • Room Database • Retrofit • Moshi (Serialization) • Kotlin Coroutines, Flow • Sandwich Business Logic 02 -
Tech Stacks • Material Components • Data Binding (Bindables) •
RecyclerView • Glide, Palette • TransformationLayout • AndroidRibbon, ProgressView, Rainbow User Interface 01 • Room Database • Retrofit • Moshi (Serialization) • Kotlin Coroutines, Flow • Sandwich Business Logic 02 • Hilt • App Startup • KSP • Baseline Profiles • Macrobenchmark Other Jetpacks 03 -
Architecture Overview Unidirectional Data Flow • Higher layers react to
changes in lower layers. • Events flow down. • Data flows up with data streams. (Kotlin Flows) Unidirectional Event Flow -
Architecture - UI Layer Transforming streams into bindable data •
View models receive streams of data as Flows from data layers. • The single flow is converted to a bindable data, which can be observed and updates UI elements. Preserve data across configuration changes • View models hold data and restore the states of UI elements from configuration changes. Processing user interactions (Unidirectional event flow) • User actions are communicated from UI elements to view models. • View models execute business logic following the user interactions. -
class DetailViewModel @AssistedInject constructor( detailRepository: DetailRepository, @Assisted private val pokemonName:
String ) : BindingViewModel() { @get:Bindable var isLoading: Boolean by bindingProperty(true) private set private val pokemonInfoFlow: Flow<PokemonInfo?> = detailRepository.fetchPokemonInfo( name = pokemonName, onComplete = { isLoading = false }, onError = { toastMessage = it } ) @get:Bindable val pokemonInfo: PokemonInfo? by pokemonInfoFlow.asBindingProperty(viewModelScope, null) DetailViewModel.kt -
class DetailViewModel @AssistedInject constructor( detailRepository: DetailRepository, @Assisted private val pokemonName:
String ) : BindingViewModel() { @get:Bindable var isLoading: Boolean by bindingProperty(true) private set private val pokemonInfoFlow: Flow<PokemonInfo?> = detailRepository.fetchPokemonInfo( name = pokemonName, onComplete = { isLoading = false }, onError = { toastMessage = it } ) @get:Bindable val pokemonInfo: PokemonInfo? by pokemonInfoFlow.asBindingProperty(viewModelScope, null) DetailViewModel.kt -
<androidx.appcompat.widget.AppCompatImageView android:id="@+id/image" android:layout_width="190dp" android:layout_height="190dp" android:layout_marginBottom="20dp" android:scaleType="center" android:translationZ="100dp" app:layout_constraintBottom_toBottomOf="@id/header" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent"
app:paletteImage="@{pokemon.imageUrl}" app:paletteView="@{header}" /> activity_detail.xml <com.skydoves.progressview.ProgressView android:id="@+id/progress_hp" android:layout_width="0dp" android:layout_height="18dp" android:layout_marginStart="32dp" android:layout_marginEnd="32dp" app:layout_constraintBottom_toBottomOf="@id/hp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/hp" app:layout_constraintTop_toTopOf="@id/hp" app:progressView_colorBackground="@color/white" app:progressView_colorProgress="@color/colorPrimary" app:progressView_labelColorInner="@color/white" app:progressView_labelColorOuter="@color/black" app:progressView_labelSize="12sp" app:progressView_labelText="@{vm.pokemonInfo.hpString}" app:progressView_max="@{vm.pokemonInfo.maxHp}" app:progressView_progress="@{vm.pokemonInfo.hp}" app:progressView_radius="12dp" /> -
UI layers must be prepared to react to data changes.
Architecture - Data Layer Persist remote sources into the local database -
Reads are performed from local storage as the single source
of truth. Architecture - Data Layer Persist remote sources into the local database -
Architecture - Data Layer A single source of truth principle
is a philosophy of aggregating data from many systems within an organization to a single location. developer.android.com/topic/architecture#single-source-of-truth -
interface MainRepository { @WorkerThread fun fetchPokemonList(page: Int, onStart: () ->
Unit, onComplete: () -> Unit, onError: (String?) -> Unit): Flow<List<Pokemon>> } internal class MainRepositoryImpl @Inject constructor( .. ) : MainRepository { @WorkerThread override fun fetchPokemonList( page: Int, onStart: () -> Unit, onComplete: () -> Unit, onError: (String?) -> Unit ) = flow { .. MainRepository.kt, MainRepositoryImpl.kt -
MainRepository.kt, MainRepositoryImpl.kt interface MainRepository { @WorkerThread fun fetchPokemonList(page: Int, onStart:
() -> Unit, onComplete: () -> Unit, onError: (String?) -> Unit): Flow<List<Pokemon>> } internal class MainRepositoryImpl @Inject constructor( .. ) : MainRepository { @WorkerThread override fun fetchPokemonList( page: Int, onStart: () -> Unit, onComplete: () -> Unit, onError: (String?) -> Unit ) = flow { .. -
MainRepositoryImpl.kt @WorkerThread override fun fetchPokemonList(page: Int, onStart: () -> Unit,
onComplete: () -> Unit, onError: (String?) -> Unit) = flow { var pokemons = pokemonDao.getPokemonList(page).asDomain() if (pokemons.isEmpty()) { val response = pokedexClient.fetchPokemonList(page = page) response.suspendOnSuccess { pokemons = data.results pokemons.forEach { pokemon -> pokemon.page = page } pokemonDao.insertPokemonList(pokemons.asEntity()) emit(pokemonDao.getAllPokemonList(page).asDomain()) }.onFailure { // handles the all error cases from the API request fails. onError(message()) } } else { emit(pokemonDao.getAllPokemonList(page).asDomain()) } }.onStart { onStart() }.onCompletion { onComplete() }.flowOn(ioDispatcher) -
MainRepositoryImpl.kt @WorkerThread override fun fetchPokemonList(page: Int, onStart: () -> Unit,
onComplete: () -> Unit, onError: (String?) -> Unit) = flow { var pokemons = pokemonDao.getPokemonList(page).asDomain() if (pokemons.isEmpty()) { val response = pokedexClient.fetchPokemonList(page = page) response.suspendOnSuccess { pokemons = data.results pokemons.forEach { pokemon -> pokemon.page = page } pokemonDao.insertPokemonList(pokemons.asEntity()) emit(pokemonDao.getAllPokemonList(page).asDomain()) }.onFailure { // handles the all error cases from the API request fails. onError(message()) } } else { emit(pokemonDao.getAllPokemonList(page).asDomain()) } }.onStart { onStart() }.onCompletion { onComplete() }.flowOn(ioDispatcher) -
MainRepositoryImpl.kt @WorkerThread override fun fetchPokemonList(page: Int, onStart: () -> Unit,
onComplete: () -> Unit, onError: (String?) -> Unit) = flow { var pokemons = pokemonDao.getPokemonList(page).asDomain() if (pokemons.isEmpty()) { val response = pokedexClient.fetchPokemonList(page = page) response.suspendOnSuccess { pokemons = data.results pokemons.forEach { pokemon -> pokemon.page = page } pokemonDao.insertPokemonList(pokemons.asEntity()) emit(pokemonDao.getAllPokemonList(page).asDomain()) }.onFailure { // handles the all error cases from the API request fails. onError(message()) } } else { emit(pokemonDao.getAllPokemonList(page).asDomain()) } }.onStart { onStart() }.onCompletion { onComplete() }.flowOn(ioDispatcher) -
MainRepositoryImpl.kt @WorkerThread override fun fetchPokemonList(page: Int, onStart: () -> Unit,
onComplete: () -> Unit, onError: (String?) -> Unit) = flow { var pokemons = pokemonDao.getPokemonList(page).asDomain() if (pokemons.isEmpty()) { val response = pokedexClient.fetchPokemonList(page = page) response.suspendOnSuccess { pokemons = data.results pokemons.forEach { pokemon -> pokemon.page = page } pokemonDao.insertPokemonList(pokemons.asEntity()) emit(pokemonDao.getAllPokemonList(page).asDomain()) }.onFailure { // handles the all error cases from the API request fails. onError(message()) } } else { emit(pokemonDao.getAllPokemonList(page).asDomain()) } }.onStart { onStart() }.onCompletion { onComplete() }.flowOn(ioDispatcher) -
MainRepositoryImpl.kt @WorkerThread override fun fetchPokemonList(page: Int, onStart: () -> Unit,
onComplete: () -> Unit, onError: (String?) -> Unit) = flow { var pokemons = pokemonDao.getPokemonList(page).asDomain() if (pokemons.isEmpty()) { val response = pokedexClient.fetchPokemonList(page = page) response.suspendOnSuccess { pokemons = data.results pokemons.forEach { pokemon -> pokemon.page = page } pokemonDao.insertPokemonList(pokemons.asEntity()) emit(pokemonDao.getAllPokemonList(page).asDomain()) }.onFailure { // handles the all error cases from the API request fails. onError(message()) } } else { emit(pokemonDao.getAllPokemonList(page).asDomain()) } }.onStart { onStart() }.onCompletion { onComplete() }.flowOn(ioDispatcher) -
Architecture - Data Layer Data Repository Remote DataSource Local DataSource
Response Success - body - headers - status code Failure - error body - headers - status code Exception - IOException - UnKnownHostException - SSLHandshakeException - … Error -
Architecture - Data Layer Data Repository Remote DataSource Local DataSource
Response Success - body - headers - status code Failure - error body - headers - status code Exception - IOException - UnKnownHostException - SSLHandshakeException - … ApiResponse (=Sealed Classes) Error -
Architecture - Data Layer Domain Interactors, Use case Interfaces Threading
Presentation States View ViewModel Data Repository Remote DataSource Local DataSource Error Error try-catch Success Success -
The recommendations and best practices present in this architecture can
be applied to a broad spectrum of apps to allow them to scale, improve quality and robustness, and make them easier to test. However, you should treat them as guidelines and adapt them to your requirements as needed. Guide to app architecture developer.android.com/topic/architecture -
@JvmStatic @BindingAdapter("progressView_labelText") fun bindProgressViewLabelText(progressView: ProgressView, text: String?) { progressView.labelText =
text } @JvmStatic @BindingAdapter("progressView_progress") fun bindProgressViewProgress(progressView: ProgressView, value: Int?) { if (value != null) { progressView.progress = value.toFloat() } } ViewBinding.kt <com.skydoves.progressview.ProgressView android:id="@+id/progress_hp" android:layout_width="0dp" android:layout_height="18dp" android:layout_marginStart="32dp" android:layout_marginEnd="32dp" app:layout_constraintBottom_toBottomOf="@id/hp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toEndOf="@id/hp" app:layout_constraintTop_toTopOf="@id/hp" app:progressView_colorBackground="@color/white" app:progressView_colorProgress="@color/colorPrimary" app:progressView_labelColorInner="@color/white" app:progressView_labelColorOuter="@color/black" app:progressView_labelSize="12sp" app:progressView_labelText="@{vm.pokemonInfo.hpString}" app:progressView_max="@{vm.pokemonInfo.maxHp}" app:progressView_progress="@{vm.pokemonInfo.hp}" app:progressView_radius="12dp" /> activity_detail.xml -
Baseline Profiles Total: 15596 lines (baseline-prof.txt) HSPLandroidx/activity/ComponentActivity$$ExternalSyntheticLambda0;-><init>(Landroidx/activity/ComponentActivity;)V HSPLandroidx/activity/ComponentActivity$$ExternalSyntheticLambda0;->onContextAvailable(Landroid/content/Context;)V HSPLandroidx/activity/ComponentActivity$$ExternalSyntheticLambda1;-><init>(Landroidx/activity/ComponentActivity;)V HSPLandroidx/activity/ComponentActivity$$ExternalSyntheticLambda1;->saveState()Landroid/os/Bundle;
HSPLandroidx/activity/ComponentActivity$$ExternalSyntheticLambda2;-><init>(Landroidx/activity/ComponentActivity;)V HSPLandroidx/activity/ComponentActivity$$ExternalSyntheticLambda2;->run()V HSPLandroidx/activity/ComponentActivity$1;-><init>(Landroidx/activity/ComponentActivity;)V HSPLandroidx/activity/ComponentActivity$2;-><init>(Landroidx/activity/ComponentActivity;)V HSPLandroidx/activity/ComponentActivity$3;-><init>(Landroidx/activity/ComponentActivity;)V HSPLandroidx/activity/ComponentActivity$3;->onStateChanged(Landroidx/lifecycle/LifecycleOwner;Landroidx/lifecycle/Lifecycle$Event;)V HSPLandroidx/activity/ComponentActivity$4;-><init>(Landroidx/activity/ComponentActivity;)V HSPLandroidx/activity/ComponentActivity$4;->onStateChanged(Landroidx/lifecycle/LifecycleOwner;Landroidx/lifecycle/Lifecycle$Event;)V HSPLandroidx/activity/ComponentActivity$5;-><init>(Landroidx/activity/ComponentActivity;)V HSPLandroidx/activity/ComponentActivity$5;->onStateChanged(Landroidx/lifecycle/LifecycleOwner;Landroidx/lifecycle/Lifecycle$Event;)V HSPLandroidx/activity/ComponentActivity$Api19Impl;->cancelPendingInputEvents(Landroid/view/View;)V HSPLandroidx/activity/ComponentActivity;-><init>()V HSPLandroidx/activity/ComponentActivity;->addMenuProvider(Landroidx/core/view/MenuProvider;)V HSPLandroidx/activity/ComponentActivity;->addOnConfigurationChangedListener(Landroidx/core/util/Consumer;)V ….