diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index f29f12f..b147bdf 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -88,6 +88,10 @@ kotlin { implementation(libs.generativeai) implementation(compose.uiTooling) implementation(libs.ktor.client.okhttp) + implementation("androidx.paging:paging-runtime:3.3.0") + implementation("com.google.firebase:firebase-firestore-ktx:24.10.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.7.1") + implementation("com.google.firebase:firebase-bom:33.0.0") } commonMain.dependencies { implementation(compose.runtime) diff --git a/composeApp/src/androidMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/MainActivity.kt b/composeApp/src/androidMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/MainActivity.kt index 131c277..66703bc 100644 --- a/composeApp/src/androidMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/MainActivity.kt +++ b/composeApp/src/androidMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/MainActivity.kt @@ -6,6 +6,7 @@ import androidx.activity.compose.setContent import androidx.compose.runtime.Composable import androidx.compose.ui.tooling.preview.Preview import com.developersbreach.kotlindictionarymultiplatform.di.appModules +import com.developersbreach.kotlindictionarymultiplatform.paging.androidPlatformModules import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin @@ -18,6 +19,7 @@ class MainActivity : ComponentActivity() { startKoin { androidContext(this@MainActivity) appModules() + androidPlatformModules() } setContent { diff --git a/composeApp/src/androidMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/AndroidPagingModule.kt b/composeApp/src/androidMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/AndroidPagingModule.kt new file mode 100644 index 0000000..82d358e --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/AndroidPagingModule.kt @@ -0,0 +1,7 @@ +package com.developersbreach.kotlindictionarymultiplatform.paging + +import org.koin.dsl.module + +val androidPagingModule = module { + single { createTopicPager() } +} \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/AndroidTopicPager.kt b/composeApp/src/androidMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/AndroidTopicPager.kt new file mode 100644 index 0000000..9567fa0 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/AndroidTopicPager.kt @@ -0,0 +1,72 @@ +package com.developersbreach.kotlindictionarymultiplatform.paging + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.google.firebase.firestore.DocumentSnapshot +import com.google.firebase.firestore.FirebaseFirestore +import com.google.firebase.firestore.Query +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.channelFlow +import kotlinx.coroutines.tasks.await + +actual fun createTopicPager(): TopicPager { + return AndroidPagingTopicPager() +} + +private class AndroidPagingTopicPager : TopicPager { + + private val query = FirebaseFirestore.getInstance() + .collection("topics") + .orderBy("title", Query.Direction.ASCENDING) + + override fun pages(): Flow> { + return channelFlow { + Pager( + config = PagingConfig(pageSize = 10, enablePlaceholders = false), + pagingSourceFactory = { FirestoreSource(query) }, + ).flow.collect { pagingData -> + val items = mutableListOf() + pagingData.collect { topic -> + items.add(topic) + } + send(Page(items = items)) + } + } + } +} + +private class FirestoreSource( + private val query: Query, +) : PagingSource() { + + override suspend fun load( + params: LoadParams, + ): LoadResult { + return try { + val currentQuery = params.key?.let { query.startAfter(it) } ?: query + val snapshot = currentQuery.limit(params.loadSize.toLong()).get().await() + val topics = snapshot.documents.map { + Topic( + id = it.id, + name = it.getString("title") ?: "", + description = it.getString("description") ?: "", + ) + } + LoadResult.Page( + data = topics, + prevKey = null, + nextKey = snapshot.documents.lastOrNull(), + ) + } catch (e: Exception) { + LoadResult.Error(e) + } + } + + override fun getRefreshKey( + state: PagingState, + ): DocumentSnapshot? { + return null + } +} \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/FirestoreTopicPagingSource.kt b/composeApp/src/androidMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/FirestoreTopicPagingSource.kt new file mode 100644 index 0000000..ca7940c --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/FirestoreTopicPagingSource.kt @@ -0,0 +1,48 @@ +package com.developersbreach.kotlindictionarymultiplatform.paging + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.developersbreach.kotlindictionarymultiplatform.data.topic.model.Topic +import com.google.firebase.firestore.DocumentSnapshot +import com.google.firebase.firestore.Query +import kotlinx.coroutines.tasks.await + +class FirestoreTopicPagingSource( + private val query: Query, +) : PagingSource() { + + override suspend fun load( + params: LoadParams, + ): LoadResult { + return try { + val pageQuery = params.key?.let { + query.startAfter(it).limit(params.loadSize.toLong()) + } ?: query.limit(params.loadSize.toLong()) + + val snapshot = pageQuery.get().await() + + val topics = snapshot.documents.map { doc -> + Topic( + name = doc.getString("title"), + description = doc.getString("description"), + ) + } + + LoadResult.Page( + data = topics, + prevKey = null, + nextKey = snapshot.documents.lastOrNull(), + ) + } catch (e: Exception) { + LoadResult.Error(e) + } + } + + override fun getRefreshKey( + state: PagingState, + ): DocumentSnapshot? { + return state.anchorPosition?.let { position -> + state.closestPageToPosition(position)?.nextKey + } + } +} \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/PlatformModule.kt b/composeApp/src/androidMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/PlatformModule.kt new file mode 100644 index 0000000..ef1944b --- /dev/null +++ b/composeApp/src/androidMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/PlatformModule.kt @@ -0,0 +1,7 @@ +package com.developersbreach.kotlindictionarymultiplatform.paging + +import org.koin.dsl.module + +val androidModule = module { + single { createTopicPager() } +} \ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/previews/topic/TopicScreenPreview.kt b/composeApp/src/androidMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/previews/topic/TopicScreenPreview.kt index c34fd62..b2875d3 100644 --- a/composeApp/src/androidMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/previews/topic/TopicScreenPreview.kt +++ b/composeApp/src/androidMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/previews/topic/TopicScreenPreview.kt @@ -15,6 +15,7 @@ private fun TopicScreenPreview() { searchQuery = "Search", onQueryChange = { }, onTopicClick = { }, + onLoadMore = {}, ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/Page.kt b/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/Page.kt new file mode 100644 index 0000000..08ce46e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/Page.kt @@ -0,0 +1,6 @@ +package com.developersbreach.kotlindictionarymultiplatform.paging + +data class Page( + val items: List, + val nextCursor: Any? = null, +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/Topic.kt b/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/Topic.kt new file mode 100644 index 0000000..51a050b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/Topic.kt @@ -0,0 +1,7 @@ +package com.developersbreach.kotlindictionarymultiplatform.paging + +data class Topic( + val id: String, + val name: String, + val description: String, +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/ui/screens/topic/TopicList.kt b/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/TopicList.kt similarity index 77% rename from composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/ui/screens/topic/TopicList.kt rename to composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/TopicList.kt index c916049..565d824 100644 --- a/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/ui/screens/topic/TopicList.kt +++ b/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/TopicList.kt @@ -1,4 +1,4 @@ -package com.developersbreach.kotlindictionarymultiplatform.ui.screens.topic +package com.developersbreach.kotlindictionarymultiplatform.paging import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize @@ -7,6 +7,8 @@ import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.developersbreach.kotlindictionarymultiplatform.ui.screens.topic.ItemTopic +import com.developersbreach.kotlindictionarymultiplatform.ui.screens.topic.TopicCard @Composable fun TopicList( diff --git a/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/TopicPager.kt b/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/TopicPager.kt new file mode 100644 index 0000000..71de207 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/TopicPager.kt @@ -0,0 +1,9 @@ +package com.developersbreach.kotlindictionarymultiplatform.paging + +import kotlinx.coroutines.flow.Flow + +interface TopicPager { + fun pages(): Flow> +} + +expect fun createTopicPager(): TopicPager \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/ui/screens/topic/TopicScreen.kt b/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/TopicScreen.kt similarity index 73% rename from composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/ui/screens/topic/TopicScreen.kt rename to composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/TopicScreen.kt index efebf55..cc2f72e 100644 --- a/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/ui/screens/topic/TopicScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/TopicScreen.kt @@ -1,9 +1,11 @@ -package com.developersbreach.kotlindictionarymultiplatform.ui.screens.topic +package com.developersbreach.kotlindictionarymultiplatform.paging import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import com.developersbreach.kotlindictionarymultiplatform.ui.components.UiStateHandler +import com.developersbreach.kotlindictionarymultiplatform.ui.screens.topic.TopicScreenUI +import com.developersbreach.kotlindictionarymultiplatform.ui.screens.topic.TopicViewModel @Composable fun TopicScreen( diff --git a/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/ui/navigation/AppNavigation.kt b/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/ui/navigation/AppNavigation.kt index 547e993..6306e85 100644 --- a/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/ui/navigation/AppNavigation.kt +++ b/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/ui/navigation/AppNavigation.kt @@ -7,7 +7,7 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.developersbreach.kotlindictionarymultiplatform.ui.screens.detail.DetailScreen import com.developersbreach.kotlindictionarymultiplatform.ui.screens.detail.DetailViewModel -import com.developersbreach.kotlindictionarymultiplatform.ui.screens.topic.TopicScreen +import com.developersbreach.kotlindictionarymultiplatform.paging.TopicScreen import com.developersbreach.kotlindictionarymultiplatform.ui.screens.topic.TopicViewModel import org.koin.compose.viewmodel.koinViewModel diff --git a/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/ui/screens/topic/TopicScreenUI.kt b/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/ui/screens/topic/TopicScreenUI.kt index 770320b..904484d 100644 --- a/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/ui/screens/topic/TopicScreenUI.kt +++ b/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/ui/screens/topic/TopicScreenUI.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.developersbreach.designsystem.components.KdScaffold +import com.developersbreach.kotlindictionarymultiplatform.paging.TopicList @Composable fun TopicScreenUI( @@ -29,7 +30,9 @@ fun TopicScreenUI( searchQuery = searchQuery, onQueryChange = onQueryChange, ) + Spacer(modifier = Modifier.height(8.dp)) + TopicList( topics = topics, onTopicClick = onTopicClick, diff --git a/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/ui/screens/topic/TopicUiState.kt b/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/ui/screens/topic/TopicUiState.kt index d0bd8cd..e5b9f61 100644 --- a/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/ui/screens/topic/TopicUiState.kt +++ b/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/ui/screens/topic/TopicUiState.kt @@ -7,6 +7,8 @@ data class TopicUi( val topics: List = emptyList(), val searchQuery: String = "", val filteredTopics: List = emptyList(), + val page: Int = 0, + val hasMore: Boolean = true, ) data class ItemTopic( diff --git a/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/ui/screens/topic/TopicViewModel.kt b/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/ui/screens/topic/TopicViewModel.kt index 1476fcd..bf633d8 100644 --- a/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/ui/screens/topic/TopicViewModel.kt +++ b/composeApp/src/commonMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/ui/screens/topic/TopicViewModel.kt @@ -3,36 +3,29 @@ package com.developersbreach.kotlindictionarymultiplatform.ui.screens.topic import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.developersbreach.kotlindictionarymultiplatform.data.topic.model.Topic -import com.developersbreach.kotlindictionarymultiplatform.data.topic.repository.TopicRepository +import com.developersbreach.kotlindictionarymultiplatform.paging.TopicPager import com.developersbreach.kotlindictionarymultiplatform.ui.components.UiState import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch class TopicViewModel( - private val repository: TopicRepository, + private val pager: TopicPager, ) : ViewModel() { private val _uiState: MutableStateFlow> = MutableStateFlow(UiState.Loading) val uiState: StateFlow> = _uiState - private var rawTopics: List = emptyList() + private var rawTopics: MutableList = mutableListOf() init { viewModelScope.launch { - fetchTopicList() - } - } - - 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() ?: "" } + _uiState.value = UiState.Success(TopicUi(isLoading = true)) + pager.pages().collect { page -> + rawTopics += page.items applyFilters(rawTopics, (_uiState.value as UiState.Success).data.searchQuery) - }, - ) + } + } } fun updateSearchQuery( @@ -57,7 +50,7 @@ class TopicViewModel( ) } - _uiState.value = (_uiState.value as UiState.Success).copy( + _uiState.value = UiState.Success( TopicUi( isLoading = false, searchQuery = query, diff --git a/composeApp/src/desktopMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/TopicPager.desktop.kt b/composeApp/src/desktopMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/TopicPager.desktop.kt new file mode 100644 index 0000000..bf2808d --- /dev/null +++ b/composeApp/src/desktopMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/TopicPager.desktop.kt @@ -0,0 +1,5 @@ +package com.developersbreach.kotlindictionarymultiplatform.paging + +actual fun createTopicPager(): TopicPager { + TODO("Not yet implemented") +} \ No newline at end of file diff --git a/composeApp/src/nativeMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/TopicPager.native.kt b/composeApp/src/nativeMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/TopicPager.native.kt new file mode 100644 index 0000000..bf2808d --- /dev/null +++ b/composeApp/src/nativeMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/TopicPager.native.kt @@ -0,0 +1,5 @@ +package com.developersbreach.kotlindictionarymultiplatform.paging + +actual fun createTopicPager(): TopicPager { + TODO("Not yet implemented") +} \ No newline at end of file diff --git a/composeApp/src/wasmJsMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/TopicPager.wasmJs.kt b/composeApp/src/wasmJsMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/TopicPager.wasmJs.kt new file mode 100644 index 0000000..bf2808d --- /dev/null +++ b/composeApp/src/wasmJsMain/kotlin/com/developersbreach/kotlindictionarymultiplatform/paging/TopicPager.wasmJs.kt @@ -0,0 +1,5 @@ +package com.developersbreach.kotlindictionarymultiplatform.paging + +actual fun createTopicPager(): TopicPager { + TODO("Not yet implemented") +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5f49d58..263b9a2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,6 +30,7 @@ kotlinStdlib = "2.1.10" runner = "1.6.2" core = "1.6.1" uiToolingPreviewAndroid = "1.8.2" +firebaseFirestoreKtx = "25.1.4" [libraries] androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigation-compose" } @@ -70,6 +71,7 @@ kotlin-stdlib = { group = "org.jetbrains.kotlin", name = "kotlin-stdlib", versio androidx-runner = { group = "androidx.test", name = "runner", version.ref = "runner" } androidx-core = { group = "androidx.test", name = "core", version.ref = "core" } androidx-ui-tooling-preview-android = { group = "androidx.compose.ui", name = "ui-tooling-preview-android", version.ref = "uiToolingPreviewAndroid" } +firebase-firestore-ktx = { group = "com.google.firebase", name = "firebase-firestore-ktx", version.ref = "firebaseFirestoreKtx" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" }