Hi, I need an advice again. the code below hide ui...
# compose
h
Hi, I need an advice again. the code below hide ui if shown == false
Copy code
if (shown) {
    Column {
        Text("Success")
    }
}
but, I would like to use the way below similar to data binding.(
android:visiblity="@{model.shown}"
) as it reduce indent depth, so, seems easy to read UI code. and feel better to separate ui and logic.
Copy code
Column(Modifier.visible(shown)) {
    Text("Success")
}
I tried to customize Modifier. but, it looks difficult. is there any possible solution? or is it not recommended approach?
t
You could use the
Copy code
Modifier.drawOpacity()
But i think the child is just invisible when you set it to 0
I also developed a modifier which can collapse / expand
Copy code
@Composable
fun Modifier.expandable(
    id: Any?,
    collapsed: CollapseState,
    onChanged: (CollapseState) -> Unit = {},
    duration: Int = 500
) = collapseable(id, collapsed, onChanged, duration, CollapseState.COLLAPSED)

@Composable
fun Modifier.collapseable(
    id: Any?,
    collapsed: CollapseState,
    onChanged: (CollapseState) -> Unit = {},
    duration: Int = 500,
    initialState: CollapseState = CollapseState.EXPANDED
) = composed {
    val animatedHeight = animatedValue(initVal = 0, converter = IntPxToVectorConverter)
    var isRunning by stateFor(id) { false }
    val originalHeightState = stateFor(id) { 0 }
    var currentState by stateFor(id) { initialState }
    val heightState = stateFor(id) { 0 }

    if (isRunning.not()) animatedHeight.snapTo(if (currentState == CollapseState.EXPANDED) heightState.value else 0)
    if (collapsed != currentState) {
        //State change requested
        val animation = TweenBuilder<Int>().apply { this.duration = duration }
        isRunning = true
        if (collapsed == CollapseState.COLLAPSED) {
            // Collapse requested
            animatedHeight.animateTo(
                0,
                onEnd = { _, _ -> onChanged(CollapseState.COLLAPSED); isRunning = false })
        } else {
            animatedHeight.animateTo(
                originalHeightState.value,
                animation,
                onEnd = { _, _ -> onChanged(CollapseState.EXPANDED); isRunning = false })
        }
        currentState = collapsed
    }
    CollapseableModifier(originalHeightState, animatedHeight, heightState)
}

private data class CollapseableModifier(
    val originalHeightState: MutableState<Int>,
    val animatedHeight: AnimatedValue<Int, AnimationVector1D>,
    val height: MutableState<Int>
) : LayoutModifier {
    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints,
        layoutDirection: LayoutDirection
    ): MeasureScope.MeasureResult {
        val childConstraints =
            constraints.copy(maxHeight = Int.MAX_VALUE, maxWidth = constraints.maxWidth)
        val placeable = measurable.measure(childConstraints)
        originalHeightState.value = placeable.height
        val width = min(placeable.width, constraints.maxWidth)
        height.value = min(placeable.height, constraints.maxHeight)

        return layout(width, animatedHeight.value) {
            placeable.place(0, 0)
        }
    }
}
Maybe you could modify it for your needs
l
we will likely add a modifier for this use case
❤️ 1
curious though
and feel better to separate ui and logic.
how is this accomplishing that? can you elaborate on this?
s
Wouldn't you be doing work for things that are not visible? Is the tradeoff of indentation and readability worth to any performace cost?
l
@Siyamed if shown oscillates frequently, doing what he suggests is actually the more efficient of the two
s
I see, thanks
l
it also might depend on what you want…. do you want it to be invisible but also affect layout? or do you just want it to be as if it’s out of the hierarchy entirely?
t
I think it depends on you. If you want it completly out of the view hiearchy than Modifieres are not the right approach. Even if it is possible to do it with them. So you could use the Modifier.collapseable to animate the removing and after it is collapsed you could remove it from the view hierarchy using the if statement outside of the component.
h
@Timo Drick Thanks for the answer. I’ll try to use it. 🙏 @Leland Richardson [G] 1. Thanks for considering my opinion. about
separating ui and logic
. it may be just my feeling which originated from layout and data binding experience. but, as you give me opportunity to explain. currently I’m thinking what is the best approach to use compose. and I felt that there is possibility that developers add logic on ui carelessly. though, if the architecture is well organized, it has no issues. so, I have thought that how to separate ui and logic. and I considered layout data binding approach. it doesn’t change ui format. just change attributes. so, developer can read ui code easy. and logic side can focus on variable only. 2. I’m not sure what is the best approach. but in my short experience. if it’s possible. it seems good to use same way with View.Visible, View.Gone, View.Invisible. but. if it’s difficult, gone and visible also be good.
as I don’t know about compose under the hood. and I just asking advice as an user of compose. kindly understand if I explain inappropriate opinion.
t
Here is your visibility modifier 😄 Not really tested it much but for me it works:
Copy code
enum class Visibility {
    VISIBLE, INVISIBLE, GONE
}

