From 11746b3ce5816f4e1e37d18a9679c1b12379dd1e Mon Sep 17 00:00:00 2001 From: Mohsen Rzna Date: Sun, 22 Oct 2023 01:54:24 +0200 Subject: [PATCH] Redesign + new logic + more news --- .../src/androidMain/AndroidManifest.xml | 2 + .../kotlin/com/client/news/MainActivity.kt | 2 +- .../ui/data/model/TopHeadlinesResponse.kt | 42 ---------- gradle/libs.versions.toml | 6 ++ shared/build.gradle.kts | 24 +++--- shared/src/androidMain/kotlin/main.android.kt | 1 + .../kotlin/data/dataSource/NewsDataSource.kt | 8 ++ .../data/dataSource/NewsDataSourceImpl.kt | 27 +++++++ .../kotlin/data/model/TopHeadlinesResponse.kt | 42 ++++++++++ .../kotlin/data/repository/NewsRepository.kt | 9 +++ .../data/repository/NewsRepositoryImpl.kt | 28 +++++++ .../src/commonMain/kotlin/data/util/Consts.kt | 6 ++ .../src/commonMain/kotlin/data/util/Result.kt | 21 +++++ .../commonMain/kotlin/data/util/Sources.kt | 9 +++ shared/src/commonMain/kotlin/di/Modules.kt | 18 ++++- .../src/commonMain/kotlin/ui/MainViewModel.kt | 46 ++++++++++- .../src/commonMain/kotlin/{ => ui}/NewsApp.kt | 34 ++------ .../kotlin/ui/components/NewsItem.kt | 37 --------- .../commonMain/kotlin/ui/main/MainScreen.kt | 80 +++++++++++++++++++ .../kotlin/ui/main/components/ArticleItem.kt | 60 ++++++++++++++ .../ui/{ => main}/components/NewsTopBar.kt | 2 +- .../kotlin/ui/main/components/SourceChip.kt | 28 +++++++ shared/src/iosMain/kotlin/main.ios.kt | 1 + 23 files changed, 409 insertions(+), 124 deletions(-) delete mode 100644 androidApp/src/androidMain/kotlin/com/client/news/ui/data/model/TopHeadlinesResponse.kt create mode 100644 shared/src/commonMain/kotlin/data/dataSource/NewsDataSource.kt create mode 100644 shared/src/commonMain/kotlin/data/dataSource/NewsDataSourceImpl.kt create mode 100644 shared/src/commonMain/kotlin/data/model/TopHeadlinesResponse.kt create mode 100644 shared/src/commonMain/kotlin/data/repository/NewsRepository.kt create mode 100644 shared/src/commonMain/kotlin/data/repository/NewsRepositoryImpl.kt create mode 100644 shared/src/commonMain/kotlin/data/util/Consts.kt create mode 100644 shared/src/commonMain/kotlin/data/util/Result.kt create mode 100644 shared/src/commonMain/kotlin/data/util/Sources.kt rename shared/src/commonMain/kotlin/{ => ui}/NewsApp.kt (71%) delete mode 100644 shared/src/commonMain/kotlin/ui/components/NewsItem.kt create mode 100644 shared/src/commonMain/kotlin/ui/main/MainScreen.kt create mode 100644 shared/src/commonMain/kotlin/ui/main/components/ArticleItem.kt rename shared/src/commonMain/kotlin/ui/{ => main}/components/NewsTopBar.kt (96%) create mode 100644 shared/src/commonMain/kotlin/ui/main/components/SourceChip.kt diff --git a/androidApp/src/androidMain/AndroidManifest.xml b/androidApp/src/androidMain/AndroidManifest.xml index 33b414b..97a37d7 100644 --- a/androidApp/src/androidMain/AndroidManifest.xml +++ b/androidApp/src/androidMain/AndroidManifest.xml @@ -1,6 +1,8 @@ + + , - @SerializedName("status") - val status: String, - @SerializedName("totalResults") - val totalResults: Int -) - -@Keep -data class Article( - @SerializedName("author") - val author: String, - @SerializedName("content") - val content: String, - @SerializedName("description") - val description: String, - @SerializedName("publishedAt") - val publishedAt: String, - @SerializedName("source") - val source: Source, - @SerializedName("title") - val title: String, - @SerializedName("url") - val url: String, - @SerializedName("urlToImage") - val urlToImage: String -) - -@Keep -data class Source( - @SerializedName("id") - val id: String, - @SerializedName("name") - val name: String -) \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bfbeb9a..c12acaa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -31,6 +31,8 @@ jetbrains-atomicfu = "0.22.0" koin = "3.5.0" koin-compose = "1.1.0" +kotlinxSerializationJson = "1.6.0" +ktorVersion = "2.3.5" loggingInterceptor = "4.12.0" mokoMvvmVersion = "0.16.1" retrofit = "2.9.0" @@ -64,6 +66,10 @@ androidx-dataStore-preferences = { group = "androidx.datastore", name = "datasto jetbrains-atomicfu = { module = "org.jetbrains.kotlinx:atomicfu", version.ref = "jetbrains-atomicfu" } coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } coil-gif = { group = "io.coil-kt", name = "coil-gif", version.ref = "coil" } +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorVersion" } +ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktorVersion" } +ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktorVersion" } logging-interceptor-4_9_1 = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "loggingInterceptor" } mlKit-Text-Recognition = { group = "com.google.android.gms", name = "play-services-mlkit-text-recognition", version.ref = "mlKitTextRecognition" } kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index cf660f6..40f7cc2 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -2,6 +2,7 @@ import org.jetbrains.compose.ExperimentalComposeLibrary plugins { kotlin("multiplatform") + kotlin("plugin.serialization") version "1.9.0" kotlin("native.cocoapods") id("com.android.library") id("org.jetbrains.compose") @@ -29,26 +30,27 @@ kotlin { sourceSets { val commonMain by getting { dependencies { - // Compose implementation(compose.material3) implementation(compose.runtime) implementation(compose.foundation) implementation(compose.ui) implementation(compose.materialIconsExtended) + @OptIn(ExperimentalComposeLibrary::class) implementation(compose.components.resources) implementation(libs.jetbrains.atomicfu) implementation(libs.koin.core) implementation(libs.koin.compose) - implementation(libs.moko.core) implementation(libs.moko.flow) + implementation(libs.ktor.client.core) + + implementation(libs.kotlinx.serialization.json) } } val androidMain by getting { dependencies { - // Compose api(libs.androidx.activity.compose) api(libs.androidx.appcompat) api(libs.androidx.core.ktx) @@ -59,16 +61,13 @@ kotlin { api(libs.androidx.compose.runtime) api(libs.androidx.lifecycle.runtimeCompose) - // Koin implementation(libs.koin.android) + implementation(libs.retrofit.core) + implementation(libs.gson.converter) + implementation(libs.okhttp.logging) + implementation(libs.kotlinx.coroutines.android) - // Retrofit - api(libs.retrofit.core) - api(libs.gson.converter) - api(libs.okhttp.logging) - - // Coroutines - api(libs.kotlinx.coroutines.android) + implementation(libs.ktor.client.okhttp) } } val iosX64Main by getting @@ -79,6 +78,9 @@ kotlin { iosX64Main.dependsOn(this) iosArm64Main.dependsOn(this) iosSimulatorArm64Main.dependsOn(this) + dependencies { + implementation(libs.ktor.client.darwin) + } } } } diff --git a/shared/src/androidMain/kotlin/main.android.kt b/shared/src/androidMain/kotlin/main.android.kt index 678c8ad..65460ea 100644 --- a/shared/src/androidMain/kotlin/main.android.kt +++ b/shared/src/androidMain/kotlin/main.android.kt @@ -1,4 +1,5 @@ import androidx.compose.runtime.Composable +import ui.NewsApp @Composable fun MainView() { diff --git a/shared/src/commonMain/kotlin/data/dataSource/NewsDataSource.kt b/shared/src/commonMain/kotlin/data/dataSource/NewsDataSource.kt new file mode 100644 index 0000000..548418a --- /dev/null +++ b/shared/src/commonMain/kotlin/data/dataSource/NewsDataSource.kt @@ -0,0 +1,8 @@ +package data.dataSource + +import data.model.TopHeadlinesResponse + +interface NewsDataSource { + suspend fun getTopHeadlines(): TopHeadlinesResponse + suspend fun getWallStreetJournal(): TopHeadlinesResponse +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/data/dataSource/NewsDataSourceImpl.kt b/shared/src/commonMain/kotlin/data/dataSource/NewsDataSourceImpl.kt new file mode 100644 index 0000000..135bc47 --- /dev/null +++ b/shared/src/commonMain/kotlin/data/dataSource/NewsDataSourceImpl.kt @@ -0,0 +1,27 @@ +package data.dataSource + +import data.model.TopHeadlinesResponse +import data.util.Consts +import data.util.Sources +import data.util.Sources.WALL_STREET_JOURNAL +import io.ktor.client.HttpClient +import io.ktor.client.request.get +import io.ktor.client.statement.bodyAsText +import kotlinx.serialization.json.Json + +class NewsDataSourceImpl(private val client: HttpClient) : NewsDataSource { + + override suspend fun getTopHeadlines(): TopHeadlinesResponse { + val response = getSpecificNewsBySource(Sources.TechCrunch.value) + return Json.decodeFromString(response) + } + + override suspend fun getWallStreetJournal(): TopHeadlinesResponse { + val response = getSpecificNewsBySource(WALL_STREET_JOURNAL.value) + return Json.decodeFromString(response) + } + + private suspend fun getSpecificNewsBySource(source: String): String { + return client.get(Consts.BASE_URL + source + Consts.API_KEY).bodyAsText() + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/data/model/TopHeadlinesResponse.kt b/shared/src/commonMain/kotlin/data/model/TopHeadlinesResponse.kt new file mode 100644 index 0000000..a8c5e2a --- /dev/null +++ b/shared/src/commonMain/kotlin/data/model/TopHeadlinesResponse.kt @@ -0,0 +1,42 @@ +package data.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class TopHeadlinesResponse( + @SerialName("articles") + val articles: List
, + @SerialName("status") + val status: String, + @SerialName("totalResults") + val totalResults: Int +) + +@Serializable +data class Article( + @SerialName("author") + val author: String, + @SerialName("content") + val content: String, + @SerialName("description") + val description: String, + @SerialName("publishedAt") + val publishedAt: String, + @SerialName("source") + val source: Source, + @SerialName("title") + val title: String, + @SerialName("url") + val url: String, + @SerialName("urlToImage") + val urlToImage: String +) + +@Serializable +data class Source( + @SerialName("id") + val id: String, + @SerialName("name") + val name: String +) diff --git a/shared/src/commonMain/kotlin/data/repository/NewsRepository.kt b/shared/src/commonMain/kotlin/data/repository/NewsRepository.kt new file mode 100644 index 0000000..adb1644 --- /dev/null +++ b/shared/src/commonMain/kotlin/data/repository/NewsRepository.kt @@ -0,0 +1,9 @@ +package data.repository + +import data.model.TopHeadlinesResponse +import kotlinx.coroutines.flow.Flow + +interface NewsRepository { + fun getTopHeadlines(): Flow + fun getTopHeadlinesByTopic(topic: String): Flow +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/data/repository/NewsRepositoryImpl.kt b/shared/src/commonMain/kotlin/data/repository/NewsRepositoryImpl.kt new file mode 100644 index 0000000..0743820 --- /dev/null +++ b/shared/src/commonMain/kotlin/data/repository/NewsRepositoryImpl.kt @@ -0,0 +1,28 @@ +package data.repository + +import data.dataSource.NewsDataSource +import data.dataSource.NewsDataSourceImpl +import data.model.TopHeadlinesResponse +import data.util.Sources +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.IO +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOn + +class NewsRepositoryImpl( + private val newsDataSource: NewsDataSource, + private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO +) : NewsRepository { + + override fun getTopHeadlines(): Flow = flow { + val topHeadlines = newsDataSource.getTopHeadlines() + emit(topHeadlines) + }.flowOn(ioDispatcher) + + override fun getTopHeadlinesByTopic(topic: String): Flow = flow { + val topHeadlines = newsDataSource.getTopHeadlines() + emit(topHeadlines) + }.flowOn(ioDispatcher) +} diff --git a/shared/src/commonMain/kotlin/data/util/Consts.kt b/shared/src/commonMain/kotlin/data/util/Consts.kt new file mode 100644 index 0000000..ae19bd2 --- /dev/null +++ b/shared/src/commonMain/kotlin/data/util/Consts.kt @@ -0,0 +1,6 @@ +package data.util + +object Consts { + const val BASE_URL = "https://newsapi.org/v2/" + const val API_KEY = "048261763a0240babf29ff0a2e567780" +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/data/util/Result.kt b/shared/src/commonMain/kotlin/data/util/Result.kt new file mode 100644 index 0000000..c785fe2 --- /dev/null +++ b/shared/src/commonMain/kotlin/data/util/Result.kt @@ -0,0 +1,21 @@ +package data.util + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart + +sealed interface Result { + data class Success(val data: T) : Result + data class Error(val exception: Throwable? = null) : Result + data object Loading : Result +} + +fun Flow.asResult(): Flow> { + return this + .map> { + Result.Success(it) + } + .onStart { emit(Result.Loading) } + .catch { emit(Result.Error(it)) } +} diff --git a/shared/src/commonMain/kotlin/data/util/Sources.kt b/shared/src/commonMain/kotlin/data/util/Sources.kt new file mode 100644 index 0000000..26d4ee5 --- /dev/null +++ b/shared/src/commonMain/kotlin/data/util/Sources.kt @@ -0,0 +1,9 @@ +package data.util + +enum class Sources(val value: String) { + TechCrunch("top-headlines?sources=techcrunch&apiKey="), + Business("top-headlines?country=us&category=business&apiKey="), + WALL_STREET_JOURNAL("everything?domains=wsj.com&apiKey="), + TESLA("everything?q=tesla&from=2023-09-21&sortBy=publishedAt&apiKey="), + APPLE("everything?q=apple&from=2023-10-20&to=2023-10-20&sortBy=popularity&apiKey=") +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/di/Modules.kt b/shared/src/commonMain/kotlin/di/Modules.kt index 3bc03a0..266de13 100644 --- a/shared/src/commonMain/kotlin/di/Modules.kt +++ b/shared/src/commonMain/kotlin/di/Modules.kt @@ -1,11 +1,21 @@ package di -import data.repository.MainRepositoryImpl +import data.dataSource.NewsDataSource +import data.dataSource.NewsDataSourceImpl +import data.repository.NewsRepository +import data.repository.NewsRepositoryImpl +import io.ktor.client.HttpClient import org.koin.dsl.module import ui.MainViewModel -fun appModule() = module { - single { MainRepositoryImpl() } +val appModule = module { + viewModelDefinition { MainViewModel(get()) } +} - viewModelDefinition { MainViewModel() } +val networkModule = module { + factory { HttpClient() } + single { NewsDataSourceImpl(get()) } + + // Repository + single { NewsRepositoryImpl(get()) } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/ui/MainViewModel.kt b/shared/src/commonMain/kotlin/ui/MainViewModel.kt index 54d4695..7830b0d 100644 --- a/shared/src/commonMain/kotlin/ui/MainViewModel.kt +++ b/shared/src/commonMain/kotlin/ui/MainViewModel.kt @@ -1,9 +1,53 @@ package ui +import androidx.compose.ui.text.capitalize +import androidx.compose.ui.text.intl.Locale +import androidx.compose.ui.text.toLowerCase +import data.model.Article +import data.repository.NewsRepository +import data.util.Sources +import data.util.asResult import dev.icerock.moko.mvvm.viewmodel.ViewModel +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn class MainViewModel( - // private val repository: MainRepository + mainRepository: NewsRepository ) : ViewModel() { + val topHeadlines: StateFlow = mainRepository.getTopHeadlines() + .asResult() + .map { result -> + when (result) { + is data.util.Result.Loading -> TopHeadlinesState.Loading + is data.util.Result.Success -> TopHeadlinesState.Success(result.data.articles) + is data.util.Result.Error -> { + TopHeadlinesState.Error(result.exception?.message ?: "Unknown error") + } + } + }.stateIn( + scope = viewModelScope, + started = SharingStarted.WhileSubscribed(5_000L), + initialValue = TopHeadlinesState.Loading + ) + + fun getSources(): List { + val sources = Sources.entries + val filteredNames = sources + .map(Sources::name) + .map { source -> + source.replace("_", " ") + .toLowerCase(Locale.current) + .capitalize(Locale.current) + } + return filteredNames + } +} + +sealed interface TopHeadlinesState { + data object Loading : TopHeadlinesState + data class Success(val articles: List
) : TopHeadlinesState + data class Error(val error: String) : TopHeadlinesState } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/NewsApp.kt b/shared/src/commonMain/kotlin/ui/NewsApp.kt similarity index 71% rename from shared/src/commonMain/kotlin/NewsApp.kt rename to shared/src/commonMain/kotlin/ui/NewsApp.kt index e32d691..4201c32 100644 --- a/shared/src/commonMain/kotlin/NewsApp.kt +++ b/shared/src/commonMain/kotlin/ui/NewsApp.kt @@ -1,3 +1,5 @@ +package ui + import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets @@ -15,19 +17,19 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp -import ui.components.NewsItem -import ui.components.NewsTopBar +import ui.main.components.NewsTopBar import di.appModule +import di.networkModule import org.koin.compose.KoinApplication import org.koin.compose.koinInject -import ui.MainViewModel +import ui.main.MainScreen @Composable fun NewsApp( modifier: Modifier = Modifier ) { KoinApplication(application = { - modules(appModule()) + modules(appModule, networkModule) }) { MaterialTheme { val snackBarHostState = remember { SnackbarHostState() } @@ -40,31 +42,9 @@ fun NewsApp( ) { Column(modifier = modifier.fillMaxSize()) { NewsTopBar() - MainContent() + MainScreen() } } } } -} - -@Composable -private fun MainContent( - viewModel: MainViewModel = koinInject() -) { - val lazyListState = rememberLazyGridState() - LazyVerticalGrid( - modifier = Modifier.fillMaxWidth(), - state = lazyListState, - columns = GridCells.Fixed(2), - contentPadding = PaddingValues( - start = 12.dp, - top = 16.dp, - end = 12.dp, - bottom = 16.dp - ), - ) { - items(100) { - NewsItem() - } - } } \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/ui/components/NewsItem.kt b/shared/src/commonMain/kotlin/ui/components/NewsItem.kt deleted file mode 100644 index aaac380..0000000 --- a/shared/src/commonMain/kotlin/ui/components/NewsItem.kt +++ /dev/null @@ -1,37 +0,0 @@ -package ui.components - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import org.jetbrains.compose.resources.ExperimentalResourceApi -import org.jetbrains.compose.resources.painterResource - -@OptIn(ExperimentalResourceApi::class) -@Composable -fun NewsItem() { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - ) { - Image( - modifier = Modifier.fillMaxSize(), - painter = painterResource("compose.png"), - contentDescription = null - ) - - Text(text = "Title") - - Spacer(modifier = Modifier.padding(4.dp)) - - Text(text = "This is a news item") - } -} - diff --git a/shared/src/commonMain/kotlin/ui/main/MainScreen.kt b/shared/src/commonMain/kotlin/ui/main/MainScreen.kt new file mode 100644 index 0000000..390987a --- /dev/null +++ b/shared/src/commonMain/kotlin/ui/main/MainScreen.kt @@ -0,0 +1,80 @@ +package ui.main + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope +import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid +import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells +import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.unit.dp +import org.koin.compose.koinInject +import ui.MainViewModel +import ui.TopHeadlinesState +import ui.main.components.ArticleItem +import ui.main.components.SourceChip + +@Composable +internal fun MainScreen( + modifier: Modifier = Modifier, + mainViewModel: MainViewModel = koinInject() +) { + val topHeadlinesState by mainViewModel.topHeadlines.collectAsState() + val state = rememberLazyStaggeredGridState() + val sources = mainViewModel.getSources() + + LazyRow( + modifier = modifier + .fillMaxWidth(), + contentPadding = PaddingValues(16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(sources.size) { + val source = sources[it] + SourceChip( + name = source, + onClick = { /* Do something! */ } + ) + } + } + + LazyVerticalStaggeredGrid( + columns = StaggeredGridCells.Adaptive(300.dp), + contentPadding = PaddingValues(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalItemSpacing = 24.dp, + modifier = Modifier.testTag("home:feed"), + state = state, + ) { + mainContent(topHeadlinesState) + } +} + +private fun LazyStaggeredGridScope.mainContent( + topHeadlinesState: TopHeadlinesState +) { + when (topHeadlinesState) { + is TopHeadlinesState.Loading -> Unit + is TopHeadlinesState.Success -> { + val articles = topHeadlinesState.articles + items(articles.size) { index -> + val article = articles[index] + ArticleItem(article) + } + } + + is TopHeadlinesState.Error -> { + val error = topHeadlinesState.error + item { + Text(text = error) + } + } + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/ui/main/components/ArticleItem.kt b/shared/src/commonMain/kotlin/ui/main/components/ArticleItem.kt new file mode 100644 index 0000000..0e22a1e --- /dev/null +++ b/shared/src/commonMain/kotlin/ui/main/components/ArticleItem.kt @@ -0,0 +1,60 @@ +package ui.main.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +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.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import data.model.Article +import org.jetbrains.compose.resources.ExperimentalResourceApi +import org.jetbrains.compose.resources.painterResource + +@OptIn(ExperimentalResourceApi::class) +@Composable +internal fun ArticleItem(article: Article) { + Card( + modifier = Modifier.fillMaxWidth() + .padding(8.dp), + elevation = CardDefaults.cardElevation( + defaultElevation = 3.dp + ) + ) { + Column( + modifier = Modifier + .fillMaxSize(), + ) { + Image( + modifier = Modifier.fillMaxWidth(), + painter = painterResource("compose.png"), + contentDescription = null + ) + + Spacer(modifier = Modifier.height(5.dp)) + + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = article.title, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold + ) + + Spacer(modifier = Modifier.padding(4.dp)) + + Text( + text = article.description, + style = MaterialTheme.typography.bodySmall + ) + } + } + } +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/ui/components/NewsTopBar.kt b/shared/src/commonMain/kotlin/ui/main/components/NewsTopBar.kt similarity index 96% rename from shared/src/commonMain/kotlin/ui/components/NewsTopBar.kt rename to shared/src/commonMain/kotlin/ui/main/components/NewsTopBar.kt index 3c26a38..f1d8481 100644 --- a/shared/src/commonMain/kotlin/ui/components/NewsTopBar.kt +++ b/shared/src/commonMain/kotlin/ui/main/components/NewsTopBar.kt @@ -1,4 +1,4 @@ -package ui.components +package ui.main.components import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.CenterAlignedTopAppBar diff --git a/shared/src/commonMain/kotlin/ui/main/components/SourceChip.kt b/shared/src/commonMain/kotlin/ui/main/components/SourceChip.kt new file mode 100644 index 0000000..7b6e9b7 --- /dev/null +++ b/shared/src/commonMain/kotlin/ui/main/components/SourceChip.kt @@ -0,0 +1,28 @@ +package ui.main.components + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SuggestionChip +import androidx.compose.material3.SuggestionChipDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.unit.dp + +@Composable +internal fun SourceChip( + onClick: () -> Unit, + name: String +) { + SuggestionChip( + onClick = onClick, + label = { Text(text = name) }, + enabled = true, + shape = RoundedCornerShape(12.dp), + colors = SuggestionChipDefaults.suggestionChipColors( + containerColor = MaterialTheme.colorScheme.secondaryContainer, + labelColor = MaterialTheme.colorScheme.onSecondaryContainer, + disabledContainerColor = MaterialTheme.colorScheme.secondaryContainer, + disabledLabelColor = MaterialTheme.colorScheme.onSecondaryContainer + ) + ) +} \ No newline at end of file diff --git a/shared/src/iosMain/kotlin/main.ios.kt b/shared/src/iosMain/kotlin/main.ios.kt index dfa4f29..e45ef4d 100644 --- a/shared/src/iosMain/kotlin/main.ios.kt +++ b/shared/src/iosMain/kotlin/main.ios.kt @@ -1,3 +1,4 @@ import androidx.compose.ui.window.ComposeUIViewController +import ui.NewsApp fun MainViewController() = ComposeUIViewController { NewsApp() } \ No newline at end of file