This Day In History Jetpack Compose App - Part 1

Objective:

Start work on the main screen of the app. Introduce a composable that will display each historical event - HistoryListItem. It will also need a helper function, HistoryListImage, to display the thumbnail image associated with the event. As we don't have a real service developed yet, we will use a mock view model - HistoryViewModelMock, and populate it with sample HistoricalEvent objects.

Components

Add/Modify the following components:

  • HistoryListItem.kt: Add - Basic building block for displaying each historical event
  • HistoryListImage: Add - Helper composable to display the thumnail associated with each historical event
  • HistoryScreen: Modify - Introduce LazyColumn that will display mock data via HistoryListItem
  • HistoryViewModelMock: Modify - Add mock data

Code

History List Image

Add HistoryListImage.kt to the com.coroutines.thisdayinhistory.ui.screens.main package in the app module:


@Composable
inline fun HistoryListImage(
    historyEvent: HistoricalEvent,
    context: Context,
    imageSize: Dp,
    crossinline onImageClick: (HistoricalEvent) -> Unit,
) {
    Image(
        rememberAsyncImagePainter(
            remember(historyEvent.imageUrl) {
                ImageRequest.Builder(context)
                    .data(
                        data = historyEvent.imageUrl
                    )
                    .diskCacheKey(historyEvent.imageUrl)
                    .precision(Precision.EXACT)
                    .memoryCachePolicy(CachePolicy.ENABLED)
                    .memoryCacheKey(historyEvent.imageUrl)
                    .build()
            }
        ),
        contentDescription = "image",
        alignment = Alignment.TopCenter,
        contentScale = ContentScale.Crop,
        modifier = Modifier
            .size(imageSize)
            .clip(MaterialTheme.shapes.small)
            .clickable {
                onImageClick(historyEvent)
            }
    )
}

History List Item

Add HistoryListItem.kt to the com.coroutines.thisdayinhistory.ui.screens.main package in the app module:


