[Proposal] Create `StateContainer.ChangeStateWithAnimation()` API
See original GitHub issueFeature name
Create StateContainer.ChangeStateWithAnimation()
API
Link to discussion
https://github.com/CommunityToolkit/Maui/issues/942
Progress tracker
- Android Implementation
- iOS Implementation
- MacCatalyst Implementation
- Windows Implementation
- Tizen Implementation
- Unit Tests
- Samples
- Documentation: https://github.com/MicrosoftDocs/CommunityToolkit/pull/235
Summary
This Proposal changes how StateContainer
handles animations.
Current/Existing Behavior
When CurrentState
changes and ShouldAnimateOnStateChange is true
, the UI in StateContainer
will fade out (eg view.FadeTo(0)
), StateContainer
then updates UI to the new state, then the StateContainer
fades back in (eg view.FadeTo(1)
)
Proposed Behavior (Breaking Change)
This Proposal removes animations when the CurrentState
property changes, immediately (synchronously) updating the UI to the new state.
This Proposal creates a new API, Task ChangeStateWithAnimation()
that the developer can use to change the state using the default fade in/out animation or a custom animation.
Moving the animation logic into an asynchronous method brings two benefits:
- The developer can
await
the state change - The developer can use a try/catch block to catch any exceptions thrown when the state changes
public static Task ChangeStateWithAnimation(BindableObject bindable, string state, CancellationToken token);
public static Task ChangeStateWithAnimation(BindableObject bindable, string state, Animation beforeStateChange, Animation afterStateChange, CancellationToken token);
This Proposal removes StateContainer.ShouldAnimateOnStateChangeProperty
which is no longer necessary after adding ChangeStateWithAnimation()
.
Motivation
The existing implementation combines animations, which are asynchronous, with BindableProperty.PropertyChanged
which is synchronous. This leads to scenarios a StateContainerException
is thrown because a user can change CurrentState
while animations are running. This the expected behavior. However, because we are running “async over sync” (ie the animation logic is asynchronously running inside of the static async void OnCurrentStateChanged()
), developers are unable to catch the StateContainerException
in certain scenarios, such as https://github.com/CommunityToolkit/Maui/issues/806.
In https://github.com/CommunityToolkit/Maui/pull/811 we added CanStateChange
which developers can leverage to ensure that they only change states when CanStateChange
is true
, there are still scenarios where unhandled StateContainerException
can still occur such as https://github.com/CommunityToolkit/Maui/issues/942.
This Proposal ensures that we no longer throw unhandled Exceptions, that the developer can always catch StateContainerException
when it is thrown, and we introduce the ability for developers to customize the Animation.
Detailed Design
New APIs
public static class StateContainer
{
// ...
// Changes state using the default animation of `StateContainerController.FadeLayoutChildren`
public static Task ChangeStateWithAnimation(BindableObject bindable, string state, CancellationToken token);
// Changes state using custom animations
public static Task ChangeStateWithAnimation(BindableObject bindable, string state, Animation beforeStateChange, Animation afterStateChange, CancellationToken token);
// ...
}
Removed APIs
Remove StateContainer.ShouldAnimateOnStateChangeProperty
This API is no longer necessary now that the developer has two options for changing state:
- Option 1: Change the state immediately (synchronously) by changing
CurrentStateProperty
directly - Option 2: Change the state asynchronously by using
StateContainer.ChangeStateWithAnimation()
Usage Syntax
var currentStateBeforeAnimation = StateContainer.GetCurrentState(myStateContainer);
try
{
var cancellationTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(5));
await StateContainer.ChangeStateWithAnimation(myStateContainer, "IsLoading", cancellationTokenSource.Token);
}
catch(Exception)
{
// State change failed. Revert to previous state.
StateContainer.SetCurrentState(myStateContainer, currentStateBeforeAnimation);
}
Drawbacks
This Proposal does introduce a Breaking Change. However, given the numerous Discussions + Issues around StateContainer + unhandled exceptions, I strongly believe that this is the best path forward for our developers to be successful.
Alternatives
One alternative is to fail silently (ie not throw an exception). This is not recommended however, because it would leave the user’s UI in a partially-animated state.
Another option is to remove animations from StateContainer
without adding the new API, ChangeStateWithAnimation()
. After speaking with developers in the community, they prefer an animation to make their app look more polished and professional.
Unresolved Questions
Let’s update the sample app to ensure that the Issue noted in https://github.com/CommunityToolkit/Maui/issues/942 where the state changes based on the size of the window is fixed. At the very least, if an Exception is thrown, it should now be managed (ie it can be caught in a try/catch block).
Issue Analytics
- State:
- Created 7 months ago
- Reactions:3
- Comments:7 (5 by maintainers)
Awesome!! Yea, we thought adding the ability to provide custom animations would be a nice tradeoff for us introducing a breaking change 😊
We’ll be keeping
CanStateChange
. I can see a scenario where a dev callsChangeStateWithAnimation()
from one thread while the ViewModel changesCurrentState
on a background thread. And the only way to enable devs to alleviate a race condition in this scenario is to ensure they first ensureCanStateChange
istrue
.Vlad had an idea to add an
ICommand
, but we decided against implementing it in this Proposal because we plan on releasing v5.0.0 next week and we didn’t want to pack in too much and risk missing our release date.So for this Proposal, we’ll have to call
await ChangeStateWithAnimation()
in code-behind to animate the state changes, which isn’t ideal. But if you have an idea about how we could implement anICommand
, feel free to open a Discussion! The good news is that we can add anICommand
state change animation API in a future release without any breaking changes.Thanks Kym! The animation is applied equally to all views in the Layout.
We did add a second API where folks can customize the Animation for each
VisualElement
in the Layout: