Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Google Tech Talk DevLibrary - Exploring an Android Open Source Project : Pokedex

Jaewoong
February 21, 2023

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
Tweet

More Decks by Jaewoong

Other Decks in Programming

Transcript

  1. Pokedex Main Screen • Displays a list of Pokemon. Details

    Screen • Displays information about a selected Pokemon.
  2. Tech Stacks • Material Components • Data Binding (Bindables) •

    RecyclerView • Glide, Palette • TransformationLayout • AndroidRibbon, ProgressView, Rainbow User Interface 01
  3. 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
  4. 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
  5. 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
  6. 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.
  7. 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
  8. 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
  9. <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" />
  10. UI layers must be prepared to react to data changes.

    Architecture - Data Layer Persist remote sources into the local database
  11. Reads are performed from local storage as the single source

    of truth. Architecture - Data Layer Persist remote sources into the local database
  12. 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
  13. 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
  14. 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 { ..
  15. 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)
  16. 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)
  17. 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)
  18. 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)
  19. 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)
  20. 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
  21. 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
  22. 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
  23. 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
  24. @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
  25. 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 ….