Skip to content
This repository has been archived by the owner on Jun 1, 2024. It is now read-only.

feat: add Android pull to refresh component #1636

Merged
merged 18 commits into from
Jun 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
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
1 change: 1 addition & 0 deletions android/beagle/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ dependencies {
implementation Dependencies.AndroidxLibraries.appcompat
implementation Dependencies.AndroidxLibraries.coreKtx
implementation Dependencies.AndroidxLibraries.recyclerView
implementation Dependencies.AndroidxLibraries.swipeRefreshLayout
implementation Dependencies.AndroidxLibraries.viewModel
implementation Dependencies.AndroidxLibraries.viewModelExtensions

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* 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.components.refresh

import android.graphics.Color
import android.view.View
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import br.com.zup.beagle.android.action.Action
import br.com.zup.beagle.android.context.Bind
import br.com.zup.beagle.android.context.ContextComponent
import br.com.zup.beagle.android.context.ContextData
import br.com.zup.beagle.android.context.valueOfNullable
import br.com.zup.beagle.android.utils.handleEvent
import br.com.zup.beagle.android.utils.observeBindChanges
import br.com.zup.beagle.android.view.ViewFactory
import br.com.zup.beagle.android.widget.RootView
import br.com.zup.beagle.android.widget.WidgetView
import br.com.zup.beagle.annotation.RegisterWidget
import br.com.zup.beagle.core.ServerDrivenComponent
import br.com.zup.beagle.core.SingleChildComponent

@RegisterWidget("pullToRefresh")
data class PullToRefresh constructor(
override val context: ContextData? = null,
val onPull: List<Action>,
val isRefreshing: Bind<Boolean>? = null,
val color: Bind<String>? = null,
override val child: ServerDrivenComponent,
) : WidgetView(), ContextComponent, SingleChildComponent {

constructor(
context: ContextData? = null,
onPull: List<Action>,
isRefreshing: Bind<Boolean>? = null,
paulomeurerzup marked this conversation as resolved.
Show resolved Hide resolved
color: String? = null,
child: ServerDrivenComponent,
) : this(
context = context,
onPull = onPull,
isRefreshing = isRefreshing,
color = valueOfNullable(color),
child = child,
)

@Transient
private val viewFactory = ViewFactory()

override fun buildView(rootView: RootView): View {
return viewFactory.makeSwipeRefreshLayout(rootView.getContext()).apply {
addView(buildChildView(rootView))
setOnRefreshListener {
handleEvent(rootView, this, onPull)
}
observeRefreshState(rootView, this)
observeColor(rootView, this)
}
}

private fun buildChildView(rootView: RootView) = viewFactory.makeBeagleFlexView(rootView).apply {
addView(child, false)
}

private fun observeRefreshState(rootView: RootView, view: SwipeRefreshLayout) {
isRefreshing?.let { bind ->
observeBindChanges(rootView, view, bind) {
view.isRefreshing = it ?: false
}
}
}

private fun observeColor(rootView: RootView, view: SwipeRefreshLayout) {
color?.let { bind ->
observeBindChanges(rootView, view, bind) {
view.setColorSchemeColors(Color.parseColor(it))
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import android.widget.ScrollView
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import br.com.zup.beagle.R
import br.com.zup.beagle.android.components.BeagleRecyclerView
import br.com.zup.beagle.android.components.utils.RoundedImageView
Expand Down Expand Up @@ -103,4 +104,6 @@ internal class ViewFactory {

fun makeBeagleRecyclerViewScrollIndicatorVertical(context: Context) =
BeagleRecyclerView(ContextThemeWrapper(context, R.style.Beagle_Widget_ScrollIndicatorVertical))

fun makeSwipeRefreshLayout(context: Context) = SwipeRefreshLayout(context)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
/*
* 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.components.refresh

import android.graphics.Color
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import br.com.zup.beagle.android.action.Action
import br.com.zup.beagle.android.components.BaseComponentTest
import br.com.zup.beagle.android.context.Bind
import br.com.zup.beagle.android.context.ContextData
import br.com.zup.beagle.android.utils.Observer
import br.com.zup.beagle.android.utils.observeBindChanges
import br.com.zup.beagle.android.view.ViewFactory
import br.com.zup.beagle.core.ServerDrivenComponent
import io.mockk.Runs
import io.mockk.every
import io.mockk.just
import io.mockk.mockk
import io.mockk.mockkStatic
import io.mockk.slot
import io.mockk.verify
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test

@DisplayName("Given a PullToRefresh")
class PullToRefreshTest : BaseComponentTest() {

private val swipeRefreshLayout: SwipeRefreshLayout = mockk(relaxed = true)
private val context: ContextData = mockk(relaxed = true)
private val action: Action = mockk(relaxed = true)
private val onPullActions = listOf(action)
private val isRefreshing: Bind<Boolean> = mockk(relaxed = true)
private val color: Bind<String> = mockk(relaxed = true)
private val child = mockk<ServerDrivenComponent>()
private val listenerSlot = slot<SwipeRefreshLayout.OnRefreshListener>()
private val booleanObserverSlot = slot<Observer<Boolean?>>()
private val stringObserverSlot = slot<Observer<String?>>()
private lateinit var pullToRefreshComponent: PullToRefresh

@BeforeEach
override fun setUp() {
super.setUp()

mockkStatic("br.com.zup.beagle.android.utils.WidgetExtensionsKt")
mockkStatic(Color::class)

every { anyConstructed<ViewFactory>().makeSwipeRefreshLayout(any()) } returns swipeRefreshLayout
every { swipeRefreshLayout.setOnRefreshListener(capture(listenerSlot)) } just Runs

pullToRefreshComponent = PullToRefresh(
context,
onPullActions,
isRefreshing,
color,
child
)
}

@DisplayName("When build view")
@Nested
inner class PullToRefreshBuildTest {

@Test
@DisplayName("Then should create correct pullToRefresh")
fun testBuildCorrectPullToRefresh() {
// When
val view = pullToRefreshComponent.buildView(rootView)

// Then
Assertions.assertTrue(view is SwipeRefreshLayout)
verify(exactly = 1) { anyConstructed<ViewFactory>().makeSwipeRefreshLayout(rootView.getContext()) }
}

@Test
@DisplayName("Then should set RefreshListener on pullToRefresh")
fun testBuildSetRefreshListener() {
// When
pullToRefreshComponent.buildView(rootView)

// Then
verify(exactly = 1) { swipeRefreshLayout.setOnRefreshListener(any()) }
}

@Test
@DisplayName("Then should observe isRefreshing changes")
fun testBuildObserveIsRefreshing() {
// When
pullToRefreshComponent.buildView(rootView)

// Then
verify(exactly = 1) {
pullToRefreshComponent.observeBindChanges(rootView, swipeRefreshLayout, isRefreshing, captureLambda())
}
}

@Test
@DisplayName("Then should observe color changes")
fun testBuildObserveColor() {
// When
pullToRefreshComponent.buildView(rootView)

// Then
verify(exactly = 1) {
pullToRefreshComponent.observeBindChanges(rootView, swipeRefreshLayout, color, captureLambda())
}
}

@Test
@DisplayName("Then should add child")
fun testBuildAddChild() {
// When
pullToRefreshComponent.buildView(rootView)

// Then
verify(exactly = 1) {
beagleFlexView.addView(child, false)
}
}
}

@DisplayName("When build view with null params")
@Nested
inner class PullToRefreshBuildWithNullParamsTest {

@Test
@DisplayName("Then should not observe isRefreshing changes")
fun testBuildNotObserveIsRefreshing() {
// Given
pullToRefreshComponent = PullToRefresh(
context,
onPullActions,
null,
"#FF0000",
child
)

// When
pullToRefreshComponent.buildView(rootView)

// Then
verify(exactly = 0) {
pullToRefreshComponent.observeBindChanges(rootView, swipeRefreshLayout, isRefreshing, captureLambda())
}
}

@Test
@DisplayName("Then should not observe color changes")
fun testBuildNotObserveColor() {
// Given
pullToRefreshComponent = PullToRefresh(
context,
onPullActions,
isRefreshing,
null as String?,
child
)

// When
pullToRefreshComponent.buildView(rootView)

// Then
verify(exactly = 0) {
pullToRefreshComponent.observeBindChanges(rootView, swipeRefreshLayout, color, captureLambda())
}
}
}

@DisplayName("When onRefresh triggered")
@Nested
inner class PullToRefreshOnRefreshTest {

@Test
@DisplayName("Then should call onPull actions")
fun testTriggerOnRefresh() {

// When
pullToRefreshComponent.buildView(rootView)
listenerSlot.captured.onRefresh()

verify(exactly = 1) { action.execute(rootView, swipeRefreshLayout) }
}
}

@DisplayName("When refresh state change")
@Nested
inner class PullToRefreshRefreshStateChangeTest {

@Test
@DisplayName("Then should update refresh state to true")
fun testChangeIsRefreshingToTrue() {
testIsRefreshingStateChange(true)
}

@Test
@DisplayName("Then should update refresh state to false")
fun testChangeIsRefreshingToFalse() {
testIsRefreshingStateChange(false)
}

@Test
@DisplayName("Then should update refresh state to false when isRefreshing evaluates to null")
fun testChangeIsRefreshingToFalseWhenEvaluatesNull() {
testIsRefreshingStateChange(null)
}

private fun testIsRefreshingStateChange(refreshing: Boolean?) {
every { pullToRefreshComponent.observeBindChanges(rootView, swipeRefreshLayout, isRefreshing, capture(booleanObserverSlot)) } just Runs

// When
pullToRefreshComponent.buildView(rootView)
booleanObserverSlot.captured.invoke(refreshing)

verify(exactly = 1) { swipeRefreshLayout.isRefreshing = refreshing ?: false }
}
}

@DisplayName("When color state change")
@Nested
inner class PullToRefreshColorStateChangeTest {

@Test
@DisplayName("Then should update the correct color")
fun testChangeColorState() {
// Given
val colorString = "#FF0000"
every { pullToRefreshComponent.observeBindChanges(rootView, swipeRefreshLayout, color, capture(stringObserverSlot)) } just Runs
every { Color.parseColor(colorString) } returns -65536
val expectedColor = Color.parseColor(colorString)

// When
pullToRefreshComponent.buildView(rootView)
stringObserverSlot.captured.invoke(colorString)

verify(exactly = 1) { swipeRefreshLayout.setColorSchemeColors(expectedColor) }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ import br.com.zup.beagle.android.data.serializer.makeObjectTextInputWithExpressi
import br.com.zup.beagle.android.data.serializer.makeObjectTouchable
import br.com.zup.beagle.android.data.serializer.makeObjectWebView
import br.com.zup.beagle.android.data.serializer.makeObjectWebViewWithExpression
import br.com.zup.beagle.android.data.serializer.makePullToRefreshJson
import br.com.zup.beagle.android.data.serializer.makePullToRefreshObject
import br.com.zup.beagle.android.data.serializer.makePullToRefreshWithoutExpressionJson
import br.com.zup.beagle.android.data.serializer.makePullToRefreshWithoutExpressionObject
import br.com.zup.beagle.android.data.serializer.makeScreenComponentJson
import br.com.zup.beagle.android.data.serializer.makeScrollViewJson
import br.com.zup.beagle.android.data.serializer.makeSimpleFormJson
Expand Down Expand Up @@ -111,6 +115,8 @@ class DefaultComponentSerializerTest : DefaultSerializerTest<ServerDrivenCompone
Arguments.of(makeTouchableJson(), makeObjectTouchable()),
Arguments.of(makeWebViewJson(), makeObjectWebView()),
Arguments.of(makeWebViewWithExpressionJson(), makeObjectWebViewWithExpression()),
Arguments.of(makePullToRefreshJson(), makePullToRefreshObject()),
Arguments.of(makePullToRefreshWithoutExpressionJson(), makePullToRefreshWithoutExpressionObject()),
)

private fun makeObjectScreenComponent() = ScreenComponent(
Expand Down
Loading