Slide 1

Slide 1 text

Introduction Exploring an Android Open-Source Project: Pokedex

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

Open-Source Projects

Slide 4

Slide 4 text

Open-Source Projects

Slide 5

Slide 5 text

Open-Source Projects Learning Building Sharing

Slide 6

Slide 6 text

Pokedex

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

Pokedex github.com/skydoves/pokedex

Slide 9

Slide 9 text

Tech Stacks

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

App Architecture

Slide 14

Slide 14 text

Architecture Overview

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

Architecture - UI Layer data binding

Slide 17

Slide 17 text

Architecture - UI Layer data binding Architecture - UI Layer

Slide 18

Slide 18 text

Architecture - UI Layer data binding

Slide 19

Slide 19 text

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.

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

Architecture - UI Layer

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

activity_detail.xml

Slide 25

Slide 25 text

Architecture - Data Layer Persist remote sources into the local database

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

Architecture - Data Layer Persist remote sources into the local database

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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)

Slide 33

Slide 33 text

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)

Slide 34

Slide 34 text

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)

Slide 35

Slide 35 text

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)

Slide 36

Slide 36 text

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)

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

UI Components

Slide 43

Slide 43 text

UI Components

Slide 44

Slide 44 text

UI Components

Slide 45

Slide 45 text

UI Components

Slide 46

Slide 46 text

@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 activity_detail.xml

Slide 47

Slide 47 text

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

Slide 48

Slide 48 text

App Performance

Slide 49

Slide 49 text

Baseline Profiles Baseline Profiles

Slide 50

Slide 50 text

Baseline Profiles

Slide 51

Slide 51 text

Baseline Profiles

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

Open-Source Projects Build your open-source project!

Slide 55

Slide 55 text

Google Dev Library devlibrary.withgoogle.com/

Slide 56

Slide 56 text

GitHub Trending github.com/trending

Slide 57

Slide 57 text

Thank you!