@Composable
fun Modifier.visibility(visibility: Visibility) = this + VisibleModifier(visibility)

private data class VisibleModifier(val visibility: Visibility) : LayoutModifier {
    override fun MeasureScope.measure(
        measurable: Measurable,
        constraints: Constraints,
        layoutDirection: LayoutDirection
    ): MeasureScope.MeasureResult {
        return if (visibility == Visibility.GONE) {
            layout(0, 0) {
                // Empty placement block
            }
        } else {
            val placeable = measurable.measure(constraints)
            layout(placeable.width, placeable.height) {
                if (visibility == Visibility.VISIBLE) {
                    placeable.place(0, 0)
                }
            }
        }
    }
}
☝️ 1
👍 1
h
@Timo Drick Wow.. thank you sooo much🙏🙏🙏
It looks working fine. but, when I use that. there is another issue. I was thinking to use compose like the example below.
Copy code
var resource by state<Resource<String>> { Resource.Loading }

Column(Modifier.visible(visible = resource.isSuccess()) {
    Text(resource.successData)// this convert success data as not null String
}
Though the
Text(resource.successData)
is shown only on success case, yet
Text(resource.successData)
is processed even if visibility is gone. so, I have to check null if the success data exists or not like
Text(resource.data?:"")
as modifier mentioned that it will shows only success case. I would like not to handle the case that data not exists. is it possible not to process sub view if visibility is gone?
l
No. The way to do that is to just conditionally compose. Use if.
h
okay thanks for the answer.🙏
if
seems proper approach 🙂 if
Column
doesn’t exits on the above example. we can’t use Modifier.visible() then it’s better not to use it.
t
This problem you see is a common use case which everyone has to solve. I think maybe a compose tutorial for doing some background thread work and show progress animation is a good idea. I do use following approach (Image loading is just an example. Of course you can also do some other business logic that takes some time):
Copy code
@Composable
fun ImageView(url: String) {
    var retryCounter by stateFor(url) { 0 }
    val imageState = ImageLoader.loadImageUI(url)
    //val state = imageState
    InitializedCrossfade(current = imageState) { state ->
        when (state) {
            is LoadingState.Start -> Box(Modifier.fillMaxSize(), backgroundColor = MaterialTheme.colors.surface,gravity = Alignment.Center)
            is LoadingState.Loading -> LoadingBox()
            is LoadingState.Success -> Image(asset = state.data, modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop)
            is LoadingState.Error -> ErrorBox(onClick = { retryCounter++ })
        }
    }
}
Also a general wrapper to do stuff on a coroutine and get the loading state:
Copy code
sealed class LoadingState<out T: Any> {
    object Start: LoadingState<Nothing>()
    object Loading: LoadingState<Nothing>()
    class Error(val error: Exception): LoadingState<Nothing>()
    class Success<T: Any>(val data: T): LoadingState<T>()
}

@Composable
fun <T: Any> loadingStateFor(vararg inputs: Any?, initBlock: () -> LoadingState<T> = { LoadingState.Start },
                             loadingBlock: suspend CoroutineScope.() -> T): LoadingState<T> {
    var state by stateFor(*inputs, init = initBlock)
    if (state !is LoadingState.Success) {
        launchInComposition(*inputs) {
            val loadingSpinnerDelay = async {
                delay(500)
                state = LoadingState.Loading
            }
            state = try {
                LoadingState.Success(loadingBlock())
            } catch (err: Exception) {
                LoadingState.Error(err)
            } finally {
                loadingSpinnerDelay.cancelAndJoin()
            }
        }
    }
    return state
}
👍 1