diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 13758ca..6f91d57 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -19,7 +19,7 @@ android { defaultConfig { applicationId = "in.iot.lab.teacherreview" - minSdk = 24 + minSdk = 26 targetSdk = 34 versionCode = 1 versionName = "1.0" @@ -128,6 +128,10 @@ dependencies { //Implementing the design module implementation(project(":core:design")) + + // Paging 3 + implementation(libs.paging.runtime) + implementation(libs.paging.compose) } fun getBaseUrlInCIEnvironment(): String { diff --git a/app/src/main/java/in/iot/lab/teacherreview/core/utils/Constants.kt b/app/src/main/java/in/iot/lab/teacherreview/core/utils/Constants.kt index 1150658..ae24a2a 100644 --- a/app/src/main/java/in/iot/lab/teacherreview/core/utils/Constants.kt +++ b/app/src/main/java/in/iot/lab/teacherreview/core/utils/Constants.kt @@ -18,4 +18,7 @@ object Constants { const val LOGIN_AUTHENTICATION_ENDPOINT = "authentication" const val TEACHER_LIST_ENDPOINT = "faculties" const val POST_TEACHER_REVIEW_ENDPOINT = "reviews" + + const val ITEMS_PER_PAGE = 10 + const val PREFETCH_DISTANCE = 3 } \ No newline at end of file diff --git a/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/data/paging_source/ReviewsSource.kt b/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/data/paging_source/ReviewsSource.kt new file mode 100644 index 0000000..61392b6 --- /dev/null +++ b/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/data/paging_source/ReviewsSource.kt @@ -0,0 +1,41 @@ +package `in`.iot.lab.teacherreview.feature_teacherlist.data.paging_source + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import `in`.iot.lab.teacherreview.core.utils.Constants +import `in`.iot.lab.teacherreview.feature_authentication.domain.repository.AuthRepository +import `in`.iot.lab.teacherreview.feature_teacherlist.data.remote.ReviewsApi +import `in`.iot.lab.teacherreview.feature_teacherlist.domain.models.remote.IndividualReviewData +class ReviewsSource ( + private val facultyId: String, + private val authRepository: AuthRepository, + private val reviewsApi: ReviewsApi +) : PagingSource() { + override fun getRefreshKey(state: PagingState): Int? { + return state.anchorPosition + } + + override suspend fun load(params: LoadParams): LoadResult { + return try { + val page = params.key ?: 0 + val skip = page * Constants.ITEMS_PER_PAGE + + val response = reviewsApi.getIndividualTeacherReviews( + token = authRepository.getUserIdToken().getOrDefault(""), + facultyId = facultyId, + limitValue = Constants.ITEMS_PER_PAGE, + skip = skip + ) + + val data = response.body()!!.individualReviewData ?: emptyList() + + LoadResult.Page( + data = data, + prevKey = if (page == 0) null else page - 1, + nextKey = if (data.isEmpty()) null else page + 1 + ) + } catch (e: Exception) { + LoadResult.Error(e) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/data/remote/ReviewsApi.kt b/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/data/remote/ReviewsApi.kt index f10fafc..6f45a8b 100644 --- a/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/data/remote/ReviewsApi.kt +++ b/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/data/remote/ReviewsApi.kt @@ -11,11 +11,13 @@ import retrofit2.http.POST import retrofit2.http.Query interface ReviewsApi { - @GET("reviews?${"$"}populate=faculty&${"$"}populate=createdBy") + // TODO: expose the sort query parameter as function parameter to make it more flexible + @GET("reviews?${"$"}populate=faculty&${"$"}populate=createdBy&${"$"}sort[createdAt]=-1") suspend fun getIndividualTeacherReviews( @Header("Authorization") token: String, @Query("faculty") facultyId: String, - @Query("${"$"}limit") limitValue: Int + @Query("${"$"}limit") limitValue: Int, + @Query("${"$"}skip") skip: Int = 0 ): Response diff --git a/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/data/repository/ReviewRepositoryImpl.kt b/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/data/repository/ReviewRepositoryImpl.kt index 4534757..d32a89f 100644 --- a/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/data/repository/ReviewRepositoryImpl.kt +++ b/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/data/repository/ReviewRepositoryImpl.kt @@ -1,11 +1,18 @@ package `in`.iot.lab.teacherreview.feature_teacherlist.data.repository import android.util.Log +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import `in`.iot.lab.teacherreview.core.utils.Constants import `in`.iot.lab.teacherreview.feature_authentication.domain.repository.AuthRepository +import `in`.iot.lab.teacherreview.feature_teacherlist.data.paging_source.ReviewsSource import `in`.iot.lab.teacherreview.feature_teacherlist.data.remote.ReviewsApi +import `in`.iot.lab.teacherreview.feature_teacherlist.domain.models.remote.IndividualReviewData import `in`.iot.lab.teacherreview.feature_teacherlist.domain.models.remote.ReviewData import `in`.iot.lab.teacherreview.feature_teacherlist.domain.models.remote.ReviewPostData import `in`.iot.lab.teacherreview.feature_teacherlist.domain.repository.ReviewRepository +import kotlinx.coroutines.flow.Flow import javax.inject.Inject class ReviewRepositoryImpl @Inject constructor( @@ -34,36 +41,22 @@ class ReviewRepositoryImpl @Inject constructor( } } - override suspend fun getTeacherReviews(facultyId: String, limitValue: Int): Result { + override suspend fun getTeacherReviews(facultyId: String): Result>> { try { - val response = reviewsApi.getIndividualTeacherReviews( - limitValue = limitValue, - facultyId = facultyId, - token = getToken() - ) - Log.d(TAG, response.toString()) - if (!response.isSuccessful) { - throw Exception("Error Connecting to the Server") - } - - // TODO: Maybe cache the response here - val reviewData = response.body()!! - - val sortedReviews = reviewData.individualReviewData?.sortedByDescending { - it.createdAt - } - - val sortedResponse = ReviewData( - avgAttendanceRating = reviewData.avgAttendanceRating, - avgMarkingRating = reviewData.avgMarkingRating, - avgTeachingRating = reviewData.avgTeachingRating, - total = reviewData.total, - limit = reviewData.limit, - skip = reviewData.skip, - individualReviewData = sortedReviews - ) + val pager = Pager( + config = PagingConfig( + pageSize = Constants.ITEMS_PER_PAGE, + prefetchDistance = Constants.PREFETCH_DISTANCE, + ) + ) { + ReviewsSource( + facultyId = facultyId, + authRepository = authRepository, + reviewsApi = reviewsApi + ) + }.flow - return Result.success(sortedResponse) + return Result.success(pager) } catch (e: Exception) { e.printStackTrace() return Result.failure(e) diff --git a/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/domain/repository/ReviewRepository.kt b/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/domain/repository/ReviewRepository.kt index f00872d..4b32a74 100644 --- a/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/domain/repository/ReviewRepository.kt +++ b/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/domain/repository/ReviewRepository.kt @@ -1,11 +1,14 @@ package `in`.iot.lab.teacherreview.feature_teacherlist.domain.repository +import androidx.paging.PagingData +import `in`.iot.lab.teacherreview.feature_teacherlist.domain.models.remote.IndividualReviewData import `in`.iot.lab.teacherreview.feature_teacherlist.domain.models.remote.ReviewData import `in`.iot.lab.teacherreview.feature_teacherlist.domain.models.remote.ReviewPostData +import kotlinx.coroutines.flow.Flow interface ReviewRepository { suspend fun postReview(review: ReviewPostData): Result - suspend fun getTeacherReviews(facultyId: String, limitValue: Int): Result + suspend fun getTeacherReviews(facultyId: String): Result>> suspend fun getStudentsReviewHistory(studentId: String, limitValue: Int): Result // TODO: To be added in the next release (maybe ?) diff --git a/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/ui/navigation/TeacherListNavGraph.kt b/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/ui/navigation/TeacherListNavGraph.kt index 1ca0dbb..ee290a3 100644 --- a/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/ui/navigation/TeacherListNavGraph.kt +++ b/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/ui/navigation/TeacherListNavGraph.kt @@ -8,6 +8,7 @@ import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import androidx.paging.compose.collectAsLazyPagingItems import `in`.iot.lab.teacherreview.feature_bottom_navigation.navigation.BottomNavRoutes import `in`.iot.lab.teacherreview.feature_teacherlist.ui.screen.HomeScreenControl import `in`.iot.lab.teacherreview.feature_teacherlist.ui.screen.IndividualTeacherControl @@ -46,12 +47,14 @@ fun TeacherListNavGraph( TeacherListRoutes.IndividualTeacherRoute.route, content = { val currentUserId by teacherListViewModel.currentUserId.collectAsStateWithLifecycle() + val lazyPagingItems = teacherListViewModel.pagingFlow.collectAsLazyPagingItems() + IndividualTeacherControl( navController = navController, selectedTeacher = teacherListViewModel.selectedTeacher!!, - action = teacherListViewModel::action, - individualTeacherReviewApiCall = teacherListViewModel.individualTeacherReviewApiCall, currentUserId = currentUserId, + lazyPagingItems = lazyPagingItems, + action = teacherListViewModel::action ) } ) diff --git a/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/ui/screen/IndividualTeacherControl.kt b/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/ui/screen/IndividualTeacherControl.kt index e0b3554..c2e90dc 100644 --- a/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/ui/screen/IndividualTeacherControl.kt +++ b/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/ui/screen/IndividualTeacherControl.kt @@ -1,129 +1,69 @@ package `in`.iot.lab.teacherreview.feature_teacherlist.ui.screen import android.annotation.SuppressLint -import android.content.res.Configuration import android.os.Build import androidx.annotation.RequiresApi -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.RateReview -import androidx.compose.material3.* +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.navigation.NavController -import androidx.navigation.compose.rememberNavController -import `in`.iot.lab.design.components.PullToRefresh -import `in`.iot.lab.design.theme.* +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems +import `in`.iot.lab.design.components.PullToRefreshLazyColumn import `in`.iot.lab.teacherreview.R import `in`.iot.lab.teacherreview.feature_teacherlist.domain.models.remote.IndividualFacultyData -import `in`.iot.lab.teacherreview.feature_teacherlist.domain.models.remote.ReviewData +import `in`.iot.lab.teacherreview.feature_teacherlist.domain.models.remote.IndividualReviewData import `in`.iot.lab.teacherreview.feature_teacherlist.ui.components.ReviewCardItem import `in`.iot.lab.teacherreview.feature_teacherlist.ui.components.TeacherDetailsHeaderCard import `in`.iot.lab.teacherreview.feature_teacherlist.ui.navigation.TeacherListRoutes import `in`.iot.lab.teacherreview.feature_teacherlist.ui.state_action.TeacherListAction -import `in`.iot.lab.teacherreview.feature_teacherlist.ui.stateholder.TeacherListViewModel -import `in`.iot.lab.teacherreview.feature_teacherlist.utils.IndividualTeacherReviewApiCall -// This is the Preview function of the Teacher Review Control Screen -@RequiresApi(Build.VERSION_CODES.O) -@Preview("Light") -@Preview( - name = "Dark", - uiMode = Configuration.UI_MODE_NIGHT_YES -) -@Composable -private fun DefaultPreviewControl() { - CustomAppTheme { - IndividualTeacherControl( - navController = rememberNavController(), - selectedTeacher = IndividualFacultyData( - _id = "" - ), - action = {}, - individualTeacherReviewApiCall = IndividualTeacherReviewApiCall.Initialized, - currentUserId = "" - - ) - } -} - -// This is the Preview function of the Teacher Review Loading Screen -@Preview("Light") -@Preview( - name = "Dark", - uiMode = Configuration.UI_MODE_NIGHT_YES -) -@Composable -private fun DefaultPreviewLoading() { - CustomAppTheme { - IndividualTeacherLoading( - selectedTeacher = IndividualFacultyData( - _id = "" - ) - ) - } -} - -// This is the Preview function of the Teacher Review Success Screen -@RequiresApi(Build.VERSION_CODES.O) -@Preview("Light") -@Preview( - name = "Dark", - uiMode = Configuration.UI_MODE_NIGHT_YES -) -@Composable -private fun DefaultPreviewSuccess() { - CustomAppTheme { - IndividualTeacherSuccess( - reviewData = ReviewData(), - selectedTeacher = IndividualFacultyData( - _id = "" - ), - currentUserId = "" - ) - } -} - -// This is the Preview function of the Teacher Review Failure Screen -@Preview("Light") -@Preview( - name = "Dark", - uiMode = Configuration.UI_MODE_NIGHT_YES -) -@Composable -private fun DefaultPreviewFailure() { - CustomAppTheme { - IndividualTeacherFailure( - selectedTeacher = IndividualFacultyData( - _id = "" - ), - onClickRetry = {}, - textToShow = stringResource(R.string.failed_to_load_tap_to_retry) - ) - } -} - -/** - * Individual Teacher Control for Review Screen - * - * @param navController This is kept to navigate to different Screens - * @param myViewModel This is the [TeacherListViewModel] variable - */ @RequiresApi(Build.VERSION_CODES.O) @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable fun IndividualTeacherControl( navController: NavController, selectedTeacher: IndividualFacultyData, - individualTeacherReviewApiCall: IndividualTeacherReviewApiCall, currentUserId: String?, + lazyPagingItems: LazyPagingItems, action: (TeacherListAction) -> Unit, ) { + val loading by remember { + derivedStateOf { + val state = lazyPagingItems.loadState + when { + (state.source.refresh is LoadState.Loading) -> true + (state.refresh is LoadState.Loading) -> true + else -> false + } + } + } + Scaffold( floatingActionButton = { FloatingActionButton( @@ -152,232 +92,143 @@ fun IndividualTeacherControl( modifier = Modifier .fillMaxSize() ) { - - when (individualTeacherReviewApiCall) { - is IndividualTeacherReviewApiCall.Initialized -> {} - is IndividualTeacherReviewApiCall.Loading -> { - - // Showing the Loading Screen - IndividualTeacherLoading( - selectedTeacher = selectedTeacher, - onBackClick = { - navController.popBackStack() - } - ) - } - - is IndividualTeacherReviewApiCall.Success -> { - - // Taking all the review Data - val reviewData = individualTeacherReviewApiCall.reviewData - - //Checking if the review Data is Empty or Not - if (reviewData.individualReviewData.isNullOrEmpty()) { - - // Calling the Failed Screen - IndividualTeacherFailure( - selectedTeacher = selectedTeacher, - onClickRetry = { - action( - TeacherListAction.GetIndividualTeacherReviews( - selectedTeacher._id - ) - ) - }, - textToShow = stringResource(id = R.string.dont_have_anything_to_show), - onBackClick = { - navController.popBackStack() - } - ) - - - } else { - IndividualTeacherSuccess( - loading = individualTeacherReviewApiCall is IndividualTeacherReviewApiCall.Loading, - reviewData = reviewData, - selectedTeacher = selectedTeacher, - onBackClick = { - navController.popBackStack() - }, - currentUserId = currentUserId, - refreshReviews = { - action( - TeacherListAction.GetIndividualTeacherReviews( - selectedTeacher._id - ) - ) - } + IndividualTeacherContent( + loading = loading, + lazyPagingItems = lazyPagingItems, + selectedTeacher = selectedTeacher, + onBackClick = navController::popBackStack, + currentUserId = currentUserId, + refreshReviews = { + action( + TeacherListAction.GetIndividualTeacherReviews( + selectedTeacher._id ) - } - } - - else -> { - - // Showing the Failure Data - IndividualTeacherFailure( - selectedTeacher = selectedTeacher, - onClickRetry = { - action( - TeacherListAction.GetIndividualTeacherReviews( - selectedTeacher._id - ) - ) - }, - textToShow = stringResource(R.string.failed_to_load_tap_to_retry) ) } - } + ) } } } -/** - * This function is Used when the Screen is Loading - * - * @param modifier Default kept to let the parent class pass any modifications it needs - * @param selectedTeacher This is the details of the Selected Teacher - */ -@Composable -fun IndividualTeacherLoading( - modifier: Modifier = Modifier, - selectedTeacher: IndividualFacultyData, - onBackClick: () -> Unit = {} -) { - - Column( - modifier = modifier - .fillMaxWidth(), - ) { - - // Showing the Teacher Details - TeacherDetailsHeaderCard( - selectedTeacher = selectedTeacher, - onBackPressed = onBackClick - ) - - // Spacer of Height 16 dp - Spacer(modifier = Modifier.height(16.dp)) - - - // Showing the Progress Bar - Box( - modifier = Modifier - .padding(end = 16.dp, start = 16.dp) - .fillMaxWidth(), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator() - } - } -} /** - * This function is Used when the Screen is Success + * Individual Teacher Main Content for Review Screen with Pull to Refresh and Lazy Column + * + * @param loading This is the Loading State of the Screen + * @param lazyPagingItems This is the Lazy Paging Items for the Reviews + * @param selectedTeacher This is the Selected Teacher Data + * @param currentUserId This is the Current User Id + * @param onBackClick This is the Function to Navigate Back + * @param refreshReviews This is the Function to Refresh the Reviews * - * @param reviewData This is the review data of the particular review - * @param selectedTeacher This is the selected Teacher */ @RequiresApi(Build.VERSION_CODES.O) @Composable -fun IndividualTeacherSuccess( +fun IndividualTeacherContent( loading: Boolean = false, - reviewData: ReviewData, + lazyPagingItems: LazyPagingItems, selectedTeacher: IndividualFacultyData, currentUserId: String?, onBackClick: () -> Unit = {}, refreshReviews: () -> Unit = {} ) { - // Lazy Column to Show the List of Reviews - PullToRefresh( - items = reviewData.individualReviewData!!, + val state: LazyListState = rememberLazyListState() + PullToRefreshLazyColumn( + lazyListState = state, isRefreshing = loading, - onRefresh = { - refreshReviews() - }, - preContent = { + onRefresh = refreshReviews, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + item { TeacherDetailsHeaderCard( selectedTeacher = selectedTeacher, onBackPressed = onBackClick ) - - // Spacer of Height 16 dp Spacer(modifier = Modifier.height(16.dp)) - }, - content = { - val reviewItem = it - val rating = with(reviewItem.rating!!) { - attendanceRating?.ratedPoints - ?.plus(teachingRating?.ratedPoints!!) - ?.plus(markingRating?.ratedPoints!!) - ?.plus(overallRating)?.div(4) ?: 0.0 + } + + items(count = lazyPagingItems.itemCount) { index -> + lazyPagingItems.get(index = index)?.let { review -> + val rating = with(review.rating!!) { + attendanceRating?.ratedPoints + ?.plus(teachingRating?.ratedPoints!!) + ?.plus(markingRating?.ratedPoints!!) + ?.plus(overallRating)?.div(4) ?: 0.0 + } + + ReviewCardItem( + modifier = Modifier + .padding(end = 16.dp, start = 16.dp), + createdBy = review.createdBy, + review = review.review!!, + rating = rating, + createdAt = review.createdAt!!, + currentUserId = currentUserId + ) + + // Spacer of Height 16 dp + Spacer(modifier = Modifier.height(16.dp)) + } + } + + when { + lazyPagingItems.loadState.refresh is LoadState.Loading && lazyPagingItems.itemCount == 0 -> { + item { + CircularProgressIndicator() + } + } + + lazyPagingItems.loadState.refresh is LoadState.Loading && lazyPagingItems.itemCount == 0 -> { + item { + CircularProgressIndicator() + } + } + + lazyPagingItems.loadState.refresh is LoadState.Error -> { + item { + FailedToLoad( + onClickRetry = {}, + textToShow = (lazyPagingItems.loadState.refresh as LoadState.Error).error.message + ?: "Error" + ) + } + } + + lazyPagingItems.loadState.append is LoadState.Loading -> { + item { + CircularProgressIndicator() + } + } + + lazyPagingItems.loadState.append is LoadState.Error -> { + item { + FailedToLoad( + onClickRetry = {}, + textToShow = (lazyPagingItems.loadState.refresh as LoadState.Error).error.message + ?: "Error" + ) + } } - ReviewCardItem( - modifier = Modifier - .padding(end = 16.dp, start = 16.dp), - createdBy = reviewItem.createdBy, - review = reviewItem.review!!, - rating = rating, - createdAt = reviewItem.createdAt!!, - currentUserId = currentUserId - ) - // Spacer of Height 16 dp - Spacer(modifier = Modifier.height(16.dp)) } - ) -// LazyColumn { -// items(reviewData.individualReviewData!!.size + 1) { -// val itemCount = it - 1 -// -// // Drawing the Header of the Teacher with his overall stats -// if (itemCount == -1) { -// TeacherDetailsHeaderCard( -// selectedTeacher = selectedTeacher, -// onBackPressed = onBackClick -// ) -// -// // Spacer of Height 16 dp -// Spacer(modifier = Modifier.height(16.dp)) -// } else { -// val reviewItem = reviewData.individualReviewData[itemCount] -// val rating = with(reviewItem.rating!!) { -// attendanceRating?.ratedPoints -// ?.plus(teachingRating?.ratedPoints!!) -// ?.plus(markingRating?.ratedPoints!!) -// ?.plus(overallRating)?.div(4) ?: 0.0 -// } -// ReviewCardItem( -// modifier = Modifier -// .padding(end = 16.dp, start = 16.dp), -// createdBy = reviewItem.createdBy, -// review = reviewItem.review!!, -// rating = rating, -// createdAt = reviewItem.createdAt!!, -// currentUserId = currentUserId -// ) -// -// // Spacer of Height 16 dp -// Spacer(modifier = Modifier.height(16.dp)) -// } -// } -// } + + if (lazyPagingItems.loadState.append.endOfPaginationReached) { + item { + Text( + text = "No more reviews to show", + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(16.dp) + ) + } + } + } } -/** - * This Screen is used when the Request is a Failure - * - * @param modifier Default modifier to pass modifications from the Parent - * @param selectedTeacher This is the selected Teacher - * @param onClickRetry This is the function which is run when the teacher clicks retry - * @param textToShow This text is shown on the Screen - */ @Composable -fun IndividualTeacherFailure( +fun FailedToLoad( modifier: Modifier = Modifier, - selectedTeacher: IndividualFacultyData, onClickRetry: () -> Unit, textToShow: String, - onBackClick: () -> Unit = {} ) { Column( modifier = modifier @@ -385,17 +236,6 @@ fun IndividualTeacherFailure( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { - - // Drawing the Header of the Teacher with his overall stats - TeacherDetailsHeaderCard( - selectedTeacher = selectedTeacher, - onBackPressed = onBackClick - ) - - // Spacer of Height 16 dp - Spacer(modifier = Modifier.height(16.dp)) - - // This is a text Button which says to Try Again TextButton( onClick = { onClickRetry() }, modifier = Modifier.padding(end = 16.dp, start = 16.dp), diff --git a/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/ui/stateholder/TeacherListViewModel.kt b/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/ui/stateholder/TeacherListViewModel.kt index 91586ee..8ad2105 100644 --- a/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/ui/stateholder/TeacherListViewModel.kt +++ b/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/ui/stateholder/TeacherListViewModel.kt @@ -5,16 +5,21 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.cachedIn import dagger.hilt.android.lifecycle.HiltViewModel import `in`.iot.lab.teacherreview.core.data.local.UserPreferences import `in`.iot.lab.teacherreview.feature_teacherlist.domain.models.remote.IndividualFacultyData +import `in`.iot.lab.teacherreview.feature_teacherlist.domain.models.remote.IndividualReviewData import `in`.iot.lab.teacherreview.feature_teacherlist.domain.repository.ReviewRepository import `in`.iot.lab.teacherreview.feature_teacherlist.domain.repository.TeachersRepository import `in`.iot.lab.teacherreview.feature_teacherlist.ui.screen.HomeScreenControl import `in`.iot.lab.teacherreview.feature_teacherlist.ui.state_action.TeacherListAction -import `in`.iot.lab.teacherreview.feature_teacherlist.utils.IndividualTeacherReviewApiCall import `in`.iot.lab.teacherreview.feature_teacherlist.utils.TeacherListApiCallState +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import java.net.ConnectException import javax.inject.Inject @@ -45,14 +50,13 @@ class TeacherListViewModel @Inject constructor( var selectedTeacher: IndividualFacultyData? = null private set - var individualTeacherReviewApiCall: IndividualTeacherReviewApiCall by mutableStateOf( - IndividualTeacherReviewApiCall.Initialized - ) - private set - private val _currentUserId = userPreferences.userId val currentUserId: MutableStateFlow = MutableStateFlow(null) + private var _pagingFlow: MutableStateFlow> = + MutableStateFlow(value = PagingData.empty()) + val pagingFlow: StateFlow> = _pagingFlow + init { viewModelScope.launch { _currentUserId.collect { @@ -78,7 +82,6 @@ class TeacherListViewModel @Inject constructor( // Response from the Server teachersRepository.getAllTeachers( limitValue = 10, - // TODO: To be implemented later searchQuery = searchQuery ).onSuccess { teacherListApiCallState = TeacherListApiCallState.Success(it) @@ -94,36 +97,28 @@ class TeacherListViewModel @Inject constructor( } // This function initialises the Teacher variable for the next Screen - fun addTeacherForNextScreen(teacher: IndividualFacultyData) { + private fun addTeacherForNextScreen(teacher: IndividualFacultyData) { selectedTeacher = teacher } // This function fetches the List of Teachers fun getIndividualTeacherReviews(facultyId: String = selectedTeacher!!._id) { - - // Setting the Current State to Loading Before Starting to Fetch Data - individualTeacherReviewApiCall = IndividualTeacherReviewApiCall.Loading - - // Fetching the Data from the Server - viewModelScope.launch { - - // Checking the State of the API call and storing it to reflect to the UI Layer + viewModelScope.launch(Dispatchers.IO) { try { + // Reset the Paging Flow + _pagingFlow.value = PagingData.empty() - // Response from the Server reviewRepository.getTeacherReviews( - limitValue = 50, facultyId = facultyId - ).onSuccess { - individualTeacherReviewApiCall = IndividualTeacherReviewApiCall.Success(it) - }.onFailure { - individualTeacherReviewApiCall = - IndividualTeacherReviewApiCall.Failure(it.message ?: "Unknown Error") - } - - } catch (_: ConnectException) { - individualTeacherReviewApiCall = - IndividualTeacherReviewApiCall.Failure("No Internet Connection") + ) + .getOrThrow() + .distinctUntilChanged() + .cachedIn(viewModelScope) + .collect { + _pagingFlow.value = it + } + } catch (e: Exception) { + e.printStackTrace() } } } diff --git a/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/utils/IndividualTeacherReviewApiCall.kt b/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/utils/IndividualTeacherReviewApiCall.kt deleted file mode 100644 index 71f9e8f..0000000 --- a/app/src/main/java/in/iot/lab/teacherreview/feature_teacherlist/utils/IndividualTeacherReviewApiCall.kt +++ /dev/null @@ -1,19 +0,0 @@ -package `in`.iot.lab.teacherreview.feature_teacherlist.utils - -import `in`.iot.lab.teacherreview.feature_teacherlist.domain.models.remote.ReviewData -import `in`.iot.lab.teacherreview.feature_teacherlist.utils.IndividualTeacherReviewApiCall.* - -/** - * This sealed Class contains all the States of the Individual Teacher Review Request of a API - * - * @property Initialized is used to define the Initial State - * @property Loading is used to define the state of the API call when it is in fetching Phase - * @property Success is used to define when the API call is a Success - * @property Failure is used to define when the API call is a Failure - */ -sealed class IndividualTeacherReviewApiCall { - object Initialized : IndividualTeacherReviewApiCall() - object Loading : IndividualTeacherReviewApiCall() - class Success(val reviewData: ReviewData) : IndividualTeacherReviewApiCall() - class Failure(val errorMessage: String) : IndividualTeacherReviewApiCall() -} \ No newline at end of file diff --git a/core/design/src/main/java/in/iot/lab/design/components/PullToRefresh.kt b/core/design/src/main/java/in/iot/lab/design/components/PullToRefresh.kt index 07c957c..a93f719 100644 --- a/core/design/src/main/java/in/iot/lab/design/components/PullToRefresh.kt +++ b/core/design/src/main/java/in/iot/lab/design/components/PullToRefresh.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.offset import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState @@ -73,3 +74,57 @@ fun PullToRefresh( ) } } + +// TODO: Merge this with the above PullToRefresh function +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PullToRefreshLazyColumn( + modifier: Modifier = Modifier, + isRefreshing: Boolean, + onRefresh: () -> Unit, + paddingValues: PaddingValues = PaddingValues(0.dp), + lazyListState: LazyListState = rememberLazyListState(), + reverseLayout: Boolean = false, + verticalArrangement: Arrangement.Vertical = Arrangement.spacedBy(8.dp), + horizontalAlignment: Alignment.Horizontal = Alignment.Start, + content: LazyListScope.() -> Unit, +) { + val pullToRefreshState = rememberPullToRefreshState() + Box( + modifier = modifier + .nestedScroll(pullToRefreshState.nestedScrollConnection) + ) { + LazyColumn( + state = lazyListState, + contentPadding = paddingValues, + modifier = Modifier + .fillMaxSize(), + reverseLayout = reverseLayout, + verticalArrangement = verticalArrangement, + horizontalAlignment = horizontalAlignment, + ) { + content(this) + } + + if(pullToRefreshState.isRefreshing) { + LaunchedEffect(true) { + onRefresh() + } + } + + LaunchedEffect(isRefreshing) { + if(isRefreshing) { + pullToRefreshState.startRefresh() + } else { + pullToRefreshState.endRefresh() + } + } + + PullToRefreshContainer( + state = pullToRefreshState, + modifier = Modifier + .align(Alignment.TopCenter) + .offset(y = -8.dp), + ) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 76993c8..b8c7a29 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -21,6 +21,7 @@ materialExtendedIcon = "1.6.1" datastorePreferences = "1.0.0" okhttp = "4.12.0" retrofit = "2.9.0" +paging3 = "3.2.1" [libraries] @@ -55,7 +56,8 @@ converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } material-icons-extended = { group = "org.jetbrains.compose.material", name = "material-icons-extended", version.ref = "materialExtendedIcon" } - +paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging3" } +paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging3" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" }