Skip to content

2022 Profolio Project: multi-module Jetpack Compose app showcasing The Met's impressionist collection. Architecture sample.

Notifications You must be signed in to change notification settings

vrickey123/the-met-impressionist-showcase

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

18 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

the-met-impressionist-showcase

2022 Profolio Project: multi-module Jetpack Compose app showcasing The Met's impressionist collection.

Built with The Metropolitan Museum of Art Collection API.

ShowcaseScreen PaintingScreen

Architecture

Follows a Redux-style architecture where the Result<T> of an API call of is reduced into a UIState<T>. A generic Composable fun <T: UIState> StatefulScreen() renders the UIState into success, loading, or error UI.

@Composable
fun <T : UIState> StatefulScreen(
modifier: Modifier = Modifier,
screenViewModel: ScreenViewModel<T>,
success: @Composable (uiState: T) -> Unit,
) {
val uiState by screenViewModel.state.collectAsState()
when {
uiState.error != null -> {
ErrorScreen(modifier, uiState.error)
}
uiState.loading -> {
LoadingScreen(modifier)
}
else -> {
success(uiState)
}
}
}

A ViewModel implements the ScreenViewModel<T: UIState> and Reducer<T: UIState, Result<D: Any>> interfaces to define the state management API's that emit an immutable StateFlow<UIState> on any change to the underlying data source.

@HiltViewModel
class ShowcaseViewModel @Inject constructor(
@MetRepoImpl val metRepository: MetRepository
) : ViewModel(), ScreenViewModel<ShowcaseUIState>, Reducer<ShowcaseUIState, List<MetObject>> {
companion object {
val TAG by lazy { ShowcaseViewModel::class.java.simpleName }
const val QUERY_IMPRESSIONISM = "impressionism"
val TAGS = listOf<String>("impressionism")
}
// Reducer
// Mutable state of all requests initiated from the ViewModel; i.e. success, error, and loading
override val mutableState: MutableStateFlow<ShowcaseUIState> =
MutableStateFlow(ShowcaseUIState(loading = true))
// Hot Flow of all Result<List<MetObject> from the database. Emits on all changes to DB.
override val stream: Flow<Result<List<MetObject>>> = metRepository.getAllMetObjects()
// ScreenViewModel
// Combines state of our network requests (mutableState) and database stream
override val state: StateFlow<ShowcaseUIState> =
mutableState.combine(stream) { oldState, streamResult ->
reduce(oldState, streamResult)
}.catch {
reduce(mutableState.value, Result.failure(it))
}.stateIn(
scope = viewModelScope,
started = SharingStarted.Lazily,
initialValue = ShowcaseUIState(loading = true)
)
init {
viewModelScope.launch {
Log.d(TAG, "Init: fetch met objects if empty")
emitLoading {
val result = metRepository.prefetchMetObjectsIfEmpty(QUERY_IMPRESSIONISM, TAGS)
result.onFailure { emitError(it) }
}
}
}
/**
* Makes a one-shot API call to [MetRepository.fetchMetObjects] to fetch latest [MetObject]'s
* from the sever.
*
* Note: for this sample app, this is unused because we already have the latest [MetObject]'s in
* local storage. However, this type of architecture would be used if we needed to fetch the
* latest content on every app launch.
* */
fun fetchPaintings(ids: List<Int>) = viewModelScope.launch {
Log.d(TAG, "Fetch showcase")
emitLoading {
val result = metRepository.fetchMetObjects(ids)
result.onFailure { emitError(it) }
}
}
// Reducer
override fun reduce(oldState: ShowcaseUIState, result: Result<List<MetObject>>): ShowcaseUIState {
return result.fold(
onSuccess = { ShowcaseUIState(data = result.getOrDefault(emptyList())) },
onFailure = { ShowcaseUIState(data = oldState.data, error = result.exceptionOrNull()) }
)
}
// Reducer
override fun emitError(e: Throwable) {
viewModelScope.launch {
Log.d(TAG, "Emit error: $e")
mutableState.update { it.copy(error = e) }
}
}
// Reducer
override suspend fun emitLoading(action: suspend () -> Unit) {
Log.e(TAG, "Emit loading")
mutableState.update { it.copy(loading = true) }
action.invoke()
mutableState.update { it.copy(loading = false) }
}
}

Multi-Module

Following the Android Guide to Modularization, the implementation is modularized across app, feature, infrastructure, and core modules. The core modules define low-level API and UIState data models. The infrastructure modules provide reusable platform tools. User-facing Screen destinations are in feature modules and are implemented with a reusable infrastructure:ui_component library. The app module contains the MainActivity and coordinates the Compose NavGraph between Screens. The infrastructure and core modules are suitable for Kotlin multiplatform.

UI Hierarchy

  • Material3 Theme
  • Screen
  • Card
  • Component
  • Material3 Design Token

State Management

UI Hierarchy State Holder State
Screen Android ViewModel StateFlow<UIState>
Card Stateless or Compose State StateFlow<T>
Component Stateless or Compose State StateFlow<T>

Guidance on Android vs Compose API's found in State in Compose documentation.

Automated Tests

The met_network module contains unit tests for the MetRepository. There are two implementations for example purposes:

  1. MetRepositoryImplTestWithMockWebServer
  2. MetRepositoryImplTestWithMockk

Notes on Real World Usage

The Metropolitan Museum of Art Collection API is more of an academic resource than it is a production-ready API suitable for news feeds at scale. It can take 30-45 seconds for a list of our 70-ish MetObject's - the data class representing a painting - to return.

From a backend perspective, the response time could possibly be improved by (1) one batch collection API call that sends a list of ids as a query parameter, (2) a MetObjectMetadata type with a few top-level fields for the work's image link, title, and artist that could drive a Card UI. These would eliminate the need for 70 sequential one-shot MetObject API calls to the `/objects/[objectID]/ endpoint and reduce the time it takes to download the data for a list feed, respectively.

Since we cache the results of the first API call in a Room SQL database, only the first launch to seed the database takes extra time and the user is informed with a loading screen message. For a portfolio sample app that's OK.

screen-20220828-211829-17-5.mp4

About

2022 Profolio Project: multi-module Jetpack Compose app showcasing The Met's impressionist collection. Architecture sample.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published