question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

[Proposal] Create `StateContainer.ChangeStateWithAnimation()` API

See original GitHub issue

Feature name

Create StateContainer.ChangeStateWithAnimation() API

Link to discussion

https://github.com/CommunityToolkit/Maui/issues/942

Progress tracker

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:

  1. The developer can await the state change
  2. 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:closed
  • Created 7 months ago
  • Reactions:3
  • Comments:7 (5 by maintainers)

github_iconTop GitHub Comments

1reaction
brminnickcommented, Feb 24, 2023

Awesome!! Yea, we thought adding the ability to provide custom animations would be a nice tradeoff for us introducing a breaking change 😊

What does this mean for CanStateChange, is it unnecessary with the presence of the new API?

We’ll be keeping CanStateChange. I can see a scenario where a dev calls ChangeStateWithAnimation() from one thread while the ViewModel changes CurrentState on a background thread. And the only way to enable devs to alleviate a race condition in this scenario is to ensure they first ensure CanStateChange is true.

When changing state based on some logic in a ViewModel in an MVVM project…

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 an ICommand, feel free to open a Discussion! The good news is that we can add an ICommand state change animation API in a future release without any breaking changes.

0reactions
brminnickcommented, Feb 27, 2023

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:

public static async Task ChangeStateWithAnimation(
 		BindableObject bindable,
 		string? state,
 		Func<VisualElement, CancellationToken, Task>? beforeStateChange,
 		Func<VisualElement, CancellationToken, Task>? afterStateChange,
 		CancellationToken cancellationToken);
Read more comments on GitHub >

github_iconTop Results From Across the Web

[BUG] StateContainer crash app when state change too fast
Successfully merging a pull request may close this issue. StateContainer, Add ChangeStateWithAnimation() CommunityToolkit/Maui. 5 participants.
Read more >
Method: accounts.proposals.create | Buyer APIs
If successful, the response body contains a newly created instance of Proposal . Authorization Scopes. Requires the following OAuth scope: https ...
Read more >
REST Resource: accounts.proposals | Buyer APIs
A proposal is the unit of negotiation between a seller and a buyer and contains deals which are served. Note: You can't update,...
Read more >
Proposal for Major Change in API · Issue #371
framework to do its work. A JSON "manifest" describing all such components is created, through some process involving a scan of all .dll...
Read more >
Admin API Reference
Proposals Endpoint. The /proposals endpoint is used for creating new Admin action proposals via a POST request. Any actions created this way ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found