Merge pull request #54 from lihenggui/compose_multiplatform

Convert :core:ui to the multiplatform module
pull/2064/head
Mercury Li 2 years ago committed by GitHub
commit 9fa70b9073
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,32 @@
/*
* Copyright 2024 The Android Open Source Project
*
* 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
*
* https://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.google.samples.apps.nowinandroid.core.network
import android.content.Context
import me.tatarka.inject.annotations.Component
import me.tatarka.inject.annotations.Provides
@Component
abstract class ApplicationComponent (
@get:Provides val context: Context,
): PlatformComponent
interface ApplicationComponentProvider {
val component: ApplicationComponent
}
val Context.applicationComponent get() = (applicationContext as ApplicationComponentProvider).component

@ -0,0 +1,26 @@
/*
* Copyright 2024 The Android Open Source Project
*
* 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
*
* https://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.google.samples.apps.nowinandroid.core.network
import android.content.Context
import coil3.PlatformContext
import me.tatarka.inject.annotations.Provides
interface PlatformComponent {
@Provides
fun providePlatformContext(context: Context): PlatformContext = context
}

@ -14,8 +14,8 @@
* limitations under the License.
*/
plugins {
alias(libs.plugins.nowinandroid.android.library)
alias(libs.plugins.nowinandroid.android.library.compose)
alias(libs.plugins.nowinandroid.kmp.library)
alias(libs.plugins.jetbrains.compose)
alias(libs.plugins.nowinandroid.android.library.jacoco)
}
@ -26,15 +26,24 @@ android {
namespace = "com.google.samples.apps.nowinandroid.core.ui"
}
dependencies {
api(libs.androidx.metrics)
api(projects.core.analytics)
api(projects.core.designsystem)
api(projects.core.model)
implementation(libs.androidx.browser)
implementation(libs.coil)
implementation(libs.coil.compose)
androidTestImplementation(projects.core.testing)
kotlin {
sourceSets {
androidMain.dependencies {
api(libs.androidx.metrics)
implementation(libs.androidx.browser)
}
commonMain.dependencies {
api(projects.core.analytics)
api(projects.core.designsystem)
api(projects.core.model)
implementation(libs.coil)
implementation(libs.coil.compose)
implementation(compose.material3)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
}
androidInstrumentedTest.dependencies {
implementation(projects.core.testing)
}
}
}

@ -24,6 +24,8 @@ import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import com.google.samples.apps.nowinandroid.core.testing.data.followableTopicTestData
import com.google.samples.apps.nowinandroid.core.testing.data.userNewsResourcesTestData
import nowinandroid.core.ui.generated.resources.Res
import nowinandroid.core.ui.generated.resources.core_ui_card_meta_data_text
import org.junit.Rule
import org.junit.Test
@ -52,7 +54,7 @@ class NewsResourceCardTest {
composeTestRule
.onNodeWithText(
composeTestRule.activity.getString(
R.string.core_ui_card_meta_data_text,
Res.string.core_ui_card_meta_data_text,
dateFormatted,
newsWithKnownResourceType.type,
),

@ -16,14 +16,15 @@
package com.google.samples.apps.nowinandroid.core.ui
import androidx.compose.ui.tooling.preview.Preview
import org.jetbrains.compose.ui.tooling.preview.Preview
/**
* Multipreview annotation that represents various device sizes. Add this annotation to a composable
* to render various devices.
*/
@Preview(name = "phone", device = "spec:shape=Normal,width=360,height=640,unit=dp,dpi=480")
@Preview(name = "landscape", device = "spec:shape=Normal,width=640,height=360,unit=dp,dpi=480")
@Preview(name = "foldable", device = "spec:shape=Normal,width=673,height=841,unit=dp,dpi=480")
@Preview(name = "tablet", device = "spec:shape=Normal,width=1280,height=800,unit=dp,dpi=480")
//@Preview(name = "phone", device = "spec:shape=Normal,width=360,height=640,unit=dp,dpi=480")
//@Preview(name = "landscape", device = "spec:shape=Normal,width=640,height=360,unit=dp,dpi=480")
//@Preview(name = "foldable", device = "spec:shape=Normal,width=673,height=841,unit=dp,dpi=480")
//@Preview(name = "tablet", device = "spec:shape=Normal,width=1280,height=800,unit=dp,dpi=480")
@Preview
annotation class DevicePreviews

@ -18,9 +18,9 @@
package com.google.samples.apps.nowinandroid.core.ui
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.Topic
import org.jetbrains.compose.ui.tooling.preview.PreviewParameterProvider
/**
* This [PreviewParameterProvider](https://developer.android.com/reference/kotlin/androidx/compose/ui/tooling/preview/PreviewParameterProvider)

@ -16,11 +16,6 @@
package com.google.samples.apps.nowinandroid.core.ui
import android.content.Context
import android.net.Uri
import androidx.annotation.ColorInt
import androidx.browser.customtabs.CustomTabColorSchemeParams
import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyListScope
@ -28,18 +23,14 @@ 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.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.unit.dp
import com.google.samples.apps.nowinandroid.core.analytics.LocalAnalyticsHelper
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
import org.jetbrains.compose.ui.tooling.preview.Preview
import org.jetbrains.compose.ui.tooling.preview.PreviewParameter
/**
* An extension on [LazyListScope] defining a feed with news resources.
@ -61,10 +52,7 @@ fun LazyStaggeredGridScope.newsFeed(
key = { it.id },
contentType = { "newsFeedItem" },
) { userNewsResource ->
val context = LocalContext.current
val analyticsHelper = LocalAnalyticsHelper.current
val backgroundColor = MaterialTheme.colorScheme.background.toArgb()
NewsResourceCardExpanded(
userNewsResource = userNewsResource,
isBookmarked = userNewsResource.isSaved,
@ -73,7 +61,7 @@ fun LazyStaggeredGridScope.newsFeed(
analyticsHelper.logNewsResourceOpened(
newsResourceId = userNewsResource.id,
)
launchCustomChromeTab(context, Uri.parse(userNewsResource.url), backgroundColor)
launchCustomChromeTab(userNewsResource.url)
onNewsResourceViewed(userNewsResource.id)
},
@ -94,14 +82,8 @@ fun LazyStaggeredGridScope.newsFeed(
}
}
fun launchCustomChromeTab(context: Context, uri: Uri, @ColorInt toolbarColor: Int) {
val customTabBarColor = CustomTabColorSchemeParams.Builder()
.setToolbarColor(toolbarColor).build()
val customTabsIntent = CustomTabsIntent.Builder()
.setDefaultColorSchemeParams(customTabBarColor)
.build()
customTabsIntent.launchUrl(context, uri)
fun launchCustomChromeTab(uri: String) {
// Empty implementation
}
/**
@ -140,7 +122,7 @@ private fun NewsFeedLoadingPreview() {
}
@Preview
@Preview(device = Devices.TABLET)
//@Preview(device = Devices.TABLET)
@Composable
private fun NewsFeedContentPreview(
@PreviewParameter(UserNewsResourcePreviewParameterProvider::class)

@ -14,6 +14,8 @@
* limitations under the License.
*/
@file:OptIn(ExperimentalResourceApi::class)
package com.google.samples.apps.nowinandroid.core.ui
import androidx.compose.foundation.Canvas
@ -48,17 +50,15 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.text.intl.Locale
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImagePainter
import coil.compose.rememberAsyncImagePainter
import com.google.samples.apps.nowinandroid.core.designsystem.R.drawable
import coil3.ImageLoader
import coil3.PlatformContext
import coil3.compose.AsyncImagePainter
import coil3.compose.rememberAsyncImagePainter
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaTopicTag
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
@ -67,16 +67,22 @@ import com.google.samples.apps.nowinandroid.core.model.data.FollowableTopic
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource
import kotlinx.datetime.Instant
import kotlinx.datetime.toJavaInstant
import kotlinx.datetime.toJavaZoneId
import java.time.format.DateTimeFormatter
import java.time.format.FormatStyle
import java.util.Locale
import nowinandroid.core.ui.generated.resources.Res
import nowinandroid.core.ui.generated.resources.core_ui_bookmark
import nowinandroid.core.ui.generated.resources.core_ui_card_meta_data_text
import nowinandroid.core.ui.generated.resources.core_ui_card_tap_action
import nowinandroid.core.ui.generated.resources.core_ui_topic_chip_content_description_when_followed
import nowinandroid.core.ui.generated.resources.core_ui_topic_chip_content_description_when_not_followed
import nowinandroid.core.ui.generated.resources.core_ui_unbookmark
import nowinandroid.core.ui.generated.resources.core_ui_unread_resource_dot_content_description
import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.compose.ui.tooling.preview.Preview
import org.jetbrains.compose.ui.tooling.preview.PreviewParameter
/**
* [NewsResource] card used on the following screens: For You, Saved
*/
@Composable
fun NewsResourceCardExpanded(
userNewsResource: UserNewsResource,
@ -87,7 +93,7 @@ fun NewsResourceCardExpanded(
onTopicClick: (String) -> Unit,
modifier: Modifier = Modifier,
) {
val clickActionLabel = stringResource(R.string.core_ui_card_tap_action)
val clickActionLabel = stringResource(Res.string.core_ui_card_tap_action)
Card(
onClick = onClick,
shape = RoundedCornerShape(16.dp),
@ -147,12 +153,13 @@ fun NewsResourceHeaderImage(
) {
var isLoading by remember { mutableStateOf(true) }
var isError by remember { mutableStateOf(false) }
val imageLoader = rememberAsyncImagePainter(
val imagePainter = rememberAsyncImagePainter(
model = headerImageUrl,
onState = { state ->
isLoading = state is AsyncImagePainter.State.Loading
isError = state is AsyncImagePainter.State.Error
},
imageLoader = imageLoader,
)
val isLocalInspection = LocalInspectionMode.current
Box(
@ -176,11 +183,12 @@ fun NewsResourceHeaderImage(
.fillMaxWidth()
.height(180.dp),
contentScale = ContentScale.Crop,
painter = if (isError.not() && !isLocalInspection) {
imageLoader
} else {
painterResource(drawable.core_designsystem_ic_placeholder_default)
},
painter =
// if (isError.not() && !isLocalInspection) {
imagePainter,
// } else {
// painterResource(drawable.core_designsystem_ic_placeholder_default)
// },
// TODO b/226661685: Investigate using alt text of image to populate content description
// decorative image,
contentDescription = null,
@ -209,13 +217,13 @@ fun BookmarkButton(
icon = {
Icon(
imageVector = NiaIcons.BookmarkBorder,
contentDescription = stringResource(R.string.core_ui_bookmark),
contentDescription = stringResource(Res.string.core_ui_bookmark),
)
},
checkedIcon = {
Icon(
imageVector = NiaIcons.Bookmark,
contentDescription = stringResource(R.string.core_ui_unbookmark),
contentDescription = stringResource(Res.string.core_ui_unbookmark),
)
},
)
@ -226,7 +234,7 @@ fun NotificationDot(
color: Color,
modifier: Modifier = Modifier,
) {
val description = stringResource(R.string.core_ui_unread_resource_dot_content_description)
val description = stringResource(Res.string.core_ui_unread_resource_dot_content_description)
Canvas(
modifier = modifier
.semantics { contentDescription = description },
@ -240,11 +248,7 @@ fun NotificationDot(
}
@Composable
fun dateFormatted(publishDate: Instant): String = DateTimeFormatter
.ofLocalizedDate(FormatStyle.MEDIUM)
.withLocale(Locale.getDefault())
.withZone(LocalTimeZone.current.toJavaZoneId())
.format(publishDate.toJavaInstant())
fun dateFormatted(publishDate: Instant): String = publishDate.toString()
@Composable
fun NewsResourceMetaData(
@ -254,7 +258,7 @@ fun NewsResourceMetaData(
val formattedDate = dateFormatted(publishDate)
Text(
if (resourceType.isNotBlank()) {
stringResource(R.string.core_ui_card_meta_data_text, formattedDate, resourceType)
stringResource(Res.string.core_ui_card_meta_data_text, formattedDate, resourceType)
} else {
formattedDate
},
@ -287,17 +291,17 @@ fun NewsResourceTopics(
text = {
val contentDescription = if (followableTopic.isFollowed) {
stringResource(
R.string.core_ui_topic_chip_content_description_when_followed,
Res.string.core_ui_topic_chip_content_description_when_followed,
followableTopic.topic.name,
)
} else {
stringResource(
R.string.core_ui_topic_chip_content_description_when_not_followed,
Res.string.core_ui_topic_chip_content_description_when_not_followed,
followableTopic.topic.name,
)
}
Text(
text = followableTopic.topic.name.uppercase(Locale.getDefault()),
text = followableTopic.topic.name.uppercase(),
modifier = Modifier.semantics {
this.contentDescription = contentDescription
},
@ -308,7 +312,8 @@ fun NewsResourceTopics(
}
}
@Preview("Bookmark Button")
//@Preview("Bookmark Button")
@Preview
@Composable
private fun BookmarkButtonPreview() {
NiaTheme {
@ -318,7 +323,8 @@ private fun BookmarkButtonPreview() {
}
}
@Preview("Bookmark Button Bookmarked")
//@Preview("Bookmark Button Bookmarked")
@Preview
@Composable
private fun BookmarkButtonBookmarkedPreview() {
NiaTheme {
@ -328,7 +334,8 @@ private fun BookmarkButtonBookmarkedPreview() {
}
}
@Preview("NewsResourceCardExpanded")
@Preview
//@Preview("NewsResourceCardExpanded")
@Composable
private fun ExpandedNewsResourcePreview(
@PreviewParameter(UserNewsResourcePreviewParameterProvider::class)

@ -16,13 +16,11 @@
package com.google.samples.apps.nowinandroid.core.ui
import android.net.Uri
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import com.google.samples.apps.nowinandroid.core.analytics.LocalAnalyticsHelper
import com.google.samples.apps.nowinandroid.core.model.data.UserNewsResource

@ -18,7 +18,6 @@
package com.google.samples.apps.nowinandroid.core.ui
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.google.samples.apps.nowinandroid.core.model.data.DarkThemeConfig
import com.google.samples.apps.nowinandroid.core.model.data.NewsResource
import com.google.samples.apps.nowinandroid.core.model.data.ThemeBrand
@ -30,6 +29,7 @@ import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toInstant
import org.jetbrains.compose.ui.tooling.preview.PreviewParameterProvider
/**
* This [PreviewParameterProvider](https://developer.android.com/reference/kotlin/androidx/compose/ui/tooling/preview/PreviewParameterProvider)

@ -1,17 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2022 The Android Open Source Project
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.
-->
<manifest />

@ -1,207 +0,0 @@
/*
* Copyright 2024 The Android Open Source Project
*
* 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
*
* https://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.google.samples.apps.nowinandroid.core.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.selected
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.google.samples.apps.nowinandroid.core.designsystem.component.DynamicAsyncImage
import com.google.samples.apps.nowinandroid.core.designsystem.component.NiaIconToggleButton
import com.google.samples.apps.nowinandroid.core.designsystem.icon.NiaIcons
import com.google.samples.apps.nowinandroid.core.designsystem.theme.NiaTheme
import com.google.samples.apps.nowinandroid.core.ui.R.string
@Composable
fun InterestsItem(
name: String,
following: Boolean,
topicImageUrl: String,
onClick: () -> Unit,
onFollowButtonClick: (Boolean) -> Unit,
modifier: Modifier = Modifier,
iconModifier: Modifier = Modifier,
description: String = "",
isSelected: Boolean = false,
) {
ListItem(
leadingContent = {
InterestsIcon(topicImageUrl, iconModifier.size(64.dp))
},
headlineContent = {
Text(text = name)
},
supportingContent = {
Text(text = description)
},
trailingContent = {
NiaIconToggleButton(
checked = following,
onCheckedChange = onFollowButtonClick,
icon = {
Icon(
imageVector = NiaIcons.Add,
contentDescription = stringResource(
id = string.core_ui_interests_card_follow_button_content_desc,
),
)
},
checkedIcon = {
Icon(
imageVector = NiaIcons.Check,
contentDescription = stringResource(
id = string.core_ui_interests_card_unfollow_button_content_desc,
),
)
},
)
},
colors = ListItemDefaults.colors(
containerColor = if (isSelected) {
MaterialTheme.colorScheme.surfaceVariant
} else {
Color.Transparent
},
),
modifier = modifier
.semantics(mergeDescendants = true) {
selected = isSelected
}
.clickable(enabled = true, onClick = onClick),
)
}
@Composable
private fun InterestsIcon(topicImageUrl: String, modifier: Modifier = Modifier) {
if (topicImageUrl.isEmpty()) {
Icon(
modifier = modifier
.background(MaterialTheme.colorScheme.surface)
.padding(4.dp),
imageVector = NiaIcons.Person,
// decorative image
contentDescription = null,
)
} else {
DynamicAsyncImage(
imageUrl = topicImageUrl,
contentDescription = null,
modifier = modifier,
)
}
}
@Preview
@Composable
private fun InterestsCardPreview() {
NiaTheme {
Surface {
InterestsItem(
name = "Compose",
description = "Description",
following = false,
topicImageUrl = "",
onClick = { },
onFollowButtonClick = { },
)
}
}
}
@Preview
@Composable
private fun InterestsCardLongNamePreview() {
NiaTheme {
Surface {
InterestsItem(
name = "This is a very very very very long name",
description = "Description",
following = true,
topicImageUrl = "",
onClick = { },
onFollowButtonClick = { },
)
}
}
}
@Preview
@Composable
private fun InterestsCardLongDescriptionPreview() {
NiaTheme {
Surface {
InterestsItem(
name = "Compose",
description = "This is a very very very very very very very " +
"very very very long description",
following = false,
topicImageUrl = "",
onClick = { },
onFollowButtonClick = { },
)
}
}
}
@Preview
@Composable
private fun InterestsCardWithEmptyDescriptionPreview() {
NiaTheme {
Surface {
InterestsItem(
name = "Compose",
description = "",
following = true,
topicImageUrl = "",
onClick = { },
onFollowButtonClick = { },
)
}
}
}
@Preview
@Composable
private fun InterestsCardSelectedPreview() {
NiaTheme {
Surface {
InterestsItem(
name = "Compose",
description = "",
following = true,
topicImageUrl = "",
onClick = { },
onFollowButtonClick = { },
isSelected = true,
)
}
}
}
Loading…
Cancel
Save