Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Access the latest APK for Kotlin Dictionary from the link below.
- [ ] Add a `Contributors Page` to showcase project contributors
- [ ] Add a `Settings Page` with basic preferences
- [ ] Implement a `Splash Screen`
- [x] Integrate multiplatform paging for `Topic Screen`

---

Expand Down
3 changes: 3 additions & 0 deletions composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ kotlin {
implementation(libs.generativeai)
implementation(compose.uiTooling)
implementation(libs.ktor.client.okhttp)
implementation("androidx.paging:paging-compose:3.3.0-alpha02")
}
commonMain.dependencies {
implementation(compose.runtime)
Expand Down Expand Up @@ -117,6 +118,8 @@ kotlin {
implementation(libs.arrow.core)
implementation(libs.arrow.fx.coroutines)
implementation(project(":design-system"))
implementation(libs.cashapp.paging.common)
implementation(libs.cashapp.paging.compose.common)
}
desktopMain.dependencies {
implementation(compose.desktop.currentOs)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.developersbreach.kotlindictionarymultiplatform.previews

import app.cash.paging.PagingData
import com.developersbreach.kotlindictionarymultiplatform.data.detail.model.CodeExample
import com.developersbreach.kotlindictionarymultiplatform.data.detail.model.KotlinTopicDetails
import com.developersbreach.kotlindictionarymultiplatform.data.detail.model.Section
import com.developersbreach.kotlindictionarymultiplatform.data.detail.model.Syntax
import com.developersbreach.kotlindictionarymultiplatform.data.topic.model.Topic
import com.developersbreach.kotlindictionarymultiplatform.ui.screens.topic.ItemTopic
import com.developersbreach.kotlindictionarymultiplatform.ui.screens.topic.TopicUi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf

internal fun sampleCodeSnippet(): String {
return """
Expand Down Expand Up @@ -76,12 +79,16 @@ private fun sampleTopicList(): List<Topic> {
)
}

internal fun sampleTopicUiList(): List<ItemTopic> {
internal fun sampleTopicUiList(): List<TopicUi> {
return sampleTopicList().map { topic ->
ItemTopic(
TopicUi(
name = topic.name ?: "",
initial = topic.name?.firstOrNull()?.uppercase() ?: "",
description = topic.description ?: "",
)
}
}

internal fun samplePagingData(): Flow<PagingData<TopicUi>> {
return flowOf(PagingData.from(sampleTopicUiList()))
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@ package com.developersbreach.kotlindictionarymultiplatform.previews.topic

import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.PreviewLightDark
import com.developersbreach.kotlindictionarymultiplatform.previews.sampleTopicUiList
import app.cash.paging.compose.collectAsLazyPagingItems
import com.developersbreach.kotlindictionarymultiplatform.previews.samplePagingData
import com.developersbreach.kotlindictionarymultiplatform.ui.screens.topic.TopicScreenUI
import com.developersbreach.kotlindictionarymultiplatform.ui.theme.KotlinDictionaryTheme

@PreviewLightDark
@Composable
private fun TopicScreenPreview() {
KotlinDictionaryTheme {
val pagingItems = samplePagingData().collectAsLazyPagingItems()
TopicScreenUI(
topics = sampleTopicUiList(),
topics = pagingItems,
searchQuery = "Search",
onQueryChange = { },
onTopicClick = { },
Expand Down
Original file line number Diff line number Diff line change
@@ -1,21 +1,44 @@
package com.developersbreach.kotlindictionarymultiplatform.data.topic.repository

import arrow.core.Either
import arrow.core.getOrElse
import com.developersbreach.kotlindictionarymultiplatform.data.topic.model.TopicResponse
import com.developersbreach.kotlindictionarymultiplatform.data.topic.model.Topic
import com.developersbreach.kotlindictionarymultiplatform.data.topic.model.toTopic
import com.developersbreach.kotlindictionarymultiplatform.core.network.topicSource.FirestoreConstants
import com.developersbreach.kotlindictionarymultiplatform.ui.screens.topic.TopicUi
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get

class TopicRepository(
private val httpClient: HttpClient,
) {
suspend fun getTopics(): Either<Throwable, List<Topic>> {
private suspend fun getTopics(): Either<Throwable, List<Topic>> {
return Either.catch {
val topicResponse: TopicResponse = httpClient.get(FirestoreConstants.TOPICS_URL).body()
topicResponse.topics.map { it.toTopic() }
}
}

suspend fun getTopicsPage(
page: Int,
pageSize: Int,
query: String,
): List<TopicUi> {
val allTopics = getTopics().getOrElse { emptyList() }
val filteredTopics = allTopics
.filter { it.name?.contains(query, ignoreCase = true) == true }
.sortedBy { it.name?.lowercase() ?: "" }
.map { topic ->
TopicUi(
name = topic.name ?: "",
initial = topic.name?.firstOrNull()?.uppercase() ?: "",
description = topic.description ?: "",
)
}
val fromIndex = (page - 1) * pageSize
val toIndex = (fromIndex + pageSize).coerceAtMost(filteredTopics.size)
return if (fromIndex < filteredTopics.size) filteredTopics.subList(fromIndex, toIndex) else emptyList()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import com.developersbreach.designsystem.components.KdText

@Composable
fun TopicCard(
itemTopic: ItemTopic,
topicUI: TopicUi,
topic: String,
description: String,
onCardClick: () -> Unit,
Expand Down Expand Up @@ -60,7 +60,7 @@ fun TopicCard(
) {
KdText(
modifier = Modifier,
text = itemTopic.initial,
text = topicUI.initial,
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,30 @@ package com.developersbreach.kotlindictionarymultiplatform.ui.screens.topic
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import app.cash.paging.compose.LazyPagingItems

@Composable
fun TopicList(
topics: List<ItemTopic>,
topics: LazyPagingItems<TopicUi>,
onTopicClick: (String) -> Unit,
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = 40.dp),
) {
items(topics) { topic ->
TopicCard(
topic = topic.name,
itemTopic = topic,
description = topic.description,
onCardClick = { onTopicClick(topic.name) },
)
items(topics.itemCount) { index ->
val topic = topics[index]
topic?.let {
TopicCard(
topic = it.name,
topicUI = it,
description = it.description,
onCardClick = { onTopicClick(it.name) },
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.developersbreach.kotlindictionarymultiplatform.ui.screens.topic

import app.cash.paging.PagingSource
import app.cash.paging.PagingState
import com.developersbreach.kotlindictionarymultiplatform.data.topic.repository.TopicRepository

class TopicPagingSource(
private val repository: TopicRepository,
private val query: String,
) : PagingSource<Int, TopicUi>() {

override suspend fun load(
params: LoadParams<Int>,
): LoadResult<Int, TopicUi> {
val page = params.key ?: 1
val pageSize = params.loadSize
return try {
val pageItems = repository.getTopicsPage(page, pageSize, query)
LoadResult.Page(
data = pageItems,
prevKey = if (page == 1) null else page - 1,
nextKey = if (pageItems.isEmpty()) null else page + 1,
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}

override fun getRefreshKey(
state: PagingState<Int, TopicUi>,
): Int {
return 1
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,20 @@ package com.developersbreach.kotlindictionarymultiplatform.ui.screens.topic

import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import com.developersbreach.kotlindictionarymultiplatform.ui.components.UiStateHandler
import app.cash.paging.compose.collectAsLazyPagingItems

@Composable
fun TopicScreen(
viewModel: TopicViewModel,
onTopicClick: (String) -> Unit,
) {
val uiState by viewModel.uiState.collectAsState()
val pagingItems = viewModel.pagingDataFlow.collectAsLazyPagingItems()
val searchQuery = viewModel.searchQuery.collectAsState().value

UiStateHandler(
uiState = uiState,
) { data ->
TopicScreenUI(
topics = data.filteredTopics,
searchQuery = data.searchQuery,
onQueryChange = viewModel::updateSearchQuery,
onTopicClick = onTopicClick,
)
}
TopicScreenUI(
topics = pagingItems,
searchQuery = searchQuery,
onQueryChange = viewModel::updateSearchQuery,
onTopicClick = onTopicClick,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.developersbreach.designsystem.components.KdScaffold

import app.cash.paging.compose.LazyPagingItems

@Composable
fun TopicScreenUI(
topics: List<ItemTopic>,
topics: LazyPagingItems<TopicUi>,
searchQuery: String,
onQueryChange: (String) -> Unit,
onTopicClick: (String) -> Unit,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.developersbreach.kotlindictionarymultiplatform.ui.screens.topic

data class TopicUi(
val name: String,
val initial: String,
val description: String,
)

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,68 +2,43 @@ package com.developersbreach.kotlindictionarymultiplatform.ui.screens.topic

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.developersbreach.kotlindictionarymultiplatform.data.topic.model.Topic
import app.cash.paging.Pager
import app.cash.paging.PagingConfig
import app.cash.paging.PagingData
import com.developersbreach.kotlindictionarymultiplatform.data.topic.repository.TopicRepository
import com.developersbreach.kotlindictionarymultiplatform.ui.components.UiState
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.stateIn

class TopicViewModel(
private val repository: TopicRepository,
) : ViewModel() {

private val _uiState: MutableStateFlow<UiState<TopicUi>> = MutableStateFlow(UiState.Loading)
val uiState: StateFlow<UiState<TopicUi>> = _uiState
private val _searchQuery = MutableStateFlow("")
val searchQuery: StateFlow<String> = _searchQuery.asStateFlow()

private var rawTopics: List<Topic> = emptyList()

init {
viewModelScope.launch {
fetchTopicList()
@OptIn(ExperimentalCoroutinesApi::class)
val pagingDataFlow: Flow<PagingData<TopicUi>> = searchQuery
.flatMapLatest { query ->
Pager(
config = PagingConfig(pageSize = 8),
pagingSourceFactory = { TopicPagingSource(repository, query) },
).flow
}
}

private suspend fun fetchTopicList() {
_uiState.value = UiState.Success(TopicUi(isLoading = true))
repository.getTopics().fold(
ifLeft = { UiState.Error(it) },
ifRight = { list ->
rawTopics = list.sortedBy { it.name?.lowercase() ?: "" }
applyFilters(rawTopics, (_uiState.value as UiState.Success).data.searchQuery)
},
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
PagingData.empty(),
)
}

fun updateSearchQuery(
newQuery: String,
) {
applyFilters(rawTopics, newQuery)
}

private fun applyFilters(
topics: List<Topic>,
query: String,
) {
val filtered = topics
.filter { topic ->
topic.name?.contains(query, ignoreCase = true) == true
}
.map { topic ->
ItemTopic(
name = topic.name ?: "",
initial = topic.name?.firstOrNull()?.uppercase() ?: "",
description = topic.description ?: "",
)
}

_uiState.value = (_uiState.value as UiState.Success).copy(
TopicUi(
isLoading = false,
searchQuery = query,
topics = topics,
filteredTopics = filtered,
),
)
_searchQuery.value = newQuery
}
}
Loading
Loading