@Composable
inline fun HistoryListItem(
    historyEvent: HistoricalEvent,
    crossinline onClick: (HistoricalEvent) -> Unit,
    crossinline onImageClick: (HistoricalEvent) -> Unit,
    crossinline onShare: (ShareableHistoryEvent) -> Unit
) {
    val context = LocalContext.current
    val imageSize = 62.dp
    val keyLine1 = 16.dp
    
    ElevatedCard (
        Modifier
            .padding(start = keyLine1, end = keyLine1, top =  1.dp),
        colors = CardColors(
            containerColor = MaterialTheme.colorScheme.background.darker(0.02f),
            contentColor = MaterialTheme.colorScheme.onBackground,
            disabledContainerColor = MaterialTheme.colorScheme.background,
            disabledContentColor = MaterialTheme.colorScheme.onBackground
        ),
        elevation = CardDefaults.cardElevation(
            defaultElevation = 1.dp
        ))
    {
        Row(
            Modifier
                .testTag(HISTORY_LIST_ITEM_ROW_TAG)
                .padding(start = keyLine1, top = keyLine1 + 17.dp)
        ) {
            HistoryListImage(historyEvent, context, imageSize, onImageClick)

            Column(
                Modifier.padding(
                    start = keyLine1,
                    end = keyLine1
                )
            ) {
                Row(
                    Modifier
                        .fillMaxWidth()
                ) {
                    Text(
                        modifier = Modifier
                            .testTag(HISTORY_LIST_ITEM_TAG)
                            .height(21.dp),
                        text = historyEvent.year.toString(),
                        textAlign = TextAlign.Start,
                        maxLines = 1,
                        style = MaterialTheme.typography.titleLarge
                    )
                }

                Text(
                    text = historyEvent.description,
                    maxLines = 15,
                    lineHeight = 20.sp,
                    style = MaterialTheme.typography.bodyMedium,
                    modifier = Modifier
                        .padding(top = 16.dp, end = keyLine1.minus(12.dp))
                        .clickable(enabled = true) {
                            onClick(historyEvent)
                        }
                )
            }
        }
    }

History Screen

Modify HistoryScreen.kt in the com.coroutines.thisdayinhistory.ui.screens.main package in the app module:

Add LazyColumn that will accept data from our mock HistoryViewModel and display each item.



@Composable
fun HistoryScreen(
    modifier: Modifier = Modifier,
    navController: NavController,
    settingsViewModel : ISettingsViewModel
){

   AppNavigationDrawerWithContent(
       navController = navController,
       settingsViewModel = settingsViewModel
   ) {
       val listState = rememberLazyListState()
       val historyViewModel = HistoryViewModelMock()
       val data = historyViewModel.historyData

       LazyColumn(
           state = listState,
           contentPadding = PaddingValues(bottom = 65.dp),
           verticalArrangement = Arrangement.Top
       ) {
           item { Spacer(Modifier.height(8.dp)) }

           items(data) { item: HistoricalEvent ->
               HistoryListItem(
                   historyEvent = item,
                   onClick = {},
                   onImageClick = {},
                   onShare = {},
               )
           }
           item { Spacer(Modifier.height(20.dp)) }
       }
   }
}

History View Model Mock

Modify HistoryViewModelMock.kt in the com.coroutines.thisdayinhistory.ui.viewmodels package in the app module:

Add a few HistoricalEvent elements to thedata collection.



private data class HistoryStateMock(
    val dataRequestState: DataRequestState = DataRequestState.NotStarted,
    val selectedCategory: String = CatsByLanguage(LangEnum.ENGLISH).getDefaultCategory(),
    val previousCategory: String = CatsByLanguage(LangEnum.ENGLISH).getDefaultCategory(),
    val selectedItem: HistoricalEvent = HistoricalEvent(description = "No Events"),
    val selectedDate: SelectedDate = SelectedDate("March", 1),
    val catsByLanguage: CatsByLanguage = CatsByLanguage(LangEnum.ENGLISH),
    val filter: String = ""
) {
    fun asActivityState() = HistoryViewModelState(
        dataRequestState = dataRequestState,
        selectedCategory = selectedCategory,
        previousCategory = previousCategory,
        selectedItem = selectedItem,
        selectedDate = selectedDate,
        catsByLanguage = catsByLanguage,
        filter =  filter
    )
}
class HistoryViewModelMock : ViewModel(), IHistoryViewModel {
    private val data = mutableStateListOf()
    private val isScrolledState = mutableStateOf(false)
    init{
        data.add(HistoricalEvent(
            description = "The worst day of the tornado outbreak sequence of April 25–28, 2024, with 42 tornadoes, including one confirmed EF4 tornado, and two confirmed EF3 tornadoes, which killed 4 people in total.",
            year = "2023",
            imageBigUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/3/34/Tornado_outbreak_sequence_of_April_25%E2%80%9328%2C_2024.png/320px-Tornado_outbreak_sequence_of_April_25%E2%80%9328%2C_2024.png",
            originalImage = OriginalImage(200, "https://upload.wikimedia.org/wikipedia/commons/thumb/3/34/Tornado_outbreak_sequence_of_April_25%E2%80%9328%2C_2024.png/320px-Tornado_outbreak_sequence_of_April_25%E2%80%9328%2C_2024.png", 200),
            shortTitle = "short title test",
            extract = "extract test",

            ))
        data.add(HistoricalEvent(
            description = "The Panmunjom Declaration is signed between North and South Korea, officially declaring their intentions to end the Korean conflict.",
            countryCodeMappings = buildList {
                CountryCodeMapping("Israel", alpha2 = "KR")
            },
            year = "2022",
            imageBigUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/7/7d/Korea_Summit_2018_v3.jpg/320px-Korea_Summit_2018_v3.jpg",
            originalImage = OriginalImage(200, "https://upload.wikimedia.org/wikipedia/commons/thumb/7/7d/Korea_Summit_2018_v3.jpg/320px-Korea_Summit_2018_v3.jpg", 200),
            shortTitle = "short title test",
            extract = "extract test",

            ))
        data.add(HistoricalEvent(
            description = "Israeli archaeologists discover the tomb of Herod the Great south of Jerusalem.",
            countryCodeMappings = buildList {
                CountryCodeMapping("Israel", alpha2 = "IL")
            },
            year = "1864",
            imageBigUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d4/Flag_of_Israel.svg/320px-Flag_of_Israel.svg.png",
            originalImage = OriginalImage(200, "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d4/Flag_of_Israel.svg/320px-Flag_of_Israel.svg.png", 200),
            shortTitle = "short title test",
            extract = "extract test",

            ))
        data.add(HistoricalEvent(
            description = "Betty Boothroyd becomes the first woman to be elected Speaker of the British House of Commons in its 700-year history.",
            countryCodeMappings = buildList {
                CountryCodeMapping("UK", alpha2 = "UK")
            },
            year = "1864",
            imageBigUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8f/Official_portrait_of_Baroness_Boothroyd_%28cropped%29.jpg/320px-Official_portrait_of_Baroness_Boothroyd_%28cropped%29.jpg",
            originalImage = OriginalImage(200, "https://upload.wikimedia.org/wikipedia/commons/thumb/8/8f/Official_portrait_of_Baroness_Boothroyd_%28cropped%29.jpg/320px-Official_portrait_of_Baroness_Boothroyd_%28cropped%29.jpg", 200),
            shortTitle = "short title test",
            extract = "extract test",

            ))
    }
    private val viewModelState = MutableStateFlow(value = HistoryStateMock())
    override val historyData: SnapshotStateList
        get() = data
    override var isScrolled: MutableState
        get() = isScrolledState
        set(value) {}
    override var filterKey: String
        get() = TODO("Not yet implemented")
        set(value) {}
    override var selectedItem: HistoricalEvent
        get() = data[0]
        set(value) {}
    override val uiState = viewModelState
        .map {it.asActivityState() }
        .stateIn (
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5_000),
            initialValue = viewModelState.value.asActivityState()
        )


    override fun onDateChanged(localDateTime: LocalDateTime) {
        TODO("Not yet implemented")
    }

    override fun updateDate(count: Int) {
        TODO("Not yet implemented")
    }

    override fun onCategoryChanged(optionSelected: String) {
        TODO("Not yet implemented")
    }

    override fun search(searchTerm: String) {
        TODO("Not yet implemented")
    }
}