From 068a6e0c41e5c954e9e82d5850d07d4c517f742c Mon Sep 17 00:00:00 2001 From: Elie Karouz Date: Fri, 11 Aug 2023 09:05:45 +0300 Subject: [PATCH 1/2] Added the concept of flow id that would allow to identify a flow per input. When context.renderChild is called, the `childFlow` is started with provided `input` if the `childFlow.id(input)` is not already started. Otherwise, the flow is resumed. Moved the ViewModal concept outside the feedbacktree core library. It is now part of the sample app. Other developers can be inspired by this code and implement their own solution. --- .gitignore | 2 +- .idea/misc.xml | 6 -- .idea/modules.xml | 34 ------- .../feedbacktree/tutorials/AppViewRegistry.kt | 3 +- .../tutorials/flows/counter/CounterFlow.kt | 12 +-- .../tutorials/flows/login/LoginFlow.kt | 11 +-- .../tutorials/flows/modals/ModalsFlow.kt | 89 ++++++++++--------- .../tutorials/flows}/modals/ViewModal.kt | 24 +++-- .../flows}/modals/ViewModalDialogBinding.kt | 50 ++++++++--- .../PhoneNumberRegistrationFlow.kt | 26 +++--- .../tutorials/flows/resources/ResoucesFlow.kt | 11 +-- .../flows/tutorialsroot/TutorialsFlow.kt | 32 +++---- build.gradle.kts | 2 +- .../java/com/feedbacktree/flow/core/Flow.kt | 17 +++- .../com/feedbacktree/flow/core/FlowNode.kt | 2 +- .../flow/ui/views/WorkflowLayout.kt | 44 +++++---- .../flow/ui/views/modals/DialogRegistry.kt | 2 +- feedbacktree/src/main/res/values/strings.xml | 1 - 18 files changed, 202 insertions(+), 166 deletions(-) delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml rename {feedbacktree/src/main/java/com/feedbacktree/flow/ui/core => app/src/main/java/com/feedbacktree/tutorials/flows}/modals/ViewModal.kt (68%) rename {feedbacktree/src/main/java/com/feedbacktree/flow/ui/views => app/src/main/java/com/feedbacktree/tutorials/flows}/modals/ViewModalDialogBinding.kt (76%) diff --git a/.gitignore b/.gitignore index bf68da48..fd4effd7 100644 --- a/.gitignore +++ b/.gitignore @@ -65,4 +65,4 @@ fastlane/screenshots fastlane/test_output fastlane/readme.md public -/.idea/artifacts/core_android_0_13_1.xml +/.idea/ diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 7e9fd42c..00000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 890f0f0c..00000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/java/com/feedbacktree/tutorials/AppViewRegistry.kt b/app/src/main/java/com/feedbacktree/tutorials/AppViewRegistry.kt index 4544f69e..408b1978 100644 --- a/app/src/main/java/com/feedbacktree/tutorials/AppViewRegistry.kt +++ b/app/src/main/java/com/feedbacktree/tutorials/AppViewRegistry.kt @@ -12,10 +12,11 @@ import com.feedbacktree.tutorials.flows.counter.CounterLayoutBinder import com.feedbacktree.tutorials.flows.login.LoginLayoutBinder import com.feedbacktree.tutorials.flows.modals.CovidInfoLayoutBinder import com.feedbacktree.tutorials.flows.modals.ModalsExampleLayoutBinder +import com.feedbacktree.tutorials.flows.modals.ViewModalDialogBinding import com.feedbacktree.tutorials.flows.tutorialsroot.TutorialsLayoutBinder private val dialogRegistry = - DialogRegistry.registry(R.style.FTDialogTheme) + DialogRegistry.registry(R.style.FTDialogTheme) + ViewModalDialogBinding() val appViewRegistry = ViewRegistry( ModalContainer.Binding(dialogRegistry), diff --git a/app/src/main/java/com/feedbacktree/tutorials/flows/counter/CounterFlow.kt b/app/src/main/java/com/feedbacktree/tutorials/flows/counter/CounterFlow.kt index 0fe97885..803bfde8 100644 --- a/app/src/main/java/com/feedbacktree/tutorials/flows/counter/CounterFlow.kt +++ b/app/src/main/java/com/feedbacktree/tutorials/flows/counter/CounterFlow.kt @@ -11,23 +11,25 @@ import com.feedbacktree.flow.core.endFlow import kotlin.math.max val CounterFlow = Flow( + id = "CounterFlow", initialState = { State(counter = 0) }, stepper = { state, event -> when (event) { Event.Increment -> state.copy( counter = state.counter + 1 ).advance() + Event.Decrement -> state.copy( counter = max(0, state.counter - 1) ).advance() + Event.BackPressed -> endFlow() } }, - feedbacks = listOf(), - render = { state, context -> - CounterScreen(state, context.sink) - } -) + feedbacks = listOf() +) { state, context -> + CounterScreen(state, context.sink) +} data class State( val counter: Int diff --git a/app/src/main/java/com/feedbacktree/tutorials/flows/login/LoginFlow.kt b/app/src/main/java/com/feedbacktree/tutorials/flows/login/LoginFlow.kt index 1a11b761..9867eef5 100644 --- a/app/src/main/java/com/feedbacktree/tutorials/flows/login/LoginFlow.kt +++ b/app/src/main/java/com/feedbacktree/tutorials/flows/login/LoginFlow.kt @@ -10,6 +10,7 @@ import com.feedbacktree.tutorials.managers.AuthenticationManager import io.reactivex.Observable val LoginFlow = Flow( + id = "LoginFlow", initialState = { lastEmailUsed -> State(email = lastEmailUsed) }, stepper = { state, event -> when (event) { @@ -23,14 +24,14 @@ val LoginFlow = Flow( state.copy(isLoggingIn = false).advance() } } + Event.BackPressed -> endFlowWith(LoginFlowOutput.Aborted) } }, - feedbacks = listOf(loginFeedback()), - render = { state, context -> - return@Flow LoginScreen(state, context.sink) - } -) + feedbacks = listOf(loginFeedback()) +) { state, context -> + return@Flow LoginScreen(state, context.sink) +} data class State( val email: String = "", diff --git a/app/src/main/java/com/feedbacktree/tutorials/flows/modals/ModalsFlow.kt b/app/src/main/java/com/feedbacktree/tutorials/flows/modals/ModalsFlow.kt index 9eaa8027..cad06ded 100644 --- a/app/src/main/java/com/feedbacktree/tutorials/flows/modals/ModalsFlow.kt +++ b/app/src/main/java/com/feedbacktree/tutorials/flows/modals/ModalsFlow.kt @@ -8,10 +8,12 @@ package com.feedbacktree.tutorials.flows.modals import com.feedbacktree.flow.core.Flow import com.feedbacktree.flow.core.advance import com.feedbacktree.flow.core.endFlow -import com.feedbacktree.flow.ui.core.modals.* +import com.feedbacktree.flow.ui.core.modals.AlertModal +import com.feedbacktree.flow.ui.core.modals.ModalContainerScreen import com.feedbacktree.tutorials.flows.login.LoginFlow val ModalsFlow = Flow>( + id = "ModalsFlow", initialState = { State.Idle }, stepper = { _, event -> when (event) { @@ -24,48 +26,51 @@ val ModalsFlow = Flow>( Event.BackClicked -> endFlow() } }, - feedbacks = listOf(), - render = { state, context -> - ModalContainerScreen( - baseScreen = ModalsScreen(sink = context.sink), - modal = when (state) { - State.Idle -> null - State.ShowingAlertModal -> AlertModal(context.sink) { - title = "COVID Screening" - message = "Do you have fever?" - positive("Yes", Event.DismissAlert("Yes Clicked")) - negative("No", Event.DismissAlert("No Clicked")) - cancelEvent = Event.DismissAlert("BackClicked") - } - State.ShowingAlertModalWithCustomView -> AlertModal(context.sink) { - title = "COVID Info" - positive("Dismiss", Event.DismissAlert("Dismiss Clicked")) - contentScreen = CovidInfoScreen() - cancelEvent = Event.DismissAlert("BackClicked") - } - State.ShowingFullScreenModal -> { - val contentScreen = - context.renderChild(input = "", flow = LoginFlow, onResult = { - context.sendEvent(Event.FullScreenFlowCompleted) - }) - // Wrap the screen produced by the LoginFlow in a FullScreenModal - FullScreenModal(contentScreen) - } - State.ShowingCustomSizeModal -> { - val contentScreen = - context.renderChild(input = "", flow = LoginFlow, onResult = { - context.sendEvent(Event.FullScreenFlowCompleted) - }) - ViewModal( - contentScreen, - widthLayout = Layout.Percentage(55), - heightLayout = Layout.Percentage(55) - ) - } + feedbacks = listOf() +) { state, context -> + ModalContainerScreen( + baseScreen = ModalsScreen(sink = context.sink), + modal = when (state) { + State.Idle -> null + State.ShowingAlertModal -> AlertModal(context.sink) { + title = "COVID Screening" + message = "Do you have fever?" + positive("Yes", Event.DismissAlert("Yes Clicked")) + negative("No", Event.DismissAlert("No Clicked")) + cancelEvent = Event.DismissAlert("BackClicked") } - ) - } -) + + State.ShowingAlertModalWithCustomView -> AlertModal(context.sink) { + title = "COVID Info" + positive("Dismiss", Event.DismissAlert("Dismiss Clicked")) + contentScreen = CovidInfoScreen() + cancelEvent = Event.DismissAlert("BackClicked") + } + + State.ShowingFullScreenModal -> { + val contentScreen = + context.renderChild(input = "", flow = LoginFlow, onResult = { + context.sendEvent(Event.FullScreenFlowCompleted) + }) + // Wrap the screen produced by the LoginFlow in a FullScreenModal + FullScreenModal(contentScreen) + } + + State.ShowingCustomSizeModal -> { + val contentScreen = + context.renderChild(input = "", flow = LoginFlow, onResult = { + context.sendEvent(Event.FullScreenFlowCompleted) + }) + ViewModal( + contentScreen, + widthLayout = Layout.Percentage(55), + heightLayout = Layout.Percentage(55), + roundCorners = false + ) + } + } + ) +} enum class State { diff --git a/feedbacktree/src/main/java/com/feedbacktree/flow/ui/core/modals/ViewModal.kt b/app/src/main/java/com/feedbacktree/tutorials/flows/modals/ViewModal.kt similarity index 68% rename from feedbacktree/src/main/java/com/feedbacktree/flow/ui/core/modals/ViewModal.kt rename to app/src/main/java/com/feedbacktree/tutorials/flows/modals/ViewModal.kt index 8ee89751..511e38cb 100644 --- a/feedbacktree/src/main/java/com/feedbacktree/flow/ui/core/modals/ViewModal.kt +++ b/app/src/main/java/com/feedbacktree/tutorials/flows/modals/ViewModal.kt @@ -1,25 +1,31 @@ /* - * Created by eliek on 9/26/2019 - * Copyright (c) 2019 eliekarouz. All rights reserved. + * Created by eliek on 8/9/2023 + * Copyright (c) 2023 eliekarouz. All rights reserved. */ @file:Suppress("FunctionName") -package com.feedbacktree.flow.ui.core.modals +package com.feedbacktree.tutorials.flows.modals import androidx.annotation.ColorInt import com.feedbacktree.flow.ui.core.Compatible +import com.feedbacktree.flow.ui.core.modals.Modal class ViewModal( val content: ScreenT, val widthLayout: Layout, val heightLayout: Layout, - @ColorInt val backgroundColor: Int? = null + @ColorInt val backgroundColor: Int? = null, + val roundCorners: Boolean? = null ) : Modal, Compatible { - constructor(content: ScreenT) : this( + constructor( + content: ScreenT, + roundCorners: Boolean?, + ) : this( content, widthLayout = Layout.Wrap, - heightLayout = Layout.Wrap + heightLayout = Layout.Wrap, + roundCorners = roundCorners ) override val compatibilityKey: String @@ -37,8 +43,8 @@ sealed class Layout { } } -fun ScreenT.asModal(): ViewModal { - return ViewModal(content = this) +fun ScreenT.asModal(roundCorners: Boolean? = null): ViewModal { + return ViewModal(content = this, roundCorners = roundCorners) } @@ -52,7 +58,7 @@ fun FullScreenModal(content: ScreenT): ViewModal { return ViewModal( content = content, widthLayout = Layout.FullScreen, - heightLayout = Layout.FullScreen + heightLayout = Layout.FullScreen, ) } diff --git a/feedbacktree/src/main/java/com/feedbacktree/flow/ui/views/modals/ViewModalDialogBinding.kt b/app/src/main/java/com/feedbacktree/tutorials/flows/modals/ViewModalDialogBinding.kt similarity index 76% rename from feedbacktree/src/main/java/com/feedbacktree/flow/ui/views/modals/ViewModalDialogBinding.kt rename to app/src/main/java/com/feedbacktree/tutorials/flows/modals/ViewModalDialogBinding.kt index a95cdf97..26cdd018 100644 --- a/feedbacktree/src/main/java/com/feedbacktree/flow/ui/views/modals/ViewModalDialogBinding.kt +++ b/app/src/main/java/com/feedbacktree/tutorials/flows/modals/ViewModalDialogBinding.kt @@ -1,29 +1,36 @@ /* - * Created by eliek on 9/26/2019 - * Copyright (c) 2019 eliekarouz. All rights reserved. + * Created by eliek on 8/11/2023 + * Copyright (c) 2023 eliekarouz. All rights reserved. */ -package com.feedbacktree.flow.ui.views.modals +package com.feedbacktree.tutorials.flows.modals import android.app.Dialog import android.content.Context +import android.content.res.ColorStateList +import android.content.res.Resources import android.graphics.Color import android.graphics.Point import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.GradientDrawable import android.util.TypedValue -import android.view.* +import android.view.Display +import android.view.Gravity +import android.view.KeyEvent +import android.view.View +import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import android.widget.FrameLayout import androidx.core.content.ContextCompat import com.feedbacktree.R -import com.feedbacktree.flow.ui.core.modals.Layout -import com.feedbacktree.flow.ui.core.modals.ViewModal import com.feedbacktree.flow.ui.views.core.HandlesBack import com.feedbacktree.flow.ui.views.core.ViewRegistry import com.feedbacktree.flow.ui.views.core.disposeScreenBinding import com.feedbacktree.flow.ui.views.core.showScreen -import com.feedbacktree.flow.utils.logAndShow +import com.feedbacktree.flow.ui.views.modals.DialogBinding +import com.feedbacktree.flow.ui.views.modals.DialogRef +import com.feedbacktree.flow.ui.views.modals.DialogRegistry import com.feedbacktree.flow.utils.windowManager import kotlin.reflect.KClass @@ -70,7 +77,7 @@ class ViewModalDialogBinding( } setContentView(fullWindowPanel) - logAndShow("FullScreen") + show() window?.setBackgroundDrawable(ColorDrawable(ContextCompat.getColor(context, R.color.dim))) @@ -79,14 +86,28 @@ class ViewModalDialogBinding( val contentPanel = FrameLayout(context) // Setting backgroundColor of the panel ideally with using the windowBackground - if (viewModal.backgroundColor != null) { - contentPanel.setBackgroundColor(viewModal.backgroundColor) + val panelBackgroundColor = if (viewModal.backgroundColor != null) { + viewModal.backgroundColor } else { val typedValue = TypedValue() context.theme.resolveAttribute(android.R.attr.windowBackground, typedValue, true) if (typedValue.type in TypedValue.TYPE_FIRST_COLOR_INT..TypedValue.TYPE_LAST_COLOR_INT) { - contentPanel.setBackgroundColor(typedValue.data) + typedValue.data + } else null + } + + if (viewModal.roundCorners ?: !viewModal.hasFullScreenDimension()) { + contentPanel.background = GradientDrawable().apply { + panelBackgroundColor?.also { color = ColorStateList.valueOf(it) } + cornerRadius = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + 20f, + Resources.getSystem().displayMetrics + ) } + contentPanel.clipToOutline = true + } else if (panelBackgroundColor != null) { + contentPanel.setBackgroundColor(panelBackgroundColor) } // Adjusting the panel size @@ -168,4 +189,11 @@ private fun layoutParams(layout: Layout, screenDimension: Int, scale: Float): In is Layout.DPs -> (layout.dps * scale).toInt() is Layout.Percentage -> (screenDimension.toFloat() * layout.percentage.toFloat() * 0.01).toInt() } +} + +private fun ViewModal.hasFullScreenDimension(): Boolean { + return widthLayout == Layout.FullScreen || + heightLayout == Layout.FullScreen || + (widthLayout is Layout.Percentage && widthLayout.percentage == 100) || + (heightLayout is Layout.Percentage && heightLayout.percentage == 100) } \ No newline at end of file diff --git a/app/src/main/java/com/feedbacktree/tutorials/flows/phonenumber/PhoneNumberRegistrationFlow.kt b/app/src/main/java/com/feedbacktree/tutorials/flows/phonenumber/PhoneNumberRegistrationFlow.kt index 64c6b69d..d775c3de 100644 --- a/app/src/main/java/com/feedbacktree/tutorials/flows/phonenumber/PhoneNumberRegistrationFlow.kt +++ b/app/src/main/java/com/feedbacktree/tutorials/flows/phonenumber/PhoneNumberRegistrationFlow.kt @@ -8,9 +8,13 @@ package com.feedbacktree.tutorials.flows.phonenumber import com.feedbacktree.flow.core.Flow import com.feedbacktree.flow.core.advance import com.feedbacktree.flow.core.endFlow -import com.feedbacktree.tutorials.flows.phonenumber.State.Progress.* +import com.feedbacktree.tutorials.flows.phonenumber.State.Progress.EnteringNumber +import com.feedbacktree.tutorials.flows.phonenumber.State.Progress.EnteringRegistrationCode +import com.feedbacktree.tutorials.flows.phonenumber.State.Progress.Registering +import com.feedbacktree.tutorials.flows.phonenumber.State.Progress.SendingCode val PhoneNumberRegistrationFlow = Flow( + id = "PhoneNumberRegistrationFlow", initialState = { State( phoneNumber = "", @@ -33,25 +37,27 @@ val PhoneNumberRegistrationFlow = Flow( is Event.ReceivedRegistrationResponse -> state.copy( progress = EnteringRegistrationCode ).advance() + is Event.EnteredRegistrationCode -> state.copy( registrationCode = event.code ).advance() + Event.ClickedValidateCode -> state.copy( progress = Registering ).advance() + is Event.ReceivedValidationResponse -> endFlow() } }, - feedbacks = listOf(), - render = { state, context -> - when (state.progress) { - EnteringNumber -> EnterPhoneScreen(state, context.sink) - SendingCode -> LoadingScreen(message = "Sending Code", context.sink) - EnteringRegistrationCode -> EnterRegistrationCodeScreen(state, context.sink) - Registering -> LoadingScreen(message = "Validating Code", context.sink) - } + feedbacks = listOf() +) { state, context -> + when (state.progress) { + EnteringNumber -> EnterPhoneScreen(state, context.sink) + SendingCode -> LoadingScreen(message = "Sending Code", context.sink) + EnteringRegistrationCode -> EnterRegistrationCodeScreen(state, context.sink) + Registering -> LoadingScreen(message = "Validating Code", context.sink) } -) +} data class State( diff --git a/app/src/main/java/com/feedbacktree/tutorials/flows/resources/ResoucesFlow.kt b/app/src/main/java/com/feedbacktree/tutorials/flows/resources/ResoucesFlow.kt index fd49da12..d33b7ca0 100644 --- a/app/src/main/java/com/feedbacktree/tutorials/flows/resources/ResoucesFlow.kt +++ b/app/src/main/java/com/feedbacktree/tutorials/flows/resources/ResoucesFlow.kt @@ -14,6 +14,7 @@ import io.reactivex.Observable import java.util.concurrent.TimeUnit val ResourcesFlow = Flow( + id = "ResourcesFlow", initialState = { State( fileResources = listOf( @@ -28,6 +29,7 @@ val ResourcesFlow = Flow( is Event.UserRequestedResource -> state.copy( resourceUrlsToDownload = state.resourceUrlsToDownload + event.fileResource.url ).advance() + is Event.LoadedResource -> state.copy( resourceUrlsToDownload = state.resourceUrlsToDownload - event.url, fileResources = state.fileResources.update( @@ -37,11 +39,10 @@ val ResourcesFlow = Flow( ).advance() } }, - feedbacks = listOf(downloadResourcesFeedback()), - render = { state, context -> - FilesScreen(state, context.sink) - } -) + feedbacks = listOf(downloadResourcesFeedback()) +) { state, context -> + FilesScreen(state, context.sink) +} data class FileResource( val url: String, diff --git a/app/src/main/java/com/feedbacktree/tutorials/flows/tutorialsroot/TutorialsFlow.kt b/app/src/main/java/com/feedbacktree/tutorials/flows/tutorialsroot/TutorialsFlow.kt index 6b6d9d86..39974110 100644 --- a/app/src/main/java/com/feedbacktree/tutorials/flows/tutorialsroot/TutorialsFlow.kt +++ b/app/src/main/java/com/feedbacktree/tutorials/flows/tutorialsroot/TutorialsFlow.kt @@ -12,6 +12,7 @@ import com.feedbacktree.tutorials.flows.login.LoginFlow import com.feedbacktree.tutorials.flows.modals.ModalsFlow val TutorialsFlow = Flow( + id = "TutorialsFlow", initialState = { State() }, stepper = { state, event -> when (event) { @@ -19,22 +20,23 @@ val TutorialsFlow = Flow( Event.CompletedTutorial -> state.copy(selectedTutorial = null).advance() } }, - feedbacks = listOf(), - render = { state, context -> - when (state.selectedTutorial) { - null -> TutorialsScreen(state, context.sink) - Tutorial.Counter -> context.renderChild(CounterFlow, onResult = { - context.sendEvent(Event.CompletedTutorial) - }) - Tutorial.Login -> context.renderChild(input = "", flow = LoginFlow, onResult = { - context.sendEvent(Event.CompletedTutorial) - }) - Tutorial.Modals -> context.renderChild(ModalsFlow, onResult = { - context.sendEvent(Event.CompletedTutorial) - }) - } + feedbacks = listOf() +) { state, context -> + when (state.selectedTutorial) { + null -> TutorialsScreen(state, context.sink) + Tutorial.Counter -> context.renderChild(CounterFlow, onResult = { + context.sendEvent(Event.CompletedTutorial) + }) + + Tutorial.Login -> context.renderChild(input = "", flow = LoginFlow, onResult = { + context.sendEvent(Event.CompletedTutorial) + }) + + Tutorial.Modals -> context.renderChild(ModalsFlow, onResult = { + context.sendEvent(Event.CompletedTutorial) + }) } -) +} data class State( val tutorials: List = listOf( diff --git a/build.gradle.kts b/build.gradle.kts index 42ba9de3..6c32c6a4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,7 +17,7 @@ buildscript { allprojects { group = "com.github.eliekarouz.feedbacktree" - version = System.getenv("GITHUB_REF_NAME")?.takeIf { it.isNotEmpty() } ?: "0.14.0" + version = System.getenv("GITHUB_REF_NAME")?.takeIf { it.isNotEmpty() } ?: "0.16.0" repositories { google() diff --git a/feedbacktree/src/main/java/com/feedbacktree/flow/core/Flow.kt b/feedbacktree/src/main/java/com/feedbacktree/flow/core/Flow.kt index d7658880..8011a7cd 100644 --- a/feedbacktree/src/main/java/com/feedbacktree/flow/core/Flow.kt +++ b/feedbacktree/src/main/java/com/feedbacktree/flow/core/Flow.kt @@ -6,8 +6,23 @@ package com.feedbacktree.flow.core data class Flow( + val id: (InputT) -> String, val initialState: (InputT) -> StateT, val stepper: (StateT, EventT) -> Step, val feedbacks: List> = listOf(), val render: (StateT, RenderingContext) -> ScreenT -) \ No newline at end of file +) { + constructor( + id: String, + initialState: (InputT) -> StateT, + stepper: (StateT, EventT) -> Step, + feedbacks: List> = listOf(), + render: (StateT, RenderingContext) -> ScreenT + ) : this( + id = { id }, + initialState = initialState, + stepper = stepper, + feedbacks = feedbacks, + render = render, + ) +} \ No newline at end of file diff --git a/feedbacktree/src/main/java/com/feedbacktree/flow/core/FlowNode.kt b/feedbacktree/src/main/java/com/feedbacktree/flow/core/FlowNode.kt index 012a1840..feae25ac 100644 --- a/feedbacktree/src/main/java/com/feedbacktree/flow/core/FlowNode.kt +++ b/feedbacktree/src/main/java/com/feedbacktree/flow/core/FlowNode.kt @@ -42,7 +42,7 @@ class RenderingContext internal constructor( id: String? = null, onResult: (ChildOutputT) -> Unit ): ChildScreenT { - val flowId = id ?: flow.toString() + val flowId = id ?: flow.id(input) logVerbose("renderChild: $flowId, node = ${node.id}, node.children= ${node.children.size}") val existingNode = node.children.firstOrNull { it.id == flowId } diff --git a/feedbacktree/src/main/java/com/feedbacktree/flow/ui/views/WorkflowLayout.kt b/feedbacktree/src/main/java/com/feedbacktree/flow/ui/views/WorkflowLayout.kt index 2c931c5c..9b9bf827 100644 --- a/feedbacktree/src/main/java/com/feedbacktree/flow/ui/views/WorkflowLayout.kt +++ b/feedbacktree/src/main/java/com/feedbacktree/flow/ui/views/WorkflowLayout.kt @@ -24,8 +24,6 @@ import android.util.SparseArray import android.view.View import android.widget.FrameLayout import com.feedbacktree.flow.ui.views.core.* -import com.squareup.coordinators.Coordinator -import com.squareup.coordinators.Coordinators import io.reactivex.Observable import io.reactivex.disposables.Disposable @@ -84,7 +82,7 @@ class WorkflowLayout( addView(newView) } - override fun onSaveInstanceState(): Parcelable? { + override fun onSaveInstanceState(): Parcelable { return SavedState( super.onSaveInstanceState()!!, SparseArray().also { array -> showing?.saveHierarchyState(array) } @@ -139,22 +137,34 @@ class WorkflowLayout( source: Observable, update: (S) -> Unit ) { - Coordinators.bind(this) { - object : Coordinator() { - var sub: Disposable? = null - - override fun attach(view: View) { - sub = source.subscribe { screen -> update(screen) } - } - - override fun detach(view: View) { - sub?.let { - showing?.disposeScreenBinding() - it.dispose() - sub = null - } + var sub: Disposable? = null + isAttachedToWindowBinder { isAttached -> + println("WorkflowLayout is Attached to window $isAttached") + if (isAttached) { + sub = source.subscribe { screen -> update(screen) } + } else { + sub?.let { + showing?.disposeScreenBinding() + it.dispose() + sub = null } } } } + + // callback is called with initial state and on subsequent updates + private fun View.isAttachedToWindowBinder( + onAttached: (Boolean) -> Unit + ) { + addOnAttachStateChangeListener(object : OnAttachStateChangeListener { + override fun onViewAttachedToWindow(v: View) { + onAttached(true) + } + + override fun onViewDetachedFromWindow(v: View) { + onAttached(false) + } + }) + onAttached(isAttachedToWindow) + } } diff --git a/feedbacktree/src/main/java/com/feedbacktree/flow/ui/views/modals/DialogRegistry.kt b/feedbacktree/src/main/java/com/feedbacktree/flow/ui/views/modals/DialogRegistry.kt index 14e1f7a6..ead7872e 100644 --- a/feedbacktree/src/main/java/com/feedbacktree/flow/ui/views/modals/DialogRegistry.kt +++ b/feedbacktree/src/main/java/com/feedbacktree/flow/ui/views/modals/DialogRegistry.kt @@ -79,7 +79,7 @@ class DialogRegistry private constructor( ): DialogRegistry { return DialogRegistry( AlertDialogBinding(dialogThemeResId = dialogThemeResId) - ) + ViewModalDialogBinding() + ) } } } \ No newline at end of file diff --git a/feedbacktree/src/main/res/values/strings.xml b/feedbacktree/src/main/res/values/strings.xml index b7869a65..85420055 100644 --- a/feedbacktree/src/main/res/values/strings.xml +++ b/feedbacktree/src/main/res/values/strings.xml @@ -1,3 +1,2 @@ - feedbacktree From 4293219f6544739e81626cbec11b001f8690f39d Mon Sep 17 00:00:00 2001 From: Elie Karouz Date: Fri, 11 Aug 2023 09:14:41 +0300 Subject: [PATCH 2/2] Remove deprecated methods --- .../flow/ui/views/FlowFragment.kt | 4 +- .../flow/ui/views/LayoutBinder.kt | 2 - .../flow/ui/views/LayoutRunner.kt | 143 ------------------ 3 files changed, 2 insertions(+), 147 deletions(-) delete mode 100644 feedbacktree/src/main/java/com/feedbacktree/flow/ui/views/LayoutRunner.kt diff --git a/feedbacktree/src/main/java/com/feedbacktree/flow/ui/views/FlowFragment.kt b/feedbacktree/src/main/java/com/feedbacktree/flow/ui/views/FlowFragment.kt index 2077fe11..ebd8b669 100644 --- a/feedbacktree/src/main/java/com/feedbacktree/flow/ui/views/FlowFragment.kt +++ b/feedbacktree/src/main/java/com/feedbacktree/flow/ui/views/FlowFragment.kt @@ -56,7 +56,7 @@ abstract class FlowFragment : Fragmen inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { + ): View { return WorkflowLayout(inflater.context) } @@ -85,7 +85,7 @@ abstract class FlowFragment : Fragmen /** * If your workflow needs to manage the back button, override [android.app.Activity.onBackPressed] - * and call this method, and have its views or [LayoutRunner]s use [HandlesBack]. + * and call this method, and have its views or [LayoutBinder]s use [HandlesBack] or `view.backPresses()`. * * e.g.: * diff --git a/feedbacktree/src/main/java/com/feedbacktree/flow/ui/views/LayoutBinder.kt b/feedbacktree/src/main/java/com/feedbacktree/flow/ui/views/LayoutBinder.kt index 960a7699..c5f963b8 100644 --- a/feedbacktree/src/main/java/com/feedbacktree/flow/ui/views/LayoutBinder.kt +++ b/feedbacktree/src/main/java/com/feedbacktree/flow/ui/views/LayoutBinder.kt @@ -3,8 +3,6 @@ * Copyright (c) 2020 eliekarouz. All rights reserved. */ -@file:Suppress("DEPRECATION") - package com.feedbacktree.flow.ui.views import android.content.Context diff --git a/feedbacktree/src/main/java/com/feedbacktree/flow/ui/views/LayoutRunner.kt b/feedbacktree/src/main/java/com/feedbacktree/flow/ui/views/LayoutRunner.kt deleted file mode 100644 index 4c461082..00000000 --- a/feedbacktree/src/main/java/com/feedbacktree/flow/ui/views/LayoutRunner.kt +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Created by eliek on 9/26/2019 - * Copyright (c) 2019 eliekarouz. All rights reserved. - */ - -package com.feedbacktree.flow.ui.views - -import android.content.Context -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.annotation.LayoutRes -import com.feedbacktree.flow.core.Feedback -import com.feedbacktree.flow.core.ObservableSchedulerContext -import com.feedbacktree.flow.ui.views.core.BuilderBinding -import com.feedbacktree.flow.ui.views.core.ViewBinding -import com.feedbacktree.flow.ui.views.core.ViewRegistry -import com.feedbacktree.flow.ui.views.core.bindShowScreen -import com.feedbacktree.flow.utils.logVerbose -import io.reactivex.Observable -import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.disposables.Disposable -import io.reactivex.subjects.BehaviorSubject -import kotlin.reflect.KClass - -/** - * (Experimental) - */ -@Deprecated("Deprecated in favor of LayoutBinder.create") -interface LayoutRunner { - - fun feedbacks(): List> - - class Binding - constructor( - override val type: KClass, - @LayoutRes private val layoutId: Int, - private val binderConstructor: (View, ViewRegistry) -> LayoutRunner, - private val sink: (ScreenT) -> (EventT) -> Unit - ) : ViewBinding { - override fun buildView( - registry: ViewRegistry, - initialScreen: ScreenT, - contextForNewView: Context, - container: ViewGroup? - ): View { - return LayoutInflater.from(container?.context ?: contextForNewView) - .cloneInContext(contextForNewView) - .inflate(layoutId, container, false) - .apply { - - val screenBehaviorSubject = BehaviorSubject.createDefault(initialScreen) - val mergedEvents by lazy { - // create the view - val layoutAttachable = binderConstructor.invoke(this, registry) - val feedbacks = layoutAttachable.feedbacks() - val events = feedbacks.map { - val observableSchedulerContext = ObservableSchedulerContext( - screenBehaviorSubject, - AndroidSchedulers.mainThread() - ) - it(observableSchedulerContext) - } - Observable.merge(events) - } - - var disposable: Disposable? = mergedEvents.subscribe { - sink(initialScreen).invoke(it) - } - logVerbose("LayoutBinder - Attached screen: $type") - - bindShowScreen( - initialScreen, - showScreen = { screen -> - screenBehaviorSubject.onNext(screen) - }, - disposeScreenBinding = { - logVerbose("LayoutBinder - Detached screen: $type") - disposable?.dispose() - disposable = null - } - ) - } - } - } - - companion object { - /** - * Creates a [ViewBinding] that inflates [layoutId] to show screens of type [ScreenT], - * using a [LayoutRunner] created by [constructor]. - */ - inline fun bind( - @LayoutRes layoutId: Int, - noinline constructor: (View, ViewRegistry) -> LayoutRunner, - noinline sink: (ScreenT) -> (EventT) -> Unit - ): ViewBinding = Binding( - type = ScreenT::class, - layoutId = layoutId, - binderConstructor = constructor, - sink = sink - ) - - /** - * Creates a [ViewBinding] that inflates [layoutId] to show screens of type [ScreenT], - * using a [LayoutRunner] created by [constructor]. - */ - inline fun bind( - @LayoutRes layoutId: Int, - noinline constructor: (View) -> LayoutRunner, - noinline sink: (ScreenT) -> (EventT) -> Unit - ): ViewBinding = - bind( - layoutId = layoutId, - constructor = { view, _ -> - constructor.invoke( - view - ) - }, - sink = sink - ) - - /** - * Creates a [ViewBinding] that inflates [layoutId] to "show" screens of type [ScreenT]. - * Handy for showing static views. - */ - inline fun bindStatic( - @LayoutRes layoutId: Int - ): ViewBinding = BuilderBinding( - type = ScreenT::class, - viewConstructor = { _, initialScreen, contextForNewView, container -> - LayoutInflater.from(container?.context ?: contextForNewView) - .cloneInContext(contextForNewView) - .inflate(layoutId, container, false).apply { - bindShowScreen(initialScreen, - showScreen = { }, - disposeScreenBinding = { } - ) - } - } - ) - - } -} \ No newline at end of file