diff --git a/core/test/src/main/AndroidManifest.xml b/core/test/src/main/AndroidManifest.xml index cc58fed..9a40236 100644 --- a/core/test/src/main/AndroidManifest.xml +++ b/core/test/src/main/AndroidManifest.xml @@ -1,15 +1,3 @@ - - - - - - - - - - diff --git a/core/ui/src/androidTest/java/co/yml/coreui/ui/ytag/TagViewContainerTesting.kt b/core/ui/src/androidTest/java/co/yml/coreui/ui/ytag/TagViewContainerTesting.kt new file mode 100644 index 0000000..96fcf88 --- /dev/null +++ b/core/ui/src/androidTest/java/co/yml/coreui/ui/ytag/TagViewContainerTesting.kt @@ -0,0 +1,102 @@ +package co.yml.coreui.ui.ytag + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.test.* +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.unit.dp +import co.yml.coreui.core.ui.ytag.TagViewContainer +import co.yml.coreui.core.ui.ytag.model.TagViewContainerModifiers +import co.yml.coreui.core.ui.ytag.model.TagViewData +import co.yml.coreui.core.ui.ytag.model.TagViewModifiers +import org.junit.Rule +import org.junit.Test + +class TagViewContainerTesting { + @get:Rule + val composeTestRule = createComposeRule() + + private fun launchTagViewContainer( + tagViewContainerModifiers: TagViewContainerModifiers = TagViewContainerModifiers.Builder() + .shape(RoundedCornerShape(10.dp)) + .width(200.dp) + .height(120.dp) + .build() + ) { + val tagViewModifiers = TagViewModifiers.Builder() + .shape(CircleShape) + .backgroundColor(Color.Black) + .textColor(Color.White) + .build() + + val tagViewData = listOf( + TagViewData(text = "Tag 1", tagViewModifiers = tagViewModifiers), + TagViewData(text = "Tag 2", tagViewModifiers = tagViewModifiers), + TagViewData(text = "Tag 3", tagViewModifiers = tagViewModifiers), + TagViewData(text = "Tag 4", tagViewModifiers = tagViewModifiers) + ) + + composeTestRule.setContent { + TagViewContainer( + tagViewData = tagViewData, + tagViewContainerModifiers = tagViewContainerModifiers + ) + } + } + + @Test + fun tagViewContainer_shown() { + launchTagViewContainer() + + println( + "tag_view_container ${composeTestRule.onNodeWithTag("tag_view_container", useUnmergedTree = true).printToString()}" + ) + + composeTestRule.onNodeWithTag("tag_view_container").assertIsDisplayed() + } + + @Test + fun tagViewContainer_with_modifiers_are_executed() { + val tagViewContainerModifiers = TagViewContainerModifiers.Builder() + .minWidth(150.dp) + .minHeight(150.dp) + .width(200.dp) + .height(150.dp) + .enableBorder(true) + .borderWidth(1.dp) + .borderColor(Color.Red) + .backgroundColor(Color.Gray) + .shape(CircleShape) + .containerPaddingValues(PaddingValues(8.dp)) + .tagSpacingHorizontal(8.dp) + .tagSpacingVertical(8.dp) + .moreTagConfiguration(TagViewData(text = "more")) + .build() + + launchTagViewContainer(tagViewContainerModifiers) + + composeTestRule.onNodeWithTag("tag_view_container") + .assertIsDisplayed() + } + + @Test + fun tagViewContainer_tags_shown(){ + launchTagViewContainer() + + composeTestRule.onNodeWithText("Tag 1").assertIsDisplayed() + } + + @Test + fun tagViewContainer_with_less_space_more_tag_shown(){ + val tagViewContainerModifiers = TagViewContainerModifiers.Builder() + .width(150.dp) + .height(50.dp) + .build() + + launchTagViewContainer(tagViewContainerModifiers) + + composeTestRule.onNodeWithText("more").assertIsDisplayed() + } +} diff --git a/core/ui/src/main/AndroidManifest.xml b/core/ui/src/main/AndroidManifest.xml index a5b3cd9..7bc4648 100644 --- a/core/ui/src/main/AndroidManifest.xml +++ b/core/ui/src/main/AndroidManifest.xml @@ -3,4 +3,4 @@ - \ No newline at end of file + diff --git a/core/ui/src/main/java/co/yml/coreui/core/ui/ytag/TagView.kt b/core/ui/src/main/java/co/yml/coreui/core/ui/ytag/TagView.kt index 7462276..9f72f73 100644 --- a/core/ui/src/main/java/co/yml/coreui/core/ui/ytag/TagView.kt +++ b/core/ui/src/main/java/co/yml/coreui/core/ui/ytag/TagView.kt @@ -7,9 +7,11 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.* +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape @@ -21,6 +23,8 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension import co.yml.coreui.core.ui.ytag.model.TagViewModifiers /** @@ -38,7 +42,9 @@ fun TagView( leadingIcon: @Composable ((enable: Boolean) -> Unit)? = null, trailingIcon: @Composable ((enable: Boolean) -> Unit)? = null, enabled: Boolean = true, - tagViewModifiers: TagViewModifiers = TagViewModifiers.Builder().build() + tagViewModifiers: TagViewModifiers = TagViewModifiers.Builder().build(), + overFlowText: String = "", + onClick: () -> Unit = {} ) { with(tagViewModifiers) { Surface( @@ -50,8 +56,10 @@ fun TagView( .width(width = width ?: Dp.Unspecified) .height(height = height) ) { - Row( + ConstraintLayout( modifier = Modifier + .width(width = width ?: Dp.Unspecified) + .height(height) .run { if (enableBorder) { border( @@ -66,6 +74,7 @@ fun TagView( .clickable { if (enabled) { onClick.invoke() + tagViewModifiers.onClick.invoke() } } .defaultMinSize(minWidth = minWidth, minHeight = minHeight) @@ -73,13 +82,21 @@ fun TagView( .background( color = backgroundColor, shape = shape - ), - verticalAlignment = Alignment.CenterVertically + ) ) { - leadingIcon?.invoke(enabled) + val (leading_icon, text_view, trailing_icon) = createRefs() + + Box(modifier = Modifier.constrainAs(leading_icon) { + start.linkTo(parent.start) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + } + ) { + leadingIcon?.invoke(enabled) + } Text( - text = text, + text = overFlowText.ifEmpty { text }, color = textColor, fontSize = fontSize, fontWeight = fontWeight, @@ -87,12 +104,18 @@ fun TagView( fontStyle = fontStyle, letterSpacing = letterSpacing, modifier = Modifier + .constrainAs(text_view) { + start.linkTo(leading_icon.end) + end.linkTo(trailing_icon.start) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + width = Dimension.fillToConstraints + } .padding( textPadding ) - .align(Alignment.CenterVertically) .semantics { - this.contentDescription = text + this.contentDescription = semantics }, style = style, textDecoration = textDecoration, @@ -103,7 +126,15 @@ fun TagView( maxLines = maxLines, onTextLayout = onTextLayout ) - trailingIcon?.invoke(enabled) + Box(modifier = Modifier.constrainAs(trailing_icon) { + end.linkTo(parent.end) + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + } + ) { + trailingIcon?.invoke(enabled) + } + } } } diff --git a/core/ui/src/main/java/co/yml/coreui/core/ui/ytag/TagViewContainer.kt b/core/ui/src/main/java/co/yml/coreui/core/ui/ytag/TagViewContainer.kt new file mode 100644 index 0000000..7cebc6e --- /dev/null +++ b/core/ui/src/main/java/co/yml/coreui/core/ui/ytag/TagViewContainer.kt @@ -0,0 +1,385 @@ +package co.yml.coreui.core.ui.ytag + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.Placeable +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.* +import co.yml.coreui.core.ui.ytag.model.TagViewContainerModifiers +import co.yml.coreui.core.ui.ytag.model.TagViewData +import co.yml.coreui.core.ui.ytag.model.TagViewModifiers + +/** + * [TagViewContainer] compose method used for hosting multiple chips + * + * @param tagViewData Defines the list of tag view data + * @param tagViewContainerModifiers collection of modifier elements that decorate or add behavior to TagView elements + */ +@Composable +fun TagViewContainer( + tagViewData: List, + tagViewContainerModifiers: TagViewContainerModifiers +) { + //add overflow details tag into the list + val overFlowText = remember { + mutableStateOf("") + } + + val moreTag = tagViewContainerModifiers.moreTagConfiguration + val remainingTags: (Int) -> Unit = { count -> + overFlowText.value = moreTag.overFlowText.invoke(count) + } + + with(tagViewContainerModifiers) { + val context = LocalContext.current + var modifier = if (tagViewContainerModifiers.width != Dp.Unspecified) { + Modifier.width(tagViewContainerModifiers.width) + } else { + Modifier.wrapContentWidth() + } + + modifier = if (tagViewContainerModifiers.height != Dp.Unspecified) { + modifier.then(Modifier.height(tagViewContainerModifiers.height)) + } else { + modifier.then(Modifier.wrapContentHeight()) + } + + modifier = modifier.then(Modifier + .run { + if (enableBorder) { + border( + width = borderWidth, + color = borderColor, + shape = shape + ) + } else { + background(color = backgroundColor, shape = shape) + } + } + .defaultMinSize(minWidth, minHeight) + .clickable { } + .semantics { + this.contentDescription = + tagViewContainerModifiers.semantics.ifEmpty { context.getString(co.yml.coreui.ui.R.string.tag_view_container_accessibility_title) } + } + .testTag("tag_view_container") + .background( + color = backgroundColor, + shape = shape + ) + .padding(containerPaddingValues) + ) + + Box( + modifier = modifier + ) { + TagViewContainerLayout( + remainingTags = remainingTags, + tagViewContainerModifiers = tagViewContainerModifiers, + content = { + tagViewData.forEach { + with(it) { + val containerItemClick = { + tagViewContainerModifiers.onClick.invoke(it) + } + TagView( + text = text, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + enabled = enabled, + tagViewModifiers = tagViewModifiers, + overFlowText = "", + onClick = containerItemClick + ) + } + } + + //over flow item + with(moreTag) { + val containerItemClick = { + tagViewContainerModifiers.onClick.invoke(this) + } + TagView( + text = overFlowText.value, + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + enabled = enabled, + tagViewModifiers = tagViewModifiers, + overFlowText = "", + onClick = containerItemClick + ) + } + }) + } + } +} + +/** + * [TagViewContainerLayout] used for creating a custom layout to hosting y tag + * @param tagViewContainerModifiers collection of modifier elements that decorate or add behavior to tag view container + * @param content content of the tag view container + * @param remainingTags return item count which are not rendered in the tag view container + */ +@Composable +fun TagViewContainerLayout( + remainingTags: (Int) -> Unit, + tagViewContainerModifiers: TagViewContainerModifiers, + content: @Composable () -> Unit +) { + val localDensity = LocalDensity.current + + Layout(content = content) { measurables, constraints -> + val looseConstraints = constraints.copy( + minWidth = 0, + minHeight = 0 + ) + + var currentRow = 0 + var currentOffset = IntOffset.Zero + + //Measurement phase + val placeAbles = measurables.map { measurable -> + val placeAble: Placeable = measurable.measure(looseConstraints) + + //calculate the offsets to place the tags in layout phase + if (currentOffset.x > 0f && currentOffset.x + placeAble.width + tagViewContainerModifiers.tagSpacingHorizontal.toPx() + .toInt() > constraints.maxWidth + ) { + currentRow += 1 + currentOffset = + currentOffset.copy( + x = 0, + y = currentOffset.y + placeAble.height + tagViewContainerModifiers.tagSpacingVertical.toPx() + .toInt() + ) + } + placeAble to currentOffset.also { + currentOffset = it.copy( + x = it.x + placeAble.width + tagViewContainerModifiers.tagSpacingHorizontal.toPx() + .toInt() + ) + } + } + + layout( + width = constraints.maxWidth, + height = constraints.maxHeight + ) { + placeAbles.forEachIndexed { index, tagPlaceable -> + if (index != placeAbles.lastIndex) { + val (placeable, offset) = tagPlaceable + //check whether container has enough space to place the current tag + if (offset.x + placeable.width < constraints.maxWidth && offset.y + placeable.height < constraints.maxHeight) { + //space available for current tag + val nextItemIndex = index + 1 + //check whether container has enough space to place the next tag + if (nextItemIndex <= placeAbles.lastIndex) { + val nextItemOffset = placeAbles[nextItemIndex].second + if (nextItemOffset.x + placeAbles[nextItemIndex].first.width < constraints.maxWidth && nextItemOffset.y + placeAbles[nextItemIndex].first.height < constraints.maxHeight) { + //space available for next tag + placeable.place(offset.x, offset.y) + } else { + //space not available for next tag + //place the over flow tag + //check whether to accommodate current tag and more + val moreTagPlaceAble = placeAbles.last() + val moreXOffset = + offset.x + placeable.width + tagViewContainerModifiers.tagSpacingHorizontal.toPx() + .toInt() + val moreYOffset = offset.y + if (moreXOffset + moreTagPlaceAble.first.width < constraints.maxWidth && + moreYOffset + moreTagPlaceAble.first.height < constraints.maxHeight + ) { + //place current tag + placeable.place(offset.x, offset.y) + //place more tag + val remainingItems = placeAbles.lastIndex - 1 - index + remainingTags.invoke(remainingItems) + moreTagPlaceAble.first.place(moreXOffset, moreYOffset) + return@layout + } else { + val overflow = showOverFlow( + index, + placeAbles, + tagViewContainerModifiers, + constraints, + localDensity, + remainingTags + ) + overflow?.let { + it.first.place(it.second) + } + return@layout + } + } + } + } else { + //space not available for current tag + //place the over flow tag + val overflow = showOverFlow( + index, + placeAbles, + tagViewContainerModifiers, + constraints, + localDensity, + remainingTags + ) + overflow?.let { + it.first.place(it.second) + } + return@layout + } + } + } + } + } +} + +/** + * Used for displaying the over flow details when tags not fit in the container + * @param index current placeAble index + * @param placeAbles placeAble details of tags + * @param tagViewContainerModifiers collection of modifier elements that decorate or add behavior to tag view container + * @param constraints immutable constraints for measuring layouts + * @param localDensity A density of the screen. Used for the conversions between pixels and Dp + * @param remainingItems return item count which are not rendered in the tag view container + */ +fun showOverFlow( + index: Int, + placeAbles: List>, + tagViewContainerModifiers: TagViewContainerModifiers, + constraints: Constraints, + localDensity: Density, + remainingItems: (Int) -> Unit +): Pair? { + val offset = placeAbles[index].second + val placeable = placeAbles[index] + if (tagViewContainerModifiers.moreTagConfiguration.showOverFlow) { + val moreTagPlaceAble = placeAbles.last() + if (offset.x + moreTagPlaceAble.first.width < constraints.maxWidth && offset.y + moreTagPlaceAble.first.height < constraints.maxHeight) { + //place more tag + //check whether space available for over flow tag to place in between current [which replace over flow tag] and previous tags + val previousIndex = index - 1 + if (previousIndex >= 0) { + val previousOffset = placeAbles[previousIndex].second + val previousTag = placeAbles[previousIndex].first + + val moreTagXOffset = + previousOffset.x + localDensity.run { tagViewContainerModifiers.tagSpacingHorizontal.toPx() } + .toInt() + previousTag.width + val moreTagYOffset = previousOffset.y + + if (moreTagXOffset + moreTagPlaceAble.first.width < constraints.maxWidth && moreTagYOffset + moreTagPlaceAble.first.height < constraints.maxHeight) { + val remainingTags = placeAbles.lastIndex - index + remainingItems.invoke(remainingTags) + return Pair(moreTagPlaceAble.first, IntOffset(moreTagXOffset, moreTagYOffset)) + } + } + val remainingTags = placeAbles.lastIndex - index + remainingItems.invoke(remainingTags) + return Pair(moreTagPlaceAble.first, IntOffset(offset.x, offset.y)) + } + } else { + return placeable + } + + return null +} + +@Preview(name = "Default Tag container") +@Composable +fun DefaultTagContainer() { + val tagViewModifiers = TagViewModifiers.Builder() + .shape(CircleShape) + .backgroundColor(Color.Black) + .textColor(Color.White) + .build() + val tagViewData = listOf( + TagViewData(text = "Tag 1", tagViewModifiers = tagViewModifiers), + TagViewData(text = "Tag 2", tagViewModifiers = tagViewModifiers), + TagViewData(text = "Tag 3", tagViewModifiers = tagViewModifiers), + TagViewData(text = "Tag 4", tagViewModifiers = tagViewModifiers) + ) + + val tagViewContainerModifiers = TagViewContainerModifiers.Builder() + .shape(RoundedCornerShape(10.dp)) + .width(200.dp) + .height(120.dp) + .build() + + TagViewContainer( + tagViewData = tagViewData, + tagViewContainerModifiers = tagViewContainerModifiers + ) +} + +@Preview(name = "Tag container with border") +@Composable +fun BorderTagContainer() { + val tagViewModifiers = TagViewModifiers.Builder() + .shape(CircleShape) + .backgroundColor(Color.Black) + .textColor(Color.White) + .build() + val tagViewData = listOf( + TagViewData(text = "Tag 1", tagViewModifiers = tagViewModifiers), + TagViewData(text = "Tag 2", tagViewModifiers = tagViewModifiers), + TagViewData(text = "Tag 3", tagViewModifiers = tagViewModifiers), + TagViewData(text = "Tag 4", tagViewModifiers = tagViewModifiers) + ) + + val tagViewContainerModifiers = TagViewContainerModifiers.Builder() + .shape(RoundedCornerShape(10.dp)) + .width(200.dp) + .height(120.dp) + .enableBorder(true) + .borderColor(Color.Red) + .borderWidth(1.dp) + .build() + + TagViewContainer( + tagViewData = tagViewData, + tagViewContainerModifiers = tagViewContainerModifiers + ) +} + +@Preview(name = "Tag container with background") +@Composable +fun BackgroundTagContainer() { + val tagViewModifiers = TagViewModifiers.Builder() + .shape(CircleShape) + .backgroundColor(Color.Black) + .textColor(Color.White) + .build() + val tagViewData = listOf( + TagViewData(text = "Tag 1", tagViewModifiers = tagViewModifiers), + TagViewData(text = "Tag 2", tagViewModifiers = tagViewModifiers), + TagViewData(text = "Tag 3", tagViewModifiers = tagViewModifiers), + TagViewData(text = "Tag 4", tagViewModifiers = tagViewModifiers) + ) + + val tagViewContainerModifiers = TagViewContainerModifiers.Builder() + .shape(RoundedCornerShape(10.dp)) + .backgroundColor(Color.Gray) + .width(200.dp) + .height(120.dp) + .build() + + TagViewContainer( + tagViewData = tagViewData, + tagViewContainerModifiers = tagViewContainerModifiers + ) +} diff --git a/core/ui/src/main/java/co/yml/coreui/core/ui/ytag/model/TagViewContainerModifiers.kt b/core/ui/src/main/java/co/yml/coreui/core/ui/ytag/model/TagViewContainerModifiers.kt new file mode 100644 index 0000000..778ffb8 --- /dev/null +++ b/core/ui/src/main/java/co/yml/coreui/core/ui/ytag/model/TagViewContainerModifiers.kt @@ -0,0 +1,122 @@ +package co.yml.coreui.core.ui.ytag.model + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +//todo sree_ is min width and min height required? +/** + * [TagViewContainerModifiers] Immutable collection of modifier elements that decorate or add behavior to TagView container. + * @param minWidth define a default min width of TagViewContainer + * @param minHeight define a default min height of TagViewContainer + * @param width define width of TagViewContainer + * @param height define height of TagViewContainer + * @param enableBorder enable border for TagViewContainer + * @param borderWidth define borderWidth of the TagViewContainer + * @param borderColor define borderColor of the TagViewContainer + * @param backgroundColor define backgroundColor of the TagViewContainer + * @param shape defines the shape of the TagViewContainer + * @param containerPaddingValues define padding for TagViewContainer + * @param tagSpacingHorizontal horizontal padding between tag views + * @param tagSpacingVertical vertical padding between tag views + * @param semantics add content description for tag view container + */ +data class TagViewContainerModifiers( + val minWidth: Dp, + val minHeight: Dp, + val width: Dp, + val height: Dp, + val enableBorder: Boolean, + val borderWidth: Dp, + val borderColor: Color, + val backgroundColor: Color, + val shape: Shape, + val containerPaddingValues: PaddingValues, + val tagSpacingHorizontal: Dp, + val tagSpacingVertical: Dp, + val moreTagConfiguration: TagViewData, + val onClick: (TagViewData) -> Unit, + val semantics: String +) { + //todo sree_ check min and max default size + class Builder { + private var minWidth: Dp = 150.dp + private var minHeight: Dp = 150.dp + private var width: Dp = Dp.Unspecified + private var height: Dp = Dp.Unspecified + private var enableBorder: Boolean = false + private var borderWidth: Dp = 1.dp + private var borderColor: Color = Color.Black + private var backgroundColor: Color = Color.White + private var shape: Shape = RectangleShape + private var containerPaddingValues: PaddingValues = + PaddingValues(horizontal = 4.dp, vertical = 4.dp) + private var tagSpacingHorizontal: Dp = 8.dp + private var tagSpacingVertical: Dp = 8.dp + private var moreTagConfiguration: TagViewData = TagViewData( + text = "more", + tagViewModifiers = TagViewModifiers.Builder() + .shape(CircleShape) + .backgroundColor(Color.Black) + .textColor(Color.White) + .build() + ) + private var onClick: (TagViewData) -> Unit = {} + private var semantics: String = "" + + fun minWidth(minWidth: Dp) = apply { this.minWidth = minWidth } + + fun minHeight(minHeight: Dp) = apply { this.minHeight = minHeight } + + fun width(width: Dp) = apply { this.width = width } + + fun height(height: Dp) = apply { this.height = height } + + fun enableBorder(enableBorder: Boolean) = apply { this.enableBorder = enableBorder } + + fun borderWidth(borderWidth: Dp) = apply { this.borderWidth = borderWidth } + + fun borderColor(borderColor: Color) = apply { this.borderColor = borderColor } + + fun backgroundColor(backgroundColor: Color) = + apply { this.backgroundColor = backgroundColor } + + fun shape(shape: Shape) = apply { this.shape = shape } + + fun containerPaddingValues(paddingValues: PaddingValues) = + apply { this.containerPaddingValues = paddingValues } + + fun tagSpacingHorizontal(space: Dp) = apply { this.tagSpacingHorizontal = space } + + fun tagSpacingVertical(space: Dp) = apply { this.tagSpacingVertical = space } + + fun moreTagConfiguration(configuration: TagViewData) = + apply { this.moreTagConfiguration = configuration } + + fun onCLick(onClick: (TagViewData) -> Unit) = apply { this.onClick = onClick } + + fun semantics(semantics: String) = apply { this.semantics = semantics } + + fun build() = TagViewContainerModifiers( + minWidth, + minHeight, + width, + height, + enableBorder, + borderWidth, + borderColor, + backgroundColor, + shape, + containerPaddingValues, + tagSpacingHorizontal, + tagSpacingVertical, + moreTagConfiguration, + onClick, + semantics + ) + } +} diff --git a/core/ui/src/main/java/co/yml/coreui/core/ui/ytag/model/TagViewData.kt b/core/ui/src/main/java/co/yml/coreui/core/ui/ytag/model/TagViewData.kt new file mode 100644 index 0000000..e1953ae --- /dev/null +++ b/core/ui/src/main/java/co/yml/coreui/core/ui/ytag/model/TagViewData.kt @@ -0,0 +1,24 @@ +package co.yml.coreui.core.ui.ytag.model + +import androidx.compose.runtime.Composable + +/** + * [TagViewData] Used for holding the TagView data + * + * @param text Tag view text to be displayed + * @param leadingIcon the optional leading icon to be displayed at the beginning of the TagView + * @param trailingIcon the optional leading icon to be displayed at the end of the TagView + * @param enabled controls the enabled state of the TagView + * @param tagViewModifiers collection of modifier elements that decorate or add behavior to TagView elements + * @param showOverFlow show or hide over flow text + * @param overFlowText to be displayed for over flow tag [use overFlowText instead of [text] for over flow tag ] + */ +data class TagViewData( + val text: String = "", + val tagViewModifiers: TagViewModifiers = TagViewModifiers.Builder().build(), + val leadingIcon: @Composable ((enable: Boolean) -> Unit)? = null, + val trailingIcon: @Composable ((enable: Boolean) -> Unit)? = null, + val enabled: Boolean = true, + val showOverFlow: Boolean = true, + val overFlowText: (Int) -> String = { _ -> "" } +) diff --git a/core/ui/src/main/java/co/yml/coreui/core/ui/ytag/model/TagViewModifiers.kt b/core/ui/src/main/java/co/yml/coreui/core/ui/ytag/model/TagViewModifiers.kt index e252d3c..fa693ba 100644 --- a/core/ui/src/main/java/co/yml/coreui/core/ui/ytag/model/TagViewModifiers.kt +++ b/core/ui/src/main/java/co/yml/coreui/core/ui/ytag/model/TagViewModifiers.kt @@ -19,7 +19,7 @@ import androidx.compose.ui.unit.sp /** - * Immutable collection of modifier elements that decorate or add behavior to TagView elements. + * [TagViewModifiers] Represents immutable collection of modifier elements that decorate or add behavior to TagView elements. * - If a parameter is explicitly set here then that parameter will always be used. * - If a parameter is not set then the corresponding default value will be used * @@ -50,6 +50,7 @@ import androidx.compose.ui.unit.sp * @param shadowElevation The size of the shadow below the surface. * @param containerPaddingValues define padding for TagView * @param onClick perform click event + * @param semantics add content description for tag view */ data class TagViewModifiers( val minWidth: Dp, @@ -79,10 +80,11 @@ data class TagViewModifiers( val tonalElevation: Dp, val shadowElevation: Dp, val containerPaddingValues: PaddingValues, - val onClick: () -> Unit + val onClick: () -> Unit, + val semantics: String ) { class Builder { - private var minWidth: Dp = 52.dp + private var minWidth: Dp = 80.dp private var minHeight: Dp = 32.dp private var width: Dp? = null private var height: Dp = minHeight @@ -111,6 +113,7 @@ data class TagViewModifiers( private var shadowElevation: Dp = 0.dp private var containerPaddingValues: PaddingValues = PaddingValues(horizontal = 4.dp) private var onClick: () -> Unit = {} + private var semantics: String = text fun minWidth(minWidth: Dp) = apply { this.minWidth = minWidth } @@ -166,6 +169,8 @@ data class TagViewModifiers( apply { this.containerPaddingValues = paddingValues } fun onCLick(onClick: () -> Unit) = apply { this.onClick = onClick } + + fun semantics(semantics: String) = apply { this.semantics = semantics } fun build() = TagViewModifiers( minWidth, minHeight, @@ -194,7 +199,8 @@ data class TagViewModifiers( tonalElevation, shadowElevation, containerPaddingValues, - onClick + onClick, + semantics ) } } diff --git a/core/ui/src/main/res/values/colors.xml b/core/ui/src/main/res/values/colors.xml new file mode 100644 index 0000000..00a3be5 --- /dev/null +++ b/core/ui/src/main/res/values/colors.xml @@ -0,0 +1,13 @@ + + + #E0E0E0 + #0C0A3E + #C8FFBE + #EDFFAB + #F9564F + #89608E + #E1F5FE + #01579B + #0288D1 + + diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index c51354a..5c92fc8 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -2,4 +2,6 @@ CoreUICatalogApp Tags + more + Tag view container diff --git a/feature/ytag/src/main/java/co/yml/coreui/feature/ytag/ui/YTagActivity.kt b/feature/ytag/src/main/java/co/yml/coreui/feature/ytag/ui/YTagActivity.kt index e0d8626..6bf076c 100644 --- a/feature/ytag/src/main/java/co/yml/coreui/feature/ytag/ui/YTagActivity.kt +++ b/feature/ytag/src/main/java/co/yml/coreui/feature/ytag/ui/YTagActivity.kt @@ -1,5 +1,6 @@ package co.yml.coreui.feature.ytag.ui +import android.content.Context import android.os.Bundle import android.widget.Toast import androidx.activity.ComponentActivity @@ -26,13 +27,18 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import co.yml.coreui.core.ui.templates.AppBarWithBackButton import co.yml.coreui.core.ui.theme.CoreUICatalogTheme +import co.yml.coreui.core.ui.ytag.TagView +import co.yml.coreui.core.ui.ytag.TagViewContainer +import co.yml.coreui.core.ui.ytag.model.TagViewContainerModifiers +import co.yml.coreui.core.ui.ytag.model.TagViewData import co.yml.coreui.core.ui.ytag.model.TagViewModifiers import co.yml.coreui.ui.R -import co.yml.coreui.core.ui.ytag.TagView import dagger.hilt.android.AndroidEntryPoint @ExperimentalMaterial3Api @@ -54,14 +60,13 @@ class YTagActivity : ComponentActivity() { { Box( modifier = Modifier - .fillMaxWidth() + .fillMaxSize() .background(Color.White) - .padding(it), - contentAlignment = Alignment.TopCenter + .padding(it) ) { LazyColumn( content = { - items(9) { item -> + items(10) { item -> when (item) { 0 -> CapsuleTag() 1 -> RectangleTag() @@ -72,11 +77,14 @@ class YTagActivity : ComponentActivity() { 6 -> TagWithLeadingTrailingIcon() 7 -> BorderTag() 8 -> ShadowTag() + 9 -> DefaultTagViewContainer() } } }, verticalArrangement = Arrangement.spacedBy(dimensionResource(id = R.dimen.padding_normal)), - modifier = Modifier.padding(dimensionResource(id = R.dimen.padding_normal_medium)) + modifier = Modifier + .padding(dimensionResource(id = R.dimen.padding_normal_medium)) + .align(Alignment.Center) ) } } @@ -97,154 +105,93 @@ fun DefaultTag() { @Composable fun CapsuleTag() { - val tagViewModifiers = TagViewModifiers.Builder() - .shape(CircleShape) - .backgroundColor(Color.Black) - .textColor(Color.White) - .style(textStyle) - .build() + val context = LocalContext.current + val data = capsuleTagData( + context = context, + backgroundColor = colorResource(id = R.color.russian_violet) + ) TagView( - text = stringResource(id = co.yml.coreui.feature.ytag.R.string.tag_capsule), - tagViewModifiers = tagViewModifiers + text = data.text, + tagViewModifiers = data.tagViewModifiers ) } @Composable fun RectangleTag() { - val tagViewModifiers = TagViewModifiers.Builder() - .shape(RectangleShape) - .backgroundColor(Color.Black) - .textColor(Color.White) - .style(textStyle) - .build() + val context = LocalContext.current + val data = + rectangleTagData( + context = context, + backgroundColor = colorResource(id = R.color.light_green), + textColor = Color.Black + ) TagView( - text = stringResource(id = co.yml.coreui.feature.ytag.R.string.tag_rectangle), - tagViewModifiers = tagViewModifiers + text = data.text, + tagViewModifiers = data.tagViewModifiers ) } @Composable fun RoundRectangleTag() { - val tagViewModifiers = TagViewModifiers.Builder() - .shape(RoundedCornerShape(dimensionResource(id = R.dimen.padding_small))) - .backgroundColor(Color.Black) - .textColor(Color.White) - .style(textStyle) - .build() - + val context = LocalContext.current + val data = roundRectTagData( + context = context, + backgroundColor = colorResource(id = R.color.light_yellow), + textColor = Color.Black + ) TagView( - text = stringResource(id = co.yml.coreui.feature.ytag.R.string.tag_round_rectangle), - tagViewModifiers = tagViewModifiers + text = data.text, + tagViewModifiers = data.tagViewModifiers ) } @Composable fun TagWithLeadingIcon() { val context = LocalContext.current - val tagViewModifiers = TagViewModifiers.Builder() - .shape(CircleShape) - .backgroundColor(Color.Black) - .textColor(Color.White) - .fontStyle(FontStyle.Italic) - .build() - val text = stringResource(id = co.yml.coreui.feature.ytag.R.string.tag_leading_icon) - TagView(text = text, leadingIcon = { enabled -> - IconButton( - modifier = Modifier.size(dimensionResource(id = R.dimen.padding_normal_medium)), - onClick = { - if (enabled) { - Toast.makeText(context, text, Toast.LENGTH_SHORT).show() - } - }) { - Icon( - painter = painterResource(id = co.yml.coreui.feature.ytag.R.drawable.ic_location_24px), - contentDescription = null, - tint = Color.White - ) - } - }, tagViewModifiers = tagViewModifiers) + val data = leadingIconTagData( + context = context, + backgroundColor = colorResource(id = R.color.bitter_sweet), + textColor = Color.White + ) + TagView( + text = data.text, + leadingIcon = data.leadingIcon, + tagViewModifiers = data.tagViewModifiers + ) } @Composable fun TagWithTrailingIcon() { val context = LocalContext.current - val tagViewModifiers = TagViewModifiers.Builder() - .shape(CircleShape) - .backgroundColor(Color.Black) - .textColor(Color.White) - .fontSize(15.sp) - .build() - - val text = stringResource(id = co.yml.coreui.feature.ytag.R.string.tag_trailing_icon) - TagView(text = text, trailingIcon = { enabled -> - IconButton(modifier = Modifier - .padding(end = dimensionResource(id = R.dimen.padding_medium)) - .size(dimensionResource(id = R.dimen.padding_normal_medium)), onClick = { - if (enabled) { - Toast.makeText(context, text, Toast.LENGTH_SHORT).show() - } - }) { - Icon( - painter = painterResource(id = co.yml.coreui.feature.ytag.R.drawable.ic_close_20px), - contentDescription = null, - tint = Color.White - ) - } - }, tagViewModifiers = tagViewModifiers) + val data = trailingIconData( + context = context, + backgroundColor = colorResource(id = R.color.power), + textColor = Color.White + ) + TagView( + text = data.text, + trailingIcon = data.trailingIcon, + tagViewModifiers = data.tagViewModifiers + ) } @Composable fun TagWithLeadingTrailingIcon() { val context = LocalContext.current - val tagViewModifiers = TagViewModifiers.Builder() - .shape(CircleShape) - .backgroundColor(Color.Black) - .maxLines(1) - .overFlow(TextOverflow.Ellipsis) - .textColor(Color.White) - .onCLick { - - } - .build() + val data = leadingIconTrailingIconData( + context = context, + backgroundColor = Color.Black, + textColor = Color.White + ) TagView( - text = stringResource(id = co.yml.coreui.feature.ytag.R.string.tag_leading_trailing_icon), - leadingIcon = { enabled -> - val text = stringResource(id = co.yml.coreui.feature.ytag.R.string.tag_leading_icon) - IconButton( - modifier = Modifier.size(dimensionResource(id = R.dimen.padding_normal_medium)), - onClick = { - if (enabled) { - Toast.makeText(context, text, Toast.LENGTH_SHORT).show() - } - }) { - Icon( - painter = painterResource(id = co.yml.coreui.feature.ytag.R.drawable.ic_location_24px), - contentDescription = null, - tint = Color.White - ) - } - }, - trailingIcon = { enabled -> - val text = stringResource(id = co.yml.coreui.feature.ytag.R.string.tag_trailing_icon) - IconButton(modifier = Modifier - .padding(end = dimensionResource(id = R.dimen.padding_medium)) - .size(dimensionResource(id = R.dimen.padding_normal_small)), onClick = { - if (enabled) { - Toast.makeText(context, text, Toast.LENGTH_SHORT).show() - } - }) { - Icon( - painter = painterResource(id = co.yml.coreui.feature.ytag.R.drawable.ic_close_20px), - contentDescription = null, - tint = Color.White - ) - } - }, - tagViewModifiers = tagViewModifiers, + text = data.text, + leadingIcon = data.leadingIcon, + trailingIcon = data.trailingIcon, + tagViewModifiers = data.tagViewModifiers, enabled = false ) } @@ -252,6 +199,7 @@ fun TagWithLeadingTrailingIcon() { @Composable fun BorderTag() { val tagViewModifiers = TagViewModifiers.Builder() + .width(100.dp) .textColor(Color.Black) .enableBorder(true) .borderColor(Color.Red) @@ -270,6 +218,7 @@ fun BorderTag() { @Composable fun ShadowTag() { val tagViewModifiers = TagViewModifiers.Builder() + .width(100.dp) .textColor(colorResource(id = co.yml.coreui.feature.ytag.R.color.tag_text_color)) .backgroundColor(colorResource(id = co.yml.coreui.feature.ytag.R.color.tag_background_color)) .shape(CircleShape) @@ -284,3 +233,266 @@ fun ShadowTag() { tagViewModifiers = tagViewModifiers ) } + +@Composable +fun DefaultTagViewContainer() { + val context = LocalContext.current + + val tagViewData = listOf( + + capsuleTagData(context = context, backgroundColor = colorResource(id = R.color.cyan_900)), + + rectangleTagData( + context = context, + backgroundColor = colorResource(id = R.color.cyan_700), + textColor = Color.White + ), + + roundRectTagData( + context = context, + backgroundColor = colorResource(id = R.color.cyan_900), + textColor = Color.White + ), + + leadingIconTagData( + context = context, + backgroundColor = colorResource(id = R.color.cyan_700), + textColor = Color.White + ), + + trailingIconData( + context = context, + backgroundColor = colorResource(id = R.color.cyan_900), + textColor = Color.White + ), + + leadingIconTrailingIconData( + context = context, + backgroundColor = colorResource(id = R.color.cyan_700), + textColor = Color.White + ) + ) + + val tagViewContainerModifiers = TagViewContainerModifiers.Builder() + .containerPaddingValues(PaddingValues(8.dp)) + .enableBorder(true) + .shape(RoundedCornerShape(4.dp)) + .tagSpacingVertical(8.dp) + .tagSpacingHorizontal(8.dp) + .backgroundColor(colorResource(id = R.color.cyan_50)) + .width(260.dp) + .height(180.dp) + .moreTagConfiguration( + TagViewData( + overFlowText = { count -> + "$count more" + }, + tagViewModifiers = TagViewModifiers.Builder() + .backgroundColor(colorResource(id = R.color.cyan_50)) + .width(80.dp) + .textAlign(TextAlign.Start) + .height(30.dp) + .textColor(Color.Black) + .onCLick { }.build() + ) + ) + .onCLick { item -> + val itemIndex = tagViewData.indexOf(item) + val updatedList = tagViewData.toMutableList() + + if (itemIndex != -1) { + updatedList.removeAt(itemIndex) + } + } + .build() + + TagViewContainer( + tagViewData = tagViewData, + tagViewContainerModifiers = tagViewContainerModifiers + ) +} + +/** + * @param context current context + * @param backgroundColor tag view background color + */ +fun capsuleTagData(context: Context, backgroundColor: Color): TagViewData { + return TagViewData( + text = context.getString(co.yml.coreui.feature.ytag.R.string.tag_capsule), + tagViewModifiers = TagViewModifiers.Builder() + .shape(CircleShape) + .backgroundColor(backgroundColor) + .textColor(Color.White) + .style(textStyle) + .build() + ) +} + +/** + * @param context current context + * @param backgroundColor tag view background color + * @param textColor tag view text color + */ +fun rectangleTagData(context: Context, backgroundColor: Color, textColor: Color): TagViewData { + return TagViewData( + text = context.getString(co.yml.coreui.feature.ytag.R.string.tag_rectangle), + tagViewModifiers = TagViewModifiers.Builder() + .width(90.dp) + .shape(RectangleShape) + .backgroundColor(backgroundColor) + .textColor(textColor) + .style(textStyle) + .build() + ) +} + +/** + * @param context current context + * @param backgroundColor tag view background color + * @param textColor tag view text color + */ +fun roundRectTagData(context: Context, backgroundColor: Color, textColor: Color): TagViewData { + return TagViewData( + text = context.getString(co.yml.coreui.feature.ytag.R.string.tag_round_rectangle), + tagViewModifiers = TagViewModifiers.Builder() + .shape(RoundedCornerShape(context.resources.getDimension(R.dimen.padding_small))) + .width(120.dp) + .backgroundColor(backgroundColor) + .textColor(textColor) + .style(textStyle) + .build() + ) +} + +/** + * @param context current context + * @param backgroundColor tag view background color + * @param textColor tag view text color + */ +fun leadingIconTagData(context: Context, backgroundColor: Color, textColor: Color): TagViewData { + return TagViewData(text = context.getString(co.yml.coreui.feature.ytag.R.string.tag_leading_icon), + tagViewModifiers = TagViewModifiers.Builder() + .width(120.dp) + .maxLines(1) + .overFlow(TextOverflow.Ellipsis) + .shape(CircleShape) + .backgroundColor(backgroundColor) + .textColor(textColor) + .fontStyle(FontStyle.Italic) + .build(), + leadingIcon = { enabled -> + IconButton( + modifier = Modifier.size(dimensionResource(id = R.dimen.padding_normal_medium)), + onClick = { + if (enabled) { + Toast.makeText( + context, + context.getString(co.yml.coreui.feature.ytag.R.string.tag_leading_icon), + Toast.LENGTH_SHORT + ).show() + } + }) { + Icon( + painter = painterResource(id = co.yml.coreui.feature.ytag.R.drawable.ic_location_24px), + contentDescription = null, + tint = Color.White + ) + } + }) +} + +/** + * @param context current context + * @param backgroundColor tag view background color + * @param textColor tag view text color + */ +fun trailingIconData(context: Context, backgroundColor: Color, textColor: Color): TagViewData { + return TagViewData(text = context.getString(co.yml.coreui.feature.ytag.R.string.tag_trailing_icon), + tagViewModifiers = TagViewModifiers.Builder() + .width(150.dp) + .maxLines(1) + .textAlign(TextAlign.Start) + .overFlow(TextOverflow.Ellipsis) + .shape(CircleShape) + .backgroundColor(backgroundColor) + .textColor(textColor) + .fontSize(15.sp) + .build(), + trailingIcon = { enabled -> + IconButton(modifier = Modifier + .padding(end = dimensionResource(id = R.dimen.padding_medium)) + .size(dimensionResource(id = R.dimen.padding_normal_medium)), onClick = { + if (enabled) { + Toast.makeText( + context, + context.getString(co.yml.coreui.feature.ytag.R.string.tag_trailing_icon), + Toast.LENGTH_SHORT + ).show() + } + }) { + Icon( + painter = painterResource(id = co.yml.coreui.feature.ytag.R.drawable.ic_close_20px), + contentDescription = null, + tint = Color.White + ) + } + }) +} + +/** + * @param context current context + * @param backgroundColor tag view background color + * @param textColor tag view text color + */ +fun leadingIconTrailingIconData( + context: Context, + backgroundColor: Color, + textColor: Color +): TagViewData { + return TagViewData(text = context.getString(co.yml.coreui.feature.ytag.R.string.tag_leading_trailing_icon), + tagViewModifiers = TagViewModifiers.Builder() + .width(140.dp) + .maxLines(1) + .overFlow(TextOverflow.Ellipsis) + .shape(CircleShape) + .backgroundColor(backgroundColor) + .maxLines(1) + .overFlow(TextOverflow.Ellipsis) + .textColor(textColor) + .onCLick { + } + .build(), + leadingIcon = { enabled -> + val text = stringResource(id = co.yml.coreui.feature.ytag.R.string.tag_leading_icon) + IconButton( + modifier = Modifier.size(dimensionResource(id = R.dimen.padding_normal_medium)), + onClick = { + if (enabled) { + Toast.makeText(context, text, Toast.LENGTH_SHORT).show() + } + }) { + Icon( + painter = painterResource(id = co.yml.coreui.feature.ytag.R.drawable.ic_location_24px), + contentDescription = null, + tint = Color.White + ) + } + }, + trailingIcon = { enabled -> + val text = + stringResource(id = co.yml.coreui.feature.ytag.R.string.tag_trailing_icon) + IconButton(modifier = Modifier + .padding(end = dimensionResource(id = R.dimen.padding_medium)) + .size(dimensionResource(id = R.dimen.padding_normal_small)), onClick = { + if (enabled) { + Toast.makeText(context, text, Toast.LENGTH_SHORT).show() + } + }) { + Icon( + painter = painterResource(id = co.yml.coreui.feature.ytag.R.drawable.ic_close_20px), + contentDescription = null, + tint = Color.White + ) + } + }) +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e673153..505c3ca 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -23,6 +23,7 @@ androidxTestRunner = "1.5.1" androidxTestMonitor = "1.6.0" androidxTestCore = "1.4.0" androidxTestExt = "1.1.4" +constraintlayout = "1.0.1" #hilt hilt = "2.44" @@ -94,6 +95,7 @@ androidx-navigation-compose = { group = "androidx.navigation", name = "navigatio androidx-lifecycle-viewModelCompose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" } androidx-lifecycle-viewModel-ktx = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-ktx", version.ref = "androidxLifecycle" } androidx-navigation-testing = { group = "androidx.navigation", name = "navigation-testing", version.ref = "androidxNavigation" } +androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout-compose", version.ref = "constraintlayout" } #hilt hilt_compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "hilt" } @@ -130,7 +132,7 @@ test_runner = { module = "androidx.test:runner", version.ref = "test_runner" } [bundles] # Define bundles/groups of libraries -compose = ["androidx.activity.compose", "androidx.compose.foundation", "androidx.navigation.compose", "androidx.compose.material3", "androidx.compose.material3.windowSizeClass", "androidx.compose.runtime","androidx.compose.ui.tooling","androidx.compose.ui.tooling.preview"] +compose = ["androidx.activity.compose", "androidx.compose.foundation", "androidx.navigation.compose", "androidx.compose.material3", "androidx.compose.material3.windowSizeClass", "androidx.compose.runtime","androidx.compose.ui.tooling","androidx.compose.ui.tooling.preview", "androidx.constraintlayout"] test = ["androidx.test.core","androidx.test.ext","androidx.test.rules","androidx.test.runner", "compose_ui_testing"] coroutine_test = ["coroutine.test", "coroutine.turbine"] hilt = ["hilt.compiler","hilt.android","hilt.test"]