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. Introduction
    Exploring an Android Open-Source Project: Pokedex

    View Slide

  2. skydoves
    @github_skydoves
    Android Developer Advocate @ Stream
    Jaewoong Eum
    Google Developer Expert

    View Slide

  3. Open-Source Projects

    View Slide

  4. Open-Source Projects

    View Slide

  5. Open-Source Projects
    Learning
    Building
    Sharing

    View Slide

  6. Pokedex

    View Slide

  7. Pokedex
    Main Screen
    ● Displays a list of Pokemon.
    Details Screen
    ● Displays information about a selected Pokemon.

    View Slide

  8. Pokedex
    github.com/skydoves/pokedex

    View Slide

  9. Tech Stacks

    View Slide

  10. Tech Stacks
    ● Material Components
    ● Data Binding (Bindables)
    ● RecyclerView
    ● Glide, Palette
    ● TransformationLayout
    ● AndroidRibbon,
    ProgressView, Rainbow
    User
    Interface
    01

    View Slide

  11. 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

    View Slide

  12. 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

    View Slide

  13. App Architecture

    View Slide

  14. Architecture Overview

    View Slide

  15. 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

    View Slide

  16. Architecture - UI Layer
    data binding

    View Slide

  17. Architecture - UI Layer
    data binding
    Architecture - UI Layer

    View Slide

  18. Architecture - UI Layer
    data binding

    View Slide

  19. 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.

    View Slide

  20. Architecture - UI Layer
    developer.android.com/topic/libraries/data-binding

    View Slide

  21. Architecture - UI Layer

    View Slide

  22. 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 = detailRepository.fetchPokemonInfo(
    name = pokemonName,
    onComplete = { isLoading = false },
    onError = { toastMessage = it }
    )
    @get:Bindable
    val pokemonInfo: PokemonInfo? by pokemonInfoFlow.asBindingProperty(viewModelScope, null)
    DetailViewModel.kt

    View Slide

  23. 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 = detailRepository.fetchPokemonInfo(
    name = pokemonName,
    onComplete = { isLoading = false },
    onError = { toastMessage = it }
    )
    @get:Bindable
    val pokemonInfo: PokemonInfo? by pokemonInfoFlow.asBindingProperty(viewModelScope, null)
    DetailViewModel.kt

    View Slide

  24. 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
    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" />

    View Slide

  25. Architecture - Data Layer
    Persist remote
    sources into the
    local database

    View Slide

  26. UI layers must be prepared
    to react to data changes.
    Architecture - Data Layer
    Persist remote
    sources into the
    local database

    View Slide

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

    View Slide

  28. 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

    View Slide

  29. Architecture - Data Layer
    Persist remote
    sources into the
    local database

    View Slide

  30. interface MainRepository {
    @WorkerThread
    fun fetchPokemonList(page: Int, onStart: () -> Unit, onComplete: () -> Unit, onError: (String?) -> Unit): Flow>
    }
    internal class MainRepositoryImpl @Inject constructor(
    ..
    ) : MainRepository {
    @WorkerThread
    override fun fetchPokemonList(
    page: Int,
    onStart: () -> Unit,
    onComplete: () -> Unit,
    onError: (String?) -> Unit
    ) = flow {
    ..
    MainRepository.kt, MainRepositoryImpl.kt

    View Slide

  31. MainRepository.kt, MainRepositoryImpl.kt
    interface MainRepository {
    @WorkerThread
    fun fetchPokemonList(page: Int, onStart: () -> Unit, onComplete: () -> Unit, onError: (String?) -> Unit): Flow>
    }
    internal class MainRepositoryImpl @Inject constructor(
    ..
    ) : MainRepository {
    @WorkerThread
    override fun fetchPokemonList(
    page: Int,
    onStart: () -> Unit,
    onComplete: () -> Unit,
    onError: (String?) -> Unit
    ) = flow {
    ..

    View Slide

  32. 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)

    View Slide

  33. 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)

    View Slide

  34. 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)

    View Slide

  35. 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)

    View Slide

  36. 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)

    View Slide

  37. 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

    View Slide

  38. 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

    View Slide

  39. 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

    View Slide

  40. Architecture - Data Layer
    github.com/skydoves/sandwich
    getstream.io/blog/modeling-retrofit-responses/

    View Slide

  41. 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

    View Slide

  42. UI Components

    View Slide

  43. UI Components

    View Slide

  44. UI Components

    View Slide

  45. UI Components

    View Slide

  46. @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
    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

    View Slide

  47. UI Components
    developer.android.com/topic/libraries/data-binding/binding-adapters

    View Slide

  48. App Performance

    View Slide

  49. Baseline Profiles
    Baseline Profiles

    View Slide

  50. Baseline Profiles

    View Slide

  51. Baseline Profiles

    View Slide

  52. Baseline Profiles
    Total: 15596 lines (baseline-prof.txt)
    HSPLandroidx/activity/ComponentActivity$$ExternalSyntheticLambda0;->(Landroidx/activity/ComponentActivity;)V
    HSPLandroidx/activity/ComponentActivity$$ExternalSyntheticLambda0;->onContextAvailable(Landroid/content/Context;)V
    HSPLandroidx/activity/ComponentActivity$$ExternalSyntheticLambda1;->(Landroidx/activity/ComponentActivity;)V
    HSPLandroidx/activity/ComponentActivity$$ExternalSyntheticLambda1;->saveState()Landroid/os/Bundle;
    HSPLandroidx/activity/ComponentActivity$$ExternalSyntheticLambda2;->(Landroidx/activity/ComponentActivity;)V
    HSPLandroidx/activity/ComponentActivity$$ExternalSyntheticLambda2;->run()V
    HSPLandroidx/activity/ComponentActivity$1;->(Landroidx/activity/ComponentActivity;)V
    HSPLandroidx/activity/ComponentActivity$2;->(Landroidx/activity/ComponentActivity;)V
    HSPLandroidx/activity/ComponentActivity$3;->(Landroidx/activity/ComponentActivity;)V
    HSPLandroidx/activity/ComponentActivity$3;->onStateChanged(Landroidx/lifecycle/LifecycleOwner;Landroidx/lifecycle/Lifecycle$Event;)V
    HSPLandroidx/activity/ComponentActivity$4;->(Landroidx/activity/ComponentActivity;)V
    HSPLandroidx/activity/ComponentActivity$4;->onStateChanged(Landroidx/lifecycle/LifecycleOwner;Landroidx/lifecycle/Lifecycle$Event;)V
    HSPLandroidx/activity/ComponentActivity$5;->(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;->()V
    HSPLandroidx/activity/ComponentActivity;->addMenuProvider(Landroidx/core/view/MenuProvider;)V
    HSPLandroidx/activity/ComponentActivity;->addOnConfigurationChangedListener(Landroidx/core/util/Consumer;)V
    ….

    View Slide

  53. Baseline Profiles
    developer.android.com/topic/performance/baselineprofiles/overview

    View Slide

  54. Open-Source Projects
    Build your open-source project!

    View Slide

  55. Google Dev Library
    devlibrary.withgoogle.com/

    View Slide

  56. GitHub Trending
    github.com/trending

    View Slide

  57. Thank you!

    View Slide