From 32e93a25717a0b1d73b45fae371c5ee229157dbb Mon Sep 17 00:00:00 2001 From: Uzias Ferreira <63263091+uziasferreirazup@users.noreply.github.com> Date: Tue, 27 Oct 2020 14:06:33 -0300 Subject: [PATCH] feat: add support to bind in Display (#1086) * fix: parse color when is RGB size (#1033) * fix: parse color when is RGB size Signed-off-by: Matheus Ribeiro * refactor: change method structure in ColorUtils.kt Signed-off-by: Matheus Ribeiro * refactor tests method names Signed-off-by: Matheus Ribeiro * fix: adjust http client default to handle exception when set body (#1050) * remove shared code bettween backend and android * adjust detekt * refactor: move module kotlin-core to inside beagle to possible add bind in fields of core * fix: adjust override title when navigate in screen (#1052) * fix: adjust context evaluation to return correct types (#1043) * refactor: remove deprecated from constructor * fix: adjust name action backend * fix: adjust serialization context * adjust try serializer * adjust context data manager * adjust try to deserializer * adjust unit tests * adjust class * remove space extra * adjust test in form * adjust evaluation * adjust lint * fix: adjust context data evaluation to enum * adjust lint * adjust suppor to bind * remove module kotlin-core * fix: adjust support to bind * adjust code * remove code used to test * create unit test to mapper and flex view * adjust import * adjust function * adjust layout change * adjust unit test * adjust unit test * adjust unit test Co-authored-by: Matheus Ribeiro Lima <57918064+matheusribeirozup@users.noreply.github.com> Co-authored-by: viniciusguardieirozup <58824112+viniciusguardieirozup@users.noreply.github.com> --- .../android/context/ContextDataExtensions.kt | 21 ++- .../android/engine/mapper/FlexMapper.kt | 23 +++- .../beagle/android/utils/WidgetExtensions.kt | 9 ++ .../android/view/custom/BeagleFlexView.kt | 21 ++- .../main/java/br/com/zup/beagle/core/Style.kt | 3 +- .../android/engine/mapper/FlexMapperTest.kt | 109 +++++++++------- .../android/view/custom/BeagleFlexViewTest.kt | 120 ++++++++++++++++++ 7 files changed, 245 insertions(+), 61 deletions(-) create mode 100644 android/beagle/src/test/java/br/com/zup/beagle/android/view/custom/BeagleFlexViewTest.kt diff --git a/android/beagle/src/main/java/br/com/zup/beagle/android/context/ContextDataExtensions.kt b/android/beagle/src/main/java/br/com/zup/beagle/android/context/ContextDataExtensions.kt index 2de69b310d..f09ac89483 100644 --- a/android/beagle/src/main/java/br/com/zup/beagle/android/context/ContextDataExtensions.kt +++ b/android/beagle/src/main/java/br/com/zup/beagle/android/context/ContextDataExtensions.kt @@ -17,6 +17,8 @@ package br.com.zup.beagle.android.context import br.com.zup.beagle.android.data.serializer.BeagleMoshi +import kotlin.reflect.KClass +import kotlin.reflect.full.isSubclassOf import org.json.JSONArray import org.json.JSONException import org.json.JSONObject @@ -31,14 +33,23 @@ internal fun ContextData.normalize(): ContextData { } internal fun Any.normalizeContextValue(): Any { - return if (isValueNormalized()) { - this - } else { - val newValue = BeagleMoshi.moshi.adapter(Any::class.java).toJson(this) ?: "" - newValue.normalizeContextValue() + return when { + isValueNormalized() -> { + this + } + isEnum(this::class) -> { + this.toString() + } + else -> { + val newValue = BeagleMoshi.moshi.adapter(Any::class.java).toJson(this) ?: "" + newValue.normalizeContextValue() + } } } +private fun isEnum(type: KClass) = type.isSubclassOf(Enum::class) + + internal fun String.normalizeContextValue(): Any { return try { JSONObject(this) diff --git a/android/beagle/src/main/java/br/com/zup/beagle/android/engine/mapper/FlexMapper.kt b/android/beagle/src/main/java/br/com/zup/beagle/android/engine/mapper/FlexMapper.kt index 4c8602fb72..cb4af5a6b3 100644 --- a/android/beagle/src/main/java/br/com/zup/beagle/android/engine/mapper/FlexMapper.kt +++ b/android/beagle/src/main/java/br/com/zup/beagle/android/engine/mapper/FlexMapper.kt @@ -16,7 +16,10 @@ package br.com.zup.beagle.android.engine.mapper +import android.view.View import br.com.zup.beagle.android.utils.dp +import br.com.zup.beagle.android.utils.internalObserveBindChanges +import br.com.zup.beagle.android.widget.RootView import br.com.zup.beagle.core.Style import br.com.zup.beagle.widget.core.EdgeValue import br.com.zup.beagle.widget.core.Size @@ -31,7 +34,7 @@ import com.facebook.yoga.YogaNode import com.facebook.yoga.YogaPositionType import com.facebook.yoga.YogaWrap -class FlexMapper { +internal class FlexMapper { fun makeYogaNode(style: Style): YogaNode = YogaNode.create().apply { flexDirection = makeYogaFlexDirection(style.flex?.flexDirection) ?: YogaFlexDirection.COLUMN @@ -40,16 +43,30 @@ class FlexMapper { alignItems = makeYogaAlignItems(style.flex?.alignItems) ?: YogaAlign.STRETCH alignSelf = makeYogaAlignSelf(style.flex?.alignSelf) ?: YogaAlign.AUTO alignContent = makeYogaAlignContent(style.flex?.alignContent) ?: YogaAlign.FLEX_START - if(style.flex?.flex == null) { + if (style.flex?.flex == null) { flexGrow = style.flex?.grow?.toFloat() ?: 0.0f flexShrink = style.flex?.shrink?.toFloat() ?: 1.0f } style.flex?.flex?.toFloat()?.let { flex = it } - display = makeYogaDisplay(style.display) ?: YogaDisplay.FLEX + + display = YogaDisplay.FLEX positionType = makeYogaPositionType(style.positionType) ?: YogaPositionType.RELATIVE applyAttributes(style, this) } + fun observeBindChangesFlex(style: Style, + rootView: RootView, + view: View, + yogaNode: YogaNode) { + + if (style.display != null) { + internalObserveBindChanges(rootView, view, style.display) { + yogaNode.display = makeYogaDisplay(it) ?: YogaDisplay.FLEX + view.requestLayout() + } + } + } + private fun applyAttributes(style: Style, yogaNode: YogaNode) { setWidth(style.size, yogaNode) setHeight(style.size, yogaNode) diff --git a/android/beagle/src/main/java/br/com/zup/beagle/android/utils/WidgetExtensions.kt b/android/beagle/src/main/java/br/com/zup/beagle/android/utils/WidgetExtensions.kt index c424e1f784..ec88690ce2 100644 --- a/android/beagle/src/main/java/br/com/zup/beagle/android/utils/WidgetExtensions.kt +++ b/android/beagle/src/main/java/br/com/zup/beagle/android/utils/WidgetExtensions.kt @@ -124,6 +124,15 @@ fun ServerDrivenComponent.observeBindChanges( view: View, bind: Bind, observes: Observer +) { + internalObserveBindChanges(rootView, view, bind, observes) +} + +internal fun internalObserveBindChanges( + rootView: RootView, + view: View, + bind: Bind, + observes: Observer ) { val value = bind.observe(rootView, view, observes) if (bind is Bind.Value) { diff --git a/android/beagle/src/main/java/br/com/zup/beagle/android/view/custom/BeagleFlexView.kt b/android/beagle/src/main/java/br/com/zup/beagle/android/view/custom/BeagleFlexView.kt index 911cf7935b..8d80ec15c1 100644 --- a/android/beagle/src/main/java/br/com/zup/beagle/android/view/custom/BeagleFlexView.kt +++ b/android/beagle/src/main/java/br/com/zup/beagle/android/view/custom/BeagleFlexView.kt @@ -28,8 +28,10 @@ import br.com.zup.beagle.core.GhostComponent import br.com.zup.beagle.core.ServerDrivenComponent import br.com.zup.beagle.core.Style import br.com.zup.beagle.core.StyleComponent +import com.facebook.yoga.YogaNode import com.facebook.yoga.YogaNodeJNIBase +@Suppress("LeakingThis") @SuppressLint("ViewConstructor") internal open class BeagleFlexView( private val rootView: RootView, @@ -39,6 +41,10 @@ internal open class BeagleFlexView( private val viewModel: ScreenContextViewModel = rootView.generateViewModelInstance() ) : YogaLayout(rootView.getContext(), flexMapper.makeYogaNode(style)) { + init { + observeStyleChanges(style, this, yogaNode) + } + constructor( rootView: RootView, flexMapper: FlexMapper = FlexMapper() @@ -47,8 +53,7 @@ internal open class BeagleFlexView( var listenerOnViewDetachedFromWindow: (() -> Unit)? = null fun addView(child: View, style: Style) { - - super.addView(child, flexMapper.makeYogaNode(style)) + addViewWithBind(style, child, this) } fun addServerDrivenComponent(serverDrivenComponent: ServerDrivenComponent, @@ -65,7 +70,17 @@ internal open class BeagleFlexView( (yogaNode as YogaNodeJNIBase).dirtyAllDescendants() } } - super.addView(view, flexMapper.makeYogaNode(style)) + addViewWithBind(style, view, view) + } + + private fun addViewWithBind(style: Style, child: View, viewBind: View){ + val childYogaNode = flexMapper.makeYogaNode(style) + observeStyleChanges(style, viewBind, childYogaNode) + super.addView(child, childYogaNode) + } + + private fun observeStyleChanges(style: Style, view: View, yogaNode: YogaNode) { + flexMapper.observeBindChangesFlex(style, rootView, view, yogaNode) } override fun onAttachedToWindow() { diff --git a/android/beagle/src/main/java/br/com/zup/beagle/core/Style.kt b/android/beagle/src/main/java/br/com/zup/beagle/core/Style.kt index 6e030e4ff2..8dc617dd01 100644 --- a/android/beagle/src/main/java/br/com/zup/beagle/core/Style.kt +++ b/android/beagle/src/main/java/br/com/zup/beagle/core/Style.kt @@ -16,6 +16,7 @@ package br.com.zup.beagle.core +import br.com.zup.beagle.android.context.Bind import br.com.zup.beagle.widget.core.EdgeValue import br.com.zup.beagle.widget.core.Flex import br.com.zup.beagle.widget.core.Size @@ -59,7 +60,7 @@ data class Style ( val position: EdgeValue? = null, val flex: Flex? = null, val positionType: PositionType? = null, - val display: Display? = null + val display: Bind? = null ) /** diff --git a/android/beagle/src/test/java/br/com/zup/beagle/android/engine/mapper/FlexMapperTest.kt b/android/beagle/src/test/java/br/com/zup/beagle/android/engine/mapper/FlexMapperTest.kt index 8f4abd95bf..bc5ae27f45 100644 --- a/android/beagle/src/test/java/br/com/zup/beagle/android/engine/mapper/FlexMapperTest.kt +++ b/android/beagle/src/test/java/br/com/zup/beagle/android/engine/mapper/FlexMapperTest.kt @@ -16,8 +16,13 @@ package br.com.zup.beagle.android.engine.mapper +import android.view.View +import br.com.zup.beagle.android.context.valueOf import br.com.zup.beagle.android.extensions.once +import br.com.zup.beagle.android.utils.Observer import br.com.zup.beagle.android.utils.dp +import br.com.zup.beagle.android.utils.internalObserveBindChanges +import br.com.zup.beagle.android.widget.RootView import br.com.zup.beagle.core.Display import br.com.zup.beagle.core.Style import br.com.zup.beagle.widget.core.AlignContent @@ -40,12 +45,16 @@ import com.facebook.yoga.YogaNode import com.facebook.yoga.YogaWrap import io.mockk.MockKAnnotations import io.mockk.Runs -import io.mockk.clearStaticMockk import io.mockk.every -import io.mockk.impl.annotations.MockK import io.mockk.just +import io.mockk.mockk import io.mockk.mockkStatic +import io.mockk.slot +import io.mockk.unmockkAll import io.mockk.verify +import io.mockk.verifyOrder +import io.mockk.verifySequence +import kotlin.test.assertFalse import org.junit.After import org.junit.Before import org.junit.Test @@ -55,8 +64,11 @@ private const val ONE_UNIT_VALUE = 1.0 class FlexMapperTest { - @MockK - private lateinit var yogaNode: YogaNode + private val yogaNodeMock: YogaNode = mockk(relaxed = true, relaxUnitFun = true) + private val rootViewMock: RootView = mockk() + private val viewMock: View = mockk(relaxUnitFun = true, relaxed = true) + + private val observeSlot = slot>() private lateinit var flexMapper: FlexMapper @@ -68,48 +80,25 @@ class FlexMapperTest { mockkStatic(YogaNode::class) mockkStatic("br.com.zup.beagle.android.utils.NumberExtensionsKt") + mockkStatic("br.com.zup.beagle.android.utils.WidgetExtensionsKt") + + every { + internalObserveBindChanges( + rootView = rootViewMock, + view = viewMock, + bind = any(), + observes = capture(observeSlot) + ) + } just Runs every { HUNDRED_UNIT_VALUE.dp() } returns HUNDRED_UNIT_VALUE every { ONE_UNIT_VALUE.dp() } returns ONE_UNIT_VALUE - every { YogaNode.create() } returns yogaNode - every { yogaNode.flexDirection = any() } just Runs - every { yogaNode.wrap = any() } just Runs - every { yogaNode.justifyContent = any() } just Runs - every { yogaNode.alignItems = any() } just Runs - every { yogaNode.alignSelf = any() } just Runs - every { yogaNode.alignContent = any() } just Runs - every { yogaNode.flex = any() } just Runs - every { yogaNode.flexGrow = any() } just Runs - every { yogaNode.flexShrink = any() } just Runs - every { yogaNode.display = any() } just Runs - every { yogaNode.aspectRatio = any() } just Runs - every { yogaNode.positionType = any() } just Runs - every { yogaNode.setWidth(any()) } just Runs - every { yogaNode.setWidthPercent(any()) } just Runs - every { yogaNode.setHeight(any()) } just Runs - every { yogaNode.setHeightPercent(any()) } just Runs - every { yogaNode.setMaxWidth(any()) } just Runs - every { yogaNode.setMaxWidthPercent(any()) } just Runs - every { yogaNode.setMaxHeight(any()) } just Runs - every { yogaNode.setMaxHeightPercent(any()) } just Runs - every { yogaNode.setMinWidth(any()) } just Runs - every { yogaNode.setMinWidthPercent(any()) } just Runs - every { yogaNode.setMinHeight(any()) } just Runs - every { yogaNode.setMinHeightPercent(any()) } just Runs - every { yogaNode.setFlexBasis(any()) } just Runs - every { yogaNode.setFlexBasisPercent(any()) } just Runs - every { yogaNode.setMargin(any(), any()) } just Runs - every { yogaNode.setMarginPercent(any(), any()) } just Runs - every { yogaNode.setPadding(any(), any()) } just Runs - every { yogaNode.setPaddingPercent(any(), any()) } just Runs - every { yogaNode.setPosition(any(), any()) } just Runs - every { yogaNode.setPositionPercent(any(), any()) } just Runs - every { yogaNode.setFlexBasisAuto() } just Runs + every { YogaNode.create() } returns yogaNodeMock } @After fun tearDown() { - clearStaticMockk() + unmockkAll() } @Test @@ -197,7 +186,7 @@ class FlexMapperTest { } @Test - fun makeYogaNode_should_set_flex_as_1(){ + fun makeYogaNode_should_set_flex_as_1() { //Given val flex = Flex( flex = ONE_UNIT_VALUE @@ -207,7 +196,7 @@ class FlexMapperTest { val yogaNode = flexMapper.makeYogaNode(Style(flex = flex)) //Then - verify (exactly = once()) { yogaNode.flex = ONE_UNIT_VALUE.toFloat() } + verify(exactly = once()) { yogaNode.flex = ONE_UNIT_VALUE.toFloat() } } @Test @@ -239,31 +228,53 @@ class FlexMapperTest { } @Test - fun makeYogaNode_should_set_display_as_FLEX() { + fun `GIVEN display NONE WHEN call observe bind changes THEN it should set in yoga node correct display`() { // Given val style = Style( - display = Display.FLEX + display = valueOf(Display.NONE) ) // When - val yogaNode = flexMapper.makeYogaNode(style) + flexMapper.observeBindChangesFlex(style, rootViewMock, viewMock, yogaNodeMock) + observeSlot.captured.invoke(Display.NONE) // Then - verify(exactly = once()) { yogaNode.display = YogaDisplay.FLEX } + verifyOrder { + yogaNodeMock.display = YogaDisplay.NONE + viewMock.requestLayout() + } } @Test - fun makeYogaNode_should_set_display_as_NONE() { + fun `GIVEN display FLEX WHEN call observe bind changes THEN it should set in yoga node correct display`() { // Given val style = Style( - display = Display.NONE + display = valueOf(Display.FLEX) ) // When - val yogaNode = flexMapper.makeYogaNode(style) + flexMapper.observeBindChangesFlex(style, rootViewMock, viewMock, yogaNodeMock) + observeSlot.captured.invoke(Display.FLEX) + + // Then + verifyOrder { + yogaNodeMock.display = YogaDisplay.FLEX + viewMock.requestLayout() + } + } + + @Test + fun `GIVEN display NULL WHEN call observe bind changes THEN it should never call bind changes`() { + // Given + val style = Style( + display = null + ) + + // When + flexMapper.observeBindChangesFlex(style, rootViewMock, viewMock, yogaNodeMock) // Then - verify(exactly = once()) { yogaNode.display = YogaDisplay.NONE } + assertFalse(observeSlot.isCaptured) } @Test diff --git a/android/beagle/src/test/java/br/com/zup/beagle/android/view/custom/BeagleFlexViewTest.kt b/android/beagle/src/test/java/br/com/zup/beagle/android/view/custom/BeagleFlexViewTest.kt new file mode 100644 index 0000000000..0e470f2825 --- /dev/null +++ b/android/beagle/src/test/java/br/com/zup/beagle/android/view/custom/BeagleFlexViewTest.kt @@ -0,0 +1,120 @@ +/* + * Copyright 2020 ZUP IT SERVICOS EM TECNOLOGIA E INOVACAO SA + * + * 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. + */ + +package br.com.zup.beagle.android.view.custom + +import android.view.View +import br.com.zup.beagle.android.engine.mapper.FlexMapper +import br.com.zup.beagle.android.engine.renderer.ViewRenderer +import br.com.zup.beagle.android.engine.renderer.ViewRendererFactory +import br.com.zup.beagle.android.view.viewmodel.ScreenContextViewModel +import br.com.zup.beagle.android.widget.RootView +import br.com.zup.beagle.core.ServerDrivenComponent +import br.com.zup.beagle.core.Style +import com.facebook.yoga.YogaNode +import io.mockk.Runs +import io.mockk.every +import io.mockk.just +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import org.junit.Test + +class BeagleFlexViewTest { + + private val rootViewMock = mockk(relaxed = true, relaxUnitFun = true) + private val flexMapperMock = mockk(relaxUnitFun = true, relaxed = true) + private val styleMock = mockk