From 949d8a2497fcdd3dffcf2d0bf2870d715fa643c3 Mon Sep 17 00:00:00 2001 From: Jacquiline Gitau Date: Fri, 30 Jun 2023 00:10:39 +0300 Subject: [PATCH] Feed api implementation (#125) * Adding Feed.kt file * Adding FeedRepo.kt Interface file * Renaming Feed.kt to FeedDTO.kt * Adding Feed ViewModel * Adds feed implementation * Changing API BaseURL Test * Adding tests in FeedScreenTest.kt file * Renamed api -> FeedApi * Changed ResourceResult> -> List * Referencing String directly from the composable * Modifying FeedScreenTest.kt file * Create droidcon_logo_dark.xml * check mode in code * Added codeAnalysis.bat for windows uers and ran it * set it for all AppBars' * use if with id * contrib-readme-action has updated readme (#127) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Update to AS Flamingo (#126) * updates * Update AS Version * Dependencies updates * update readme * Update README.md * Update README.md * Update README.md * Update README.md * rename * update java version on CI * contrib-readme-action has updated readme (#130) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * contrib-readme-action has updated readme (#131) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * contrib-readme-action has updated readme (#132) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Migrate some State and viewModel calls from their composables to their respective ViewModels * contrib-readme-action has updated readme * contrib-readme-action has updated readme (#134) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> * Adds feed implementation * Handling state in the FeedScreen.kt file * Refactoring results * Fixes failing tests * Fixes String resource merge conflicts --------- Co-authored-by: brian.orwe Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Harun Wangereka Co-authored-by: yveskalume --- .../java/com/android254/data/di/RepoModule.kt | 6 + .../android254/data/network/apis/FeedApi.kt | 14 +- .../models/responses/{Feed.kt => FeedDTO.kt} | 2 +- .../com/android254/data/repos/FeedManager.kt | 37 +++++ .../data/repos/mappers/FeedMappers.kt | 28 ++++ .../data/network/apis/FeedApiTest.kt | 18 ++- .../java/com/android254/domain/models/Feed.kt | 25 ++++ .../com/android254/domain/repos/FeedRepo.kt | 23 ++++ .../common/bottomsheet/Strings.kt | 52 ++++++++ .../presentation/feed/FeedViewModel.kt | 51 +++++++ .../presentation/feed/view/FeedComponent.kt | 39 +++--- .../presentation/feed/view/FeedMappers.kt | 28 ++++ .../presentation/feed/view/FeedScreen.kt | 126 ++++++++++++------ .../presentation/feed/view/FeedUIState.kt | 25 ++++ .../feedback/view/FeedBackScreen.kt | 3 +- .../android254/presentation/models/FeedUI.kt | 25 ++++ presentation/src/main/res/values/strings.xml | 1 + .../presentation/feed/view/FeedScreenTest.kt | 16 ++- 18 files changed, 442 insertions(+), 77 deletions(-) rename data/src/main/java/com/android254/data/network/models/responses/{Feed.kt => FeedDTO.kt} (98%) create mode 100644 data/src/main/java/com/android254/data/repos/FeedManager.kt create mode 100644 data/src/main/java/com/android254/data/repos/mappers/FeedMappers.kt create mode 100644 domain/src/main/java/com/android254/domain/models/Feed.kt create mode 100644 domain/src/main/java/com/android254/domain/repos/FeedRepo.kt create mode 100644 presentation/src/main/java/com/android254/presentation/common/bottomsheet/Strings.kt create mode 100644 presentation/src/main/java/com/android254/presentation/feed/FeedViewModel.kt create mode 100644 presentation/src/main/java/com/android254/presentation/feed/view/FeedMappers.kt create mode 100644 presentation/src/main/java/com/android254/presentation/feed/view/FeedUIState.kt create mode 100644 presentation/src/main/java/com/android254/presentation/models/FeedUI.kt diff --git a/data/src/main/java/com/android254/data/di/RepoModule.kt b/data/src/main/java/com/android254/data/di/RepoModule.kt index c6c4e7fd..abea8f5d 100644 --- a/data/src/main/java/com/android254/data/di/RepoModule.kt +++ b/data/src/main/java/com/android254/data/di/RepoModule.kt @@ -16,11 +16,13 @@ package com.android254.data.di import com.android254.data.repos.AuthManager +import com.android254.data.repos.FeedManager import com.android254.data.repos.HomeRepoImpl import com.android254.data.repos.OrganizersSource import com.android254.data.repos.SessionsManager import com.android254.data.repos.SpeakersManager import com.android254.domain.repos.AuthRepo +import com.android254.domain.repos.FeedRepo import com.android254.domain.repos.HomeRepo import com.android254.domain.repos.OrganizersRepository import com.android254.domain.repos.SessionsRepo @@ -54,4 +56,8 @@ abstract class RepoModule { @Binds @Singleton abstract fun provideOrganizersRepo(source: OrganizersSource): OrganizersRepository + + @Binds + @Singleton + abstract fun provideFeedRepo(manager: FeedManager): FeedRepo } \ No newline at end of file diff --git a/data/src/main/java/com/android254/data/network/apis/FeedApi.kt b/data/src/main/java/com/android254/data/network/apis/FeedApi.kt index e532925c..9b832f45 100644 --- a/data/src/main/java/com/android254/data/network/apis/FeedApi.kt +++ b/data/src/main/java/com/android254/data/network/apis/FeedApi.kt @@ -15,20 +15,20 @@ */ package com.android254.data.network.apis -import com.android254.data.network.models.responses.Feed +import com.android254.data.network.models.responses.FeedDTO import com.android254.data.network.models.responses.PaginatedResponse import com.android254.data.network.util.dataResultSafeApiCall -import com.android254.data.network.util.provideEventBaseUrl -import io.ktor.client.* -import io.ktor.client.call.* -import io.ktor.client.request.* +import com.android254.data.network.util.provideBaseUrl +import io.ktor.client.HttpClient +import io.ktor.client.call.body +import io.ktor.client.request.get import javax.inject.Inject class FeedApi @Inject constructor(private val client: HttpClient) { suspend fun fetchFeed(page: Int = 1, size: Int = 100) = dataResultSafeApiCall { - val response: PaginatedResponse> = - client.get("${provideEventBaseUrl()}/feeds") { + val response: PaginatedResponse> = + client.get("${provideBaseUrl()}/feeds") { url { parameters.append("page", page.toString()) parameters.append("per_page", size.toString()) diff --git a/data/src/main/java/com/android254/data/network/models/responses/Feed.kt b/data/src/main/java/com/android254/data/network/models/responses/FeedDTO.kt similarity index 98% rename from data/src/main/java/com/android254/data/network/models/responses/Feed.kt rename to data/src/main/java/com/android254/data/network/models/responses/FeedDTO.kt index 0deecc1f..f82f99c2 100644 --- a/data/src/main/java/com/android254/data/network/models/responses/Feed.kt +++ b/data/src/main/java/com/android254/data/network/models/responses/FeedDTO.kt @@ -27,7 +27,7 @@ import java.time.LocalDateTime import java.time.format.DateTimeFormatter @Serializable -data class Feed( +data class FeedDTO( val title: String, val body: String, val topic: String, diff --git a/data/src/main/java/com/android254/data/repos/FeedManager.kt b/data/src/main/java/com/android254/data/repos/FeedManager.kt new file mode 100644 index 00000000..40fbfcd8 --- /dev/null +++ b/data/src/main/java/com/android254/data/repos/FeedManager.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2023 DroidconKE + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android254.data.repos + +import com.android254.data.network.apis.FeedApi +import com.android254.data.repos.mappers.toDomain +import com.android254.domain.models.DataResult +import com.android254.domain.models.Feed +import com.android254.domain.models.ResourceResult +import com.android254.domain.repos.FeedRepo +import javax.inject.Inject + +class FeedManager @Inject constructor( + private val FeedApi: FeedApi +) : FeedRepo { + override suspend fun fetchFeed(): ResourceResult> { + return when (val result = FeedApi.fetchFeed(1, 100)) { + DataResult.Empty -> ResourceResult.Empty("Empty list ") + is DataResult.Error -> ResourceResult.Error(result.message) + is DataResult.Loading -> ResourceResult.Loading(true) + is DataResult.Success -> ResourceResult.Success(result.data.map { it.toDomain() }) + } + } +} \ No newline at end of file diff --git a/data/src/main/java/com/android254/data/repos/mappers/FeedMappers.kt b/data/src/main/java/com/android254/data/repos/mappers/FeedMappers.kt new file mode 100644 index 00000000..cdda6bda --- /dev/null +++ b/data/src/main/java/com/android254/data/repos/mappers/FeedMappers.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2023 DroidconKE + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android254.data.repos.mappers + +import com.android254.data.network.models.responses.FeedDTO +import com.android254.domain.models.Feed + +fun FeedDTO.toDomain() = Feed( + title = title, + body = body, + topic = topic, + url = url, + image = image, + createdAt = createdAt.toString() +) \ No newline at end of file diff --git a/data/src/test/java/com/android254/data/network/apis/FeedApiTest.kt b/data/src/test/java/com/android254/data/network/apis/FeedApiTest.kt index 3b3fce75..5c6a6611 100644 --- a/data/src/test/java/com/android254/data/network/apis/FeedApiTest.kt +++ b/data/src/test/java/com/android254/data/network/apis/FeedApiTest.kt @@ -15,15 +15,19 @@ */ package com.android254.data.network.apis -import com.android254.data.network.models.responses.Feed +import com.android254.data.network.models.responses.FeedDTO import com.android254.data.network.util.HttpClientFactory import com.android254.data.network.util.MockTokenProvider import com.android254.data.network.util.RemoteFeatureToggle -import com.android254.data.network.util.provideEventBaseUrl +import com.android254.data.network.util.provideBaseUrl import com.android254.domain.models.DataResult import com.google.firebase.remoteconfig.FirebaseRemoteConfig -import io.ktor.client.engine.mock.* -import io.ktor.http.* +import io.ktor.client.engine.mock.MockEngine +import io.ktor.client.engine.mock.respond +import io.ktor.client.engine.mock.respondOk +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpMethod +import io.ktor.http.headersOf import io.mockk.mockk import kotlinx.coroutines.test.runTest import org.hamcrest.CoreMatchers.`is` @@ -53,7 +57,7 @@ class FeedApiTest { assertThat(mockEngine.requestHistory.size, `is`(1)) mockEngine.requestHistory.first().run { - val expectedUrl = "${provideEventBaseUrl()}/feeds?page=2&per_page=50" + val expectedUrl = "${provideBaseUrl()}/feeds?page=2&per_page=50" assertThat(url.toString(), `is`(expectedUrl)) assertThat(method, `is`(HttpMethod.Get)) } @@ -108,7 +112,7 @@ class FeedApiTest { `is`( DataResult.Success( listOf( - Feed( + FeedDTO( title = "Test", body = "Good one", topic = "droidconweb", @@ -119,7 +123,7 @@ class FeedApiTest { LocalTime.parse("18:45:49") ) ), - Feed( + FeedDTO( title = "niko fine", body = "this is a test", topic = "droidconweb", diff --git a/domain/src/main/java/com/android254/domain/models/Feed.kt b/domain/src/main/java/com/android254/domain/models/Feed.kt new file mode 100644 index 00000000..2199abba --- /dev/null +++ b/domain/src/main/java/com/android254/domain/models/Feed.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2023 DroidconKE + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android254.domain.models + +class Feed( + val title: String, + val body: String, + val topic: String, + val url: String, + val image: String?, + val createdAt: String +) \ No newline at end of file diff --git a/domain/src/main/java/com/android254/domain/repos/FeedRepo.kt b/domain/src/main/java/com/android254/domain/repos/FeedRepo.kt new file mode 100644 index 00000000..78eb68c7 --- /dev/null +++ b/domain/src/main/java/com/android254/domain/repos/FeedRepo.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2023 DroidconKE + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android254.domain.repos + +import com.android254.domain.models.Feed +import com.android254.domain.models.ResourceResult + +interface FeedRepo { + suspend fun fetchFeed(): ResourceResult> +} \ No newline at end of file diff --git a/presentation/src/main/java/com/android254/presentation/common/bottomsheet/Strings.kt b/presentation/src/main/java/com/android254/presentation/common/bottomsheet/Strings.kt new file mode 100644 index 00000000..103dcbdf --- /dev/null +++ b/presentation/src/main/java/com/android254/presentation/common/bottomsheet/Strings.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2023 DroidconKE + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android254.presentation.common.bottomsheet + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.R +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalContext + +@Immutable +@kotlin.jvm.JvmInline +value class Strings private constructor(@Suppress("unused") private val value: Int) { + companion object { + val NavigationMenu = Strings(0) + val CloseDrawer = Strings(1) + val CloseSheet = Strings(2) + val DefaultErrorMessage = Strings(3) + val ExposedDropdownMenu = Strings(4) + val SliderRangeStart = Strings(5) + val SliderRangeEnd = Strings(6) + } +} + +@Composable +fun getString(string: Strings): String { + LocalConfiguration.current + val resources = LocalContext.current.resources + return when (string) { + Strings.NavigationMenu -> resources.getString(R.string.navigation_menu) + Strings.CloseDrawer -> resources.getString(R.string.close_drawer) + Strings.CloseSheet -> resources.getString(R.string.close_sheet) + Strings.DefaultErrorMessage -> resources.getString(R.string.default_error_message) + Strings.ExposedDropdownMenu -> resources.getString(R.string.dropdown_menu) + Strings.SliderRangeStart -> resources.getString(R.string.range_start) + Strings.SliderRangeEnd -> resources.getString(R.string.range_end) + else -> "" + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/android254/presentation/feed/FeedViewModel.kt b/presentation/src/main/java/com/android254/presentation/feed/FeedViewModel.kt new file mode 100644 index 00000000..ccee2ca2 --- /dev/null +++ b/presentation/src/main/java/com/android254/presentation/feed/FeedViewModel.kt @@ -0,0 +1,51 @@ +/* + * Copyright 2023 DroidconKE + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android254.presentation.feed + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.android254.domain.models.ResourceResult +import com.android254.domain.repos.FeedRepo +import com.android254.presentation.feed.view.FeedUIState +import com.android254.presentation.feed.view.toPresentation +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class FeedViewModel @Inject constructor( + private val feedRepo: FeedRepo +) : ViewModel() { + var viewState: FeedUIState by mutableStateOf(FeedUIState.Loading) + private set + + fun fetchFeed() { + viewModelScope.launch { + viewState = when (val value = feedRepo.fetchFeed()) { + is ResourceResult.Empty -> FeedUIState.Empty + is ResourceResult.Error -> FeedUIState.Error(value.message) + is ResourceResult.Loading -> FeedUIState.Loading + is ResourceResult.Success -> FeedUIState.Success( + value.data?.map { it.toPresentation() } + ?: emptyList() + ) + } + } + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/android254/presentation/feed/view/FeedComponent.kt b/presentation/src/main/java/com/android254/presentation/feed/view/FeedComponent.kt index 49b4be1e..5303fbce 100644 --- a/presentation/src/main/java/com/android254/presentation/feed/view/FeedComponent.kt +++ b/presentation/src/main/java/com/android254/presentation/feed/view/FeedComponent.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 DroidconKE + * Copyright 2023 DroidconKE * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,17 +15,13 @@ */ package com.android254.presentation.feed.view -import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Newspaper import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon @@ -35,7 +31,7 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -45,6 +41,9 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage +import coil.request.ImageRequest +import com.android254.presentation.models.FeedUI import com.droidconke.chai.ChaiDCKE22Theme import com.droidconke.chai.atoms.ChaiBlue import com.droidconke.chai.atoms.ChaiLightGrey @@ -53,7 +52,11 @@ import com.droidconke.chai.atoms.MontserratBold import ke.droidcon.kotlin.presentation.R @Composable -fun FeedComponent(modifier: Modifier, onClickItem: (Int) -> Unit) { +fun FeedComponent( + modifier: Modifier, + feedPresentationModel: FeedUI, + onClickItem: (Int) -> Unit +) { Card( modifier = modifier .fillMaxWidth() @@ -70,7 +73,7 @@ fun FeedComponent(modifier: Modifier, onClickItem: (Int) -> Unit) { val textFromNetwork = stringResource(id = R.string.placeholder_long_text) Text( - text = textFromNetwork, + text = feedPresentationModel.body, color = MaterialTheme.colorScheme.onBackground, fontSize = MaterialTheme.typography.bodyMedium.fontSize, fontWeight = MaterialTheme.typography.bodyMedium.fontWeight, @@ -80,13 +83,11 @@ fun FeedComponent(modifier: Modifier, onClickItem: (Int) -> Unit) { overflow = TextOverflow.Ellipsis ) - Image( - modifier = Modifier - .fillMaxWidth() - .height(220.dp) - .clip(RoundedCornerShape(6.dp)), - imageVector = Icons.Rounded.Newspaper, - contentDescription = textFromNetwork + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(feedPresentationModel.image) + .build(), + contentDescription = stringResource(id = R.string.feed_image) ) Row( @@ -135,7 +136,11 @@ fun FeedComponent(modifier: Modifier, onClickItem: (Int) -> Unit) { @Composable fun Preview() { ChaiDCKE22Theme { - FeedComponent(modifier = Modifier) { - } + FeedComponent( + modifier = Modifier, + feedPresentationModel = + FeedUI("Feed", "Feed feed", "test", "", "", ""), + onClickItem = {} + ) } } \ No newline at end of file diff --git a/presentation/src/main/java/com/android254/presentation/feed/view/FeedMappers.kt b/presentation/src/main/java/com/android254/presentation/feed/view/FeedMappers.kt new file mode 100644 index 00000000..159b90d8 --- /dev/null +++ b/presentation/src/main/java/com/android254/presentation/feed/view/FeedMappers.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2023 DroidconKE + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android254.presentation.feed.view + +import com.android254.domain.models.Feed +import com.android254.presentation.models.FeedUI + +fun Feed.toPresentation() = FeedUI( + title = title, + body = body, + topic = topic, + url = url, + image = image, + createdAt = createdAt +) \ No newline at end of file diff --git a/presentation/src/main/java/com/android254/presentation/feed/view/FeedScreen.kt b/presentation/src/main/java/com/android254/presentation/feed/view/FeedScreen.kt index c2f1b146..ee31faa3 100644 --- a/presentation/src/main/java/com/android254/presentation/feed/view/FeedScreen.kt +++ b/presentation/src/main/java/com/android254/presentation/feed/view/FeedScreen.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 DroidconKE + * Copyright 2023 DroidconKE * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,74 +16,116 @@ package com.android254.presentation.feed.view import android.content.res.Configuration -import androidx.activity.compose.BackHandler +import androidx.compose.foundation.Image import androidx.compose.foundation.background -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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.material3.* +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Error +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.rememberSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.platform.testTag import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel import com.android254.presentation.common.components.DroidconAppBarWithFeedbackButton +import com.android254.presentation.feed.FeedViewModel import com.droidconke.chai.ChaiDCKE22Theme import kotlinx.coroutines.launch @Composable fun FeedScreen( - navigateToFeedbackScreen: () -> Unit = {} + navigateToFeedbackScreen: () -> Unit = {}, + feedViewModel: FeedViewModel = hiltViewModel() ) { + val bottomSheetState = rememberSheetState() val scope = rememberCoroutineScope() - val bottomSheetState = rememberSheetState( - skipHalfExpanded = true - ) - - BackHandler(bottomSheetState.isVisible) { - scope.launch { bottomSheetState.hide() } - } - - Scaffold( - topBar = { - DroidconAppBarWithFeedbackButton( - onButtonClick = { - navigateToFeedbackScreen() - }, - userProfile = "https://media-exp1.licdn.com/dms/image/C4D03AQGn58utIO-x3w/profile-displayphoto-shrink_200_200/0/1637478114039?e=2147483647&v=beta&t=3kIon0YJQNHZojD3Dt5HVODJqHsKdf2YKP1SfWeROnI" - ) + feedViewModel.fetchFeed() + val feedUIState = feedViewModel.viewState + if (bottomSheetState.isVisible) { + ModalBottomSheet( + sheetState = bottomSheetState, + onDismissRequest = { scope.launch { bottomSheetState.hide() } } + ) { + FeedShareSection() } - ) { paddingValues -> + } + Scaffold(topBar = { + DroidconAppBarWithFeedbackButton( + onButtonClick = { + navigateToFeedbackScreen() + }, + userProfile = "https://media-exp1.licdn.com/dms/image/C4D03AQGn58utIO-x3w/profile-displayphoto-shrink_200_200/0/1637478114039?e=2147483647&v=beta&t=3kIon0YJQNHZojD3Dt5HVODJqHsKdf2YKP1SfWeROnI" + ) + }) { paddingValues -> Box( modifier = Modifier - .padding(paddingValues) + .padding(paddingValues = paddingValues) .fillMaxSize() ) { - LazyColumn( - modifier = Modifier.testTag("feeds_lazy_column"), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - items(count = 10) { - FeedComponent(modifier = Modifier.fillMaxWidth()) { - scope.launch { - bottomSheetState.show() + when (feedUIState) { + is FeedUIState.Error -> { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + modifier = Modifier + .width(50.dp) + .height(50.dp), + imageVector = Icons.Rounded.Error, + contentDescription = "Error", + colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.error) + ) + Text(text = feedUIState.message) + } + } + + FeedUIState.Loading -> { + CircularProgressIndicator() + } + + is FeedUIState.Success -> { + LazyColumn( + modifier = Modifier.testTag("feeds_lazy_column"), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + items(feedUIState.feeds) { feedPresentationModel -> + FeedComponent( + modifier = Modifier.fillMaxWidth(), + feedPresentationModel + ) { + scope.launch { + bottomSheetState.show() + } + } } } } - } - } - } - if (bottomSheetState.isVisible) { - ModalBottomSheet( - sheetState = bottomSheetState, - onDismissRequest = { - scope.launch { - bottomSheetState.hide() + FeedUIState.Empty -> Column(modifier = Modifier.fillMaxSize()) { + Text(text = "Empty") } } - ) { - FeedShareSection() } } } diff --git a/presentation/src/main/java/com/android254/presentation/feed/view/FeedUIState.kt b/presentation/src/main/java/com/android254/presentation/feed/view/FeedUIState.kt new file mode 100644 index 00000000..03f82a6d --- /dev/null +++ b/presentation/src/main/java/com/android254/presentation/feed/view/FeedUIState.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2023 DroidconKE + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android254.presentation.feed.view + +import com.android254.presentation.models.FeedUI + +sealed interface FeedUIState { + object Loading : FeedUIState + object Empty : FeedUIState + data class Error(val message: String) : FeedUIState + data class Success(val feeds: List) : FeedUIState +} \ No newline at end of file diff --git a/presentation/src/main/java/com/android254/presentation/feedback/view/FeedBackScreen.kt b/presentation/src/main/java/com/android254/presentation/feedback/view/FeedBackScreen.kt index 1e1b5dc4..8c6402f0 100644 --- a/presentation/src/main/java/com/android254/presentation/feedback/view/FeedBackScreen.kt +++ b/presentation/src/main/java/com/android254/presentation/feedback/view/FeedBackScreen.kt @@ -171,7 +171,8 @@ fun FeedBackScreen( contentDescription = stringResource(id = R.string.sign_in_label) ) Text( - text = stringResource(R.string.Bad)) + text = stringResource(R.string.Bad) + ) } } Spacer(modifier = Modifier.width(20.dp)) diff --git a/presentation/src/main/java/com/android254/presentation/models/FeedUI.kt b/presentation/src/main/java/com/android254/presentation/models/FeedUI.kt new file mode 100644 index 00000000..9df74323 --- /dev/null +++ b/presentation/src/main/java/com/android254/presentation/models/FeedUI.kt @@ -0,0 +1,25 @@ +/* + * Copyright 2023 DroidconKE + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android254.presentation.models + +data class FeedUI( + val title: String, + val body: String, + val topic: String, + val url: String, + val image: String?, + val createdAt: String +) \ No newline at end of file diff --git a/presentation/src/main/res/values/strings.xml b/presentation/src/main/res/values/strings.xml index 81872397..400f0977 100644 --- a/presentation/src/main/res/values/strings.xml +++ b/presentation/src/main/res/values/strings.xml @@ -79,6 +79,7 @@ Bad Okay Great + Feed Image diff --git a/presentation/src/test/java/com/android254/presentation/feed/view/FeedScreenTest.kt b/presentation/src/test/java/com/android254/presentation/feed/view/FeedScreenTest.kt index 4644b34a..a3d5b08b 100644 --- a/presentation/src/test/java/com/android254/presentation/feed/view/FeedScreenTest.kt +++ b/presentation/src/test/java/com/android254/presentation/feed/view/FeedScreenTest.kt @@ -19,7 +19,13 @@ import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createComposeRule import androidx.compose.ui.test.onNodeWithTag import androidx.compose.ui.test.performClick +import com.android254.domain.models.Feed +import com.android254.domain.models.ResourceResult +import com.android254.domain.repos.FeedRepo import com.android254.presentation.common.theme.DroidconKE2023Theme +import com.android254.presentation.feed.FeedViewModel +import io.mockk.coEvery +import io.mockk.mockk import org.junit.Before import org.junit.Rule import org.junit.Test @@ -32,6 +38,8 @@ import org.robolectric.shadows.ShadowLog @Config(instrumentedPackages = ["androidx.loader.content"]) class FeedScreenTest { + private val repo = mockk() + @get:Rule val composeTestRule = createComposeRule() @@ -43,9 +51,11 @@ class FeedScreenTest { @Test fun `should display feed items`() { + coEvery { repo.fetchFeed() } returns ResourceResult.Success(listOf(Feed("", "", "", "", "", ""))) + composeTestRule.setContent { DroidconKE2023Theme { - FeedScreen() + FeedScreen(feedViewModel = FeedViewModel(repo)) } } @@ -58,9 +68,11 @@ class FeedScreenTest { @Test fun `test share bottom sheet is shown`() { + coEvery { repo.fetchFeed() } returns ResourceResult.Success(listOf(Feed("", "", "", "", "", ""))) + composeTestRule.setContent { DroidconKE2023Theme { - FeedScreen() + FeedScreen(feedViewModel = FeedViewModel(repo)) } } composeTestRule.onNodeWithTag("share_button").assertExists()