Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions android_kmp/craftd-compose/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,7 @@ plugins {
}

kotlin {
androidTarget {
publishLibraryVariants("release", "debug")
}
androidTarget { publishLibraryVariants("release", "debug") }
sourceSets {
androidMain.dependencies {
implementation(libs.androidx.core)
Expand All @@ -24,4 +22,6 @@ kotlin {
implementation(libs.kotlinx.collections.immutable)
}
}
}
}


Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.github.codandotv.craftd.compose.ui.image

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import coil3.compose.AsyncImage
import com.github.codandotv.craftd.androidcore.data.model.image.ImageProperties
import com.github.codandotv.craftd.compose.extensions.toArrangementCompose

/**
* CraftDImage composable for rendering images using Coil 3.
*
* Supports both local and network images via Coil's AsyncImage.
*/
@Composable
fun CraftDImage(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking if we need this component here, considering a new one was created in our commonMain source set. Any thoughts?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending on the library that you will use, you can have different features os specs per platform or custom extensions.

imageProperties: ImageProperties,
modifier: Modifier = Modifier,
clickable: (() -> Unit)? = null,
) {
val modifierCustom = clickable?.let { Modifier.clickable { clickable.invoke() } } ?: modifier

Row(
horizontalArrangement = imageProperties.align.toArrangementCompose(),
modifier = Modifier.fillMaxWidth()
) {
AsyncImage(
model = imageProperties.url,
contentDescription = imageProperties.contentDescription,
modifier =
modifierCustom
.then(
if (imageProperties.fillMaxSize == true) {
Modifier.fillMaxSize()
} else {
Modifier
}
)
.then(
imageProperties.aspectRatio?.let { ratio ->
Modifier.aspectRatio(ratio)
}
?: Modifier
),
contentScale = imageProperties.contentScale?.toContentScale() ?: ContentScale.Fit
)
}
}

private fun String.toContentScale(): ContentScale =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Niceeee!! 👏🏼

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#suggestion
We could add some unit tests for this toContentScale. I think it would be awesome! For sure, for that we need to make it internal

when (this.lowercase()) {
"crop" -> ContentScale.Crop
"fit" -> ContentScale.Fit
"fillbounds" -> ContentScale.FillBounds
"fillwidth" -> ContentScale.FillWidth
"fillheight" -> ContentScale.FillHeight
"inside" -> ContentScale.Inside
"none" -> ContentScale.None
else -> ContentScale.Fit
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.github.codandotv.craftd.compose.ui.image

import androidx.compose.runtime.Composable
import com.github.codandotv.craftd.androidcore.data.convertToElement
import com.github.codandotv.craftd.androidcore.data.model.base.SimpleProperties
import com.github.codandotv.craftd.androidcore.data.model.image.ImageProperties
import com.github.codandotv.craftd.androidcore.presentation.CraftDComponentKey
import com.github.codandotv.craftd.androidcore.presentation.CraftDViewListener
import com.github.codandotv.craftd.compose.builder.CraftDBuilder

class CraftDImageBuilder(override val key: String = CraftDComponentKey.IMAGE_COMPONENT.key) :
CraftDBuilder {
@Composable
override fun craft(model: SimpleProperties, listener: CraftDViewListener) {
val imageProperties = model.value.convertToElement<ImageProperties>()
imageProperties?.let {
CraftDImage(it) { imageProperties.actionProperties?.let { listener.invoke(it) } }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.github.codandotv.craftd.compose.ui.image

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import coil3.compose.AsyncImage
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#discussion

Hey folks! I’ve been thinking about something: imagine someone adds Craftd to a project that’s already using a very recent version of Coil—or even another image-loading library.

Wouldn’t it make sense for us to introduce an abstraction layer so developers can plug in whichever image loader they prefer? This way, Craftd wouldn’t need updates every time a new Coil 3 release comes out, and teams would have more flexibility overall.

What do you think? Does this direction make sense to you?

Cc: @jacksonfdam, @rviannaoliveira

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kind of agree, but actually, just Coil is 100% compatible with Multiplatform

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the problem that this lib has support to android 100% too.

I agree with moro, i was thinking

I was thinking, I've had to do similar things using those server-driven UIs I made.

If you create a constructor in the Builder with a function that propagates to the component, like:

class CraftDImageBuilder(override val key: String = CraftDComponentKey.IMAGE_COMPONENT.key, loadImage : (String) -> Loader( algo que seja comum entre as libs teria que ver cada para pensar o objetvo certo aqui)) :
        CraftDBuilder {
    @Composable
    override fun craft(model: SimpleProperties, listener: CraftDViewListener) {
        val imageProperties = model.value.convertToElement<ImageProperties>()
        imageProperties?.let {
            CraftDImage(it, loadImage) { imageProperties.actionProperties?.let { listener.invoke(it) } }
        }
    }
}

that accepts any kind of "result" from a coil or Picasso or something like that that the person implements, then when registering the components in the CraftedManager, the person could pass an implementation block using

and in the component there would be something inside the ImageView block, a loadImage.invoke(url) in Imageview

and when you register the component you can do this

val craftdBuilderManager = remember {
        CraftDBuilderManager().add(
            MySampleButtonComposeBuilder(),
CraftdIMageBuilder(){ url ->

//call your frameworker and return
}
        )
    }
    LaunchedEffect(Unit) {
        vm.loadProperties()
    }

    CraftDynamic(
        properties = properties,
        craftDBuilderManager = craftdBuilderManager
    ) {
        println(
            ">>>> category ${it.analytics?.category} -" + " action ${it.analytics?.action} -" + " label  ${it.analytics?.label} -" + " deeplink ${it.deeplink}"
        )
    }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inside from craftdManager will be override de current CraftdImageBuilder default to new instancia

    fun add(vararg arrayCraftDBuilder: CraftDBuilder) : CraftDBuilderManager{
        arrayCraftDBuilder.forEach {
            mapBuilder[it.key] = it
        }
        return this
    }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

another thing

This PR you miss to register in CraftdManager your builder

CraftDBuilderManager

@Stable
@Immutable
class CraftDBuilderManager {
    private val mapBuilder = hashMapOf(
        CraftDComponentKey.BUTTON_COMPONENT.key to CraftDButtonBuilder(),
        CraftDComponentKey.TEXT_VIEW_COMPONENT.key to CraftDTextBuilder(),
        CraftDComponentKey.CHECK_BOX_COMPONENT.key to CraftDCheckBoxBuilder(),
        CraftDComponentKey.IMAGE_COMPONENT.key to CraftDImageBuilder(),
    )

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking... what I showed you is a good approach that works for various situations, like loading an image into an ImageView. This will be quite common when thinking about components, such as having more complex components using component composition, and you can also have an ImageView.

In this case, I think it's better to create an additional parameter in the CraftManager constructor, and then implement it within the builder (still keeping the builder as I mentioned).

like:

Stable
@Immutable
class CraftDBuilderManager (private val imageLoader : (String) -> Loader = {}){
    private val mapBuilder = hashMapOf(
        CraftDComponentKey.BUTTON_COMPONENT.key to CraftDButtonBuilder(),
        CraftDComponentKey.TEXT_VIEW_COMPONENT.key to CraftDTextBuilder(),
        CraftDComponentKey.CHECK_BOX_COMPONENT.key to CraftDCheckBoxBuilder(),
        CraftDComponentKey.IMAGE_COMPONENT.key to CraftDImageBuilder(imageLoader),
    )

import com.github.codandotv.craftd.androidcore.data.model.image.ImageProperties
import com.github.codandotv.craftd.compose.extensions.toArrangementCompose

/**
* CraftDImage composable for rendering images using Coil 3.
*
* Supports both local and network images via Coil's AsyncImage.
*/
@Composable
fun CraftDImage(
imageProperties: ImageProperties,
modifier: Modifier = Modifier,
clickable: (() -> Unit)? = null,
) {
val modifierCustom = clickable?.let { Modifier.clickable { clickable.invoke() } } ?: modifier

Row(
horizontalArrangement = imageProperties.align.toArrangementCompose(),
modifier = Modifier.fillMaxWidth()
) {
AsyncImage(
model = imageProperties.url,
contentDescription = imageProperties.contentDescription,
modifier =
modifierCustom
.then(
if (imageProperties.fillMaxSize == true) {
Modifier.fillMaxSize()
} else {
Modifier
}
)
.then(
imageProperties.aspectRatio?.let { ratio ->
Modifier.aspectRatio(ratio)
}
?: Modifier
),
contentScale = imageProperties.contentScale?.toContentScale() ?: ContentScale.Fit
)
}
}

private fun String.toContentScale(): ContentScale =
when (this.lowercase()) {
"crop" -> ContentScale.Crop
"fit" -> ContentScale.Fit
"fillbounds" -> ContentScale.FillBounds
"fillwidth" -> ContentScale.FillWidth
"fillheight" -> ContentScale.FillHeight
"inside" -> ContentScale.Inside
"none" -> ContentScale.None
else -> ContentScale.Fit
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.github.codandotv.craftd.compose.ui.image

import androidx.compose.runtime.Composable
import com.github.codandotv.craftd.androidcore.data.convertToElement
import com.github.codandotv.craftd.androidcore.data.model.base.SimpleProperties
import com.github.codandotv.craftd.androidcore.data.model.image.ImageProperties
import com.github.codandotv.craftd.androidcore.presentation.CraftDComponentKey
import com.github.codandotv.craftd.androidcore.presentation.CraftDViewListener
import com.github.codandotv.craftd.compose.builder.CraftDBuilder

class CraftDImageBuilder(override val key: String = CraftDComponentKey.IMAGE_COMPONENT.key) :
CraftDBuilder {
@Composable
override fun craft(model: SimpleProperties, listener: CraftDViewListener) {
val imageProperties = model.value.convertToElement<ImageProperties>()
imageProperties?.let {
CraftDImage(it) { imageProperties.actionProperties?.let { listener.invoke(it) } }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.github.codandotv.craftd.androidcore.data.model.image

import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import com.github.codandotv.craftd.androidcore.data.model.action.ActionProperties
import com.github.codandotv.craftd.androidcore.domain.CraftDAlign
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
@Immutable
@Stable
data class ImageProperties(
@SerialName("url") val url: String? = null,
@SerialName("contentDescription") val contentDescription: String? = null,
@SerialName("align") val align: CraftDAlign? = null,
@SerialName("fillMaxSize") val fillMaxSize: Boolean? = false,
@SerialName("aspectRatio") val aspectRatio: Float? = null,
@SerialName("contentScale") val contentScale: String? = null,
@SerialName("actionProperties") var actionProperties: ActionProperties? = null,
)
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package com.github.codandotv.craftd.androidcore.presentation


enum class CraftDComponentKey(val key: String) {
TEXT_VIEW_COMPONENT("${CRAFT_D}TextView"),
BUTTON_COMPONENT("${CRAFT_D}Button"),
CHECK_BOX_COMPONENT("${CRAFT_D}CheckBox"),
IMAGE_COMPONENT("${CRAFT_D}Image"),
}

internal const val CRAFT_D = "CraftD"
internal const val CRAFT_D = "CraftD"
8 changes: 2 additions & 6 deletions android_kmp/craftd-xml/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
android{
buildFeatures{
viewBinding = true
}
}
android { buildFeatures { viewBinding = true } }

plugins {
id("com.codandotv.android-library")
Expand All @@ -14,4 +10,4 @@ dependencies {
implementation(libs.androidx.core)
implementation(libs.androidx.appcompat)
implementation(libs.google.material)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package com.github.codandotv.craftd.xml.ui.image

import android.content.Context
import android.util.AttributeSet
import android.view.LayoutInflater
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.RelativeLayout
import coil3.load
import com.github.codandotv.craftd.androidcore.data.model.image.ImageProperties
import com.github.codandotv.craftd.androidcore.domain.CraftDAlign
import com.github.codandotv.craftd.xml.databinding.ImageBinding

/**
* CraftDImageComponent for XML-based image rendering using Coil 3.
*
* Supports both local and network images via Coil's load extension.
*/
class CraftDImageComponent
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is nice, we have it in xml too 🎉

@JvmOverloads
constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0,
) : RelativeLayout(context, attrs, defStyleAttr) {
private var binding: ImageBinding

init {
binding = ImageBinding.inflate(LayoutInflater.from(context), this)
}

fun setProperties(imageProperties: ImageProperties) {
// Load image using Coil
imageProperties.url?.let { url -> binding.imageView.load(url) }

imageProperties.contentDescription?.let { description ->
binding.imageView.contentDescription = description
}

setupFillMaxSize(imageProperties)

imageProperties.aspectRatio?.let { ratio ->
binding.imageView.adjustViewBounds = true
// AspectRatio can be handled via custom logic or ConstraintLayout
}

imageProperties.contentScale?.let { scale ->
binding.imageView.scaleType = scale.toScaleType()
}

imageProperties.align?.let {
binding.imageView.layoutParams =
LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
.apply { addRule(it.toRelativeLayoutParams()) }
}
}

private fun setupFillMaxSize(imageProperties: ImageProperties) {
binding.imageView.layoutParams =
imageProperties.fillMaxSize?.let { isFillMaxSize ->
LayoutParams(
if (isFillMaxSize) {
ViewGroup.LayoutParams.MATCH_PARENT
} else {
ViewGroup.LayoutParams.WRAP_CONTENT
},
ViewGroup.LayoutParams.MATCH_PARENT
)
}
?: LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
)
}

private fun CraftDAlign?.toRelativeLayoutParams(): Int =
when (this) {
CraftDAlign.CENTER -> CENTER_IN_PARENT
CraftDAlign.RIGHT -> ALIGN_PARENT_END
else -> ALIGN_PARENT_START
}

private fun String.toScaleType(): ImageView.ScaleType =
when (this.lowercase()) {
"crop" -> ImageView.ScaleType.CENTER_CROP
"fit" -> ImageView.ScaleType.FIT_CENTER
"fillbounds" -> ImageView.ScaleType.FIT_XY
"fillwidth" -> ImageView.ScaleType.FIT_START
"fillheight" -> ImageView.ScaleType.FIT_END
"inside" -> ImageView.ScaleType.CENTER_INSIDE
"none" -> ImageView.ScaleType.CENTER
else -> ImageView.ScaleType.FIT_CENTER
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.github.codandotv.craftd.xml.ui.image

import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.github.codandotv.craftd.androidcore.data.convertToElement
import com.github.codandotv.craftd.androidcore.data.model.base.SimpleProperties
import com.github.codandotv.craftd.androidcore.data.model.image.ImageProperties
import com.github.codandotv.craftd.androidcore.presentation.CraftDComponentKey
import com.github.codandotv.craftd.androidcore.presentation.CraftDViewListener
import com.github.codandotv.craftd.xml.ui.CraftDViewRenderer

class ImageComponentRender(override var onClickListener: CraftDViewListener?) :
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you need to register here too

object CraftDBuilderManager {
    fun getBuilderRenders(
        simpleProperties: List<SimpleProperties>,
        customDynamicBuilderList: List<CraftDViewRenderer<*>> = emptyList(),
        onAction: CraftDViewListener
    ): List<CraftDViewRenderer<*>> {
        val allViewRenders = (customDynamicBuilderList + listOf(
            CraftDTextViewComponentRender(onAction),
            ButtonComponentRender(onAction),
            ImageComponentRender(onAction)

        ))

CraftDViewRenderer<ImageComponentRender.ImageHolder>(
CraftDComponentKey.IMAGE_COMPONENT.key,
CraftDComponentKey.IMAGE_COMPONENT.ordinal
) {

inner class ImageHolder(val image: CraftDImageComponent) : RecyclerView.ViewHolder(image)

override fun bindView(model: SimpleProperties, holder: ImageHolder, position: Int) {
val imageProperties = model.value.convertToElement<ImageProperties>()

imageProperties?.let { holder.image.setProperties(it) }
imageProperties?.actionProperties?.let { actionProperties ->
holder.image.setOnClickListener { onClickListener?.invoke(actionProperties) }
}
}

override fun createViewHolder(parent: ViewGroup): ImageHolder {
return ImageHolder(CraftDImageComponent(parent.context))
}
}
16 changes: 16 additions & 0 deletions android_kmp/craftd-xml/src/main/res/layout/image.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>

<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="true"
tools:parentTag="android.widget.RelativeLayout">

<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@null"
tools:src="@tools:sample/backgrounds/scenic" />
</merge>
Loading