Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b217ae0
chore: enable expanded StyleSharp analyzers and fix all violations
glennawatson Jun 23, 2026
62d6c1c
fix: resolve analyzer violations in android, Maui, and benchmark TFMs
glennawatson Jun 23, 2026
4bd0deb
fix: resolve analyzer violations in Apple platform TFMs
glennawatson Jun 23, 2026
cbb3b3d
chore(deps): bump StyleSharp.Analyzers to 3.13.3
glennawatson Jun 23, 2026
25f1418
chore: exclude ReactiveUI.Primitives analyzers from downstream flow
glennawatson Jun 23, 2026
bf05475
fix: resolve analyzer violations in AndroidX, Maui tests, and examples
glennawatson Jun 23, 2026
f2de70f
fix: clear remaining analyzer violations across the full slnx
glennawatson Jun 23, 2026
12d6eb7
test: cover ObservableAsPropertyHelper constructors, factories, dispo…
glennawatson Jun 24, 2026
6cea461
test: cover converter registries; remove dead CompatMixins
glennawatson Jun 24, 2026
a11bf9c
test: cover INPCObservableForProperty affinity overload, null type, a…
glennawatson Jun 24, 2026
d2112ea
refactor: collapse redundant ExpressionMixins.GetParent throw-arms in…
glennawatson Jun 24, 2026
b69d821
refactor: extract SkipFirstObserver to its own file and cover it dire…
glennawatson Jun 24, 2026
2f67799
test: cover Reflection property-chain failure paths (empty chain, nul…
glennawatson Jun 24, 2026
b1d2e6f
test: cover ViewModelViewHost no-view and non-View resolution throws
glennawatson Jun 24, 2026
8bf899b
refactor: deduplicate iOS/tvOS LinkerOverrides via shared uikit-commo…
glennawatson Jun 24, 2026
9f97959
test: cover RoutedViewHost invalidate-no-op and ReactiveShellContent …
glennawatson Jun 24, 2026
3c48150
test: cover ValidationBindingWpf constructor and Bind via a realized …
glennawatson Jun 24, 2026
0632980
test: add AutoDataTemplateBindingHook tests (template parse + Execute…
glennawatson Jun 24, 2026
e8db070
test: cover CreatesWinformsCommandBinding event-name and add/remove-h…
glennawatson Jun 24, 2026
a0c9cd8
test: cover RoutedControlHost view-contract observable and disposal
glennawatson Jun 24, 2026
8100eeb
test: fix TrySetValueToPropertyChain test to assert the writable-memb…
glennawatson Jun 24, 2026
e6c8099
fix: remove redundant null-conditionals flagged by SonarCloud
glennawatson Jun 24, 2026
05d48f7
refactor: extract AutoPersistCollection removal cleanup into DisposeA…
glennawatson Jun 24, 2026
34a58a1
refactor: share ActivityResultAwaiter across AndroidX activities
glennawatson Jun 24, 2026
f7aae16
feat: add ReactiveWindow and ReactiveTitleBar for MAUI
glennawatson Jun 24, 2026
65c95c0
fix: update .Reactive shim PublicAPI baselines for AndroidX Intent? a…
glennawatson Jun 24, 2026
44a2063
fix: resolve SonarCloud S2589/S2583 warnings on new code
glennawatson Jun 24, 2026
fcbc2b8
test: add comprehensive unit tests for ReactiveUI core mixins/extensions
glennawatson Jun 24, 2026
ecb65d1
test: enhance DependencyResolverMixinsTypeFactoryTests with additiona…
glennawatson Jun 24, 2026
f99dc47
test: exclude unreachable ReactiveProperty IObserver terminals from c…
glennawatson Jun 24, 2026
6fd9e7b
feat: add environment variable override for multicast address in Chat…
glennawatson Jun 24, 2026
18eb5f8
chore(examples): replace networked chat sample with a mock EFTPOS ter…
glennawatson Jun 24, 2026
ddc5e9b
test: stabilize Maui RoutedViewHost null-view-model test on net10
glennawatson Jun 24, 2026
70e6cfe
test: ensure WpfPropertyBinder queues are flushed before assertions
glennawatson Jun 24, 2026
b316c35
Observe PayCommand on UI thread; init WinForms
ChrisPulman Jun 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
215 changes: 139 additions & 76 deletions .editorconfig

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,17 @@ routing/collection/auto-persist helpers now live in a separate **`ReactiveUI.Rou
for the System.Reactive flavor), so core no longer depends on DynamicData. Add `ReactiveUI.Routing` if you use those
extensions.

### Analyzers are opt-in

The ReactiveUI packages reference `ReactiveUI.Primitives` with `ExcludeAssets="analyzers"`, so the analyzers that ship
inside ReactiveUI.Primitives **do not flow to your project** and will not run against your code just because you
installed ReactiveUI. We don't impose our analyzers on downstream consumers. If you want them, opt in explicitly by
adding a direct reference to `ReactiveUI.Primitives` (without excluding the analyzer assets), e.g.:

```xml
<PackageReference Include="ReactiveUI.Primitives" Version="x.y.z" />
```

[Core]: https://www.nuget.org/packages/ReactiveUI/

[CoreBadge]: https://img.shields.io/nuget/v/ReactiveUI.svg
Expand Down
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@

<!-- Platform-specific targets - always defined but only used on supported platforms -->
<ReactiveUIFrameworkTargets>net462;net472;net481</ReactiveUIFrameworkTargets>
<ReactiveUIMauiWindowsTargets>net9.0-windows10.0.19041.0;net10.0-windows10.0.19041.0;net11.0-windows10.0.19041.0</ReactiveUIMauiWindowsTargets>
<ReactiveUIMauiWindowsTargets>net10.0-windows10.0.19041.0;net11.0-windows10.0.19041.0</ReactiveUIMauiWindowsTargets>
<ReactiveUIWindowsTargets>net8.0-windows10.0.19041.0;net9.0-windows10.0.19041.0;net10.0-windows10.0.19041.0;net11.0-windows10.0.19041.0</ReactiveUIWindowsTargets>
<ReactiveUIWinUITargets>net8.0-windows10.0.19041.0;net9.0-windows10.0.19041.0;net10.0-windows10.0.19041.0;net11.0-windows10.0.19041.0</ReactiveUIWinUITargets>
<!-- net9 mobile (iOS/tvOS/macOS/maccatalyst) support ended 12 May 2026; target net10 only. -->
Expand Down
6 changes: 3 additions & 3 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

<PropertyGroup Label="Shared Version Variables">
<SplatVersion>20.0.0</SplatVersion>
<PrimitivesVersion>5.4.0</PrimitivesVersion>
<TUnitVersion>1.54.0</TUnitVersion>
<PrimitivesVersion>5.6.0</PrimitivesVersion>
<TUnitVersion>1.56.25</TUnitVersion>
<XamarinAndroidXLifecycleLiveDataVersion>2.10.0.2</XamarinAndroidXLifecycleLiveDataVersion>
</PropertyGroup>

Expand Down Expand Up @@ -78,7 +78,7 @@
</ItemGroup>

<ItemGroup Label="Analyzers">
<PackageVersion Include="StyleSharp.Analyzers" Version="3.11.2"/>
<PackageVersion Include="StyleSharp.Analyzers" Version="3.13.4"/>
<PackageVersion Include="Roslynator.Analyzers" Version="4.15.0"/>
<PackageVersion Include="SonarAnalyzer.CSharp" Version="10.27.0.140913"/>
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.3.0"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,15 @@ ReactiveUI.Reactive.AndroidX.ReactiveFragment<TViewModel>.ViewModel.get -> TView
ReactiveUI.Reactive.AndroidX.ReactiveFragment<TViewModel>.ViewModel.set -> void
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.Activated.get -> System.IObservable<System.Reactive.Unit>!
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.ActivityResult.get -> System.IObservable<(int requestCode, Android.App.Result result, Android.Content.Intent! intent)>!
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.ActivityResult.get -> System.IObservable<(int requestCode, Android.App.Result result, Android.Content.Intent? intent)>!
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.Changed.get -> System.IObservable<ReactiveUI.IReactivePropertyChangedEventArgs<ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity!>!>!
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.Changing.get -> System.IObservable<ReactiveUI.IReactivePropertyChangedEventArgs<ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity!>!>!
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.Deactivated.get -> System.IObservable<System.Reactive.Unit>!
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.PropertyChanged -> System.ComponentModel.PropertyChangedEventHandler?
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.PropertyChanging -> System.ComponentModel.PropertyChangingEventHandler?
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.ReactiveFragmentActivity() -> void
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.StartActivityForResultAsync(Android.Content.Intent! intent, int requestCode) -> System.Threading.Tasks.Task<(Android.App.Result result, Android.Content.Intent! intent)>!
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.StartActivityForResultAsync(System.Type! type, int requestCode) -> System.Threading.Tasks.Task<(Android.App.Result result, Android.Content.Intent! intent)>!
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.StartActivityForResultAsync(Android.Content.Intent! intent, int requestCode) -> System.Threading.Tasks.Task<(Android.App.Result result, Android.Content.Intent? intent)>!
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.StartActivityForResultAsync(System.Type! type, int requestCode) -> System.Threading.Tasks.Task<(Android.App.Result result, Android.Content.Intent? intent)>!
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.SuppressChangeNotifications() -> System.IDisposable!
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.ThrownExceptions.get -> System.IObservable<System.Exception!>!
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity<TViewModel>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,15 @@ ReactiveUI.Reactive.AndroidX.ReactiveFragment<TViewModel>.ViewModel.get -> TView
ReactiveUI.Reactive.AndroidX.ReactiveFragment<TViewModel>.ViewModel.set -> void
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.Activated.get -> System.IObservable<System.Reactive.Unit>!
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.ActivityResult.get -> System.IObservable<(int requestCode, Android.App.Result result, Android.Content.Intent! intent)>!
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.ActivityResult.get -> System.IObservable<(int requestCode, Android.App.Result result, Android.Content.Intent? intent)>!
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.Changed.get -> System.IObservable<ReactiveUI.IReactivePropertyChangedEventArgs<ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity!>!>!
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.Changing.get -> System.IObservable<ReactiveUI.IReactivePropertyChangedEventArgs<ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity!>!>!
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.Deactivated.get -> System.IObservable<System.Reactive.Unit>!
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.PropertyChanged -> System.ComponentModel.PropertyChangedEventHandler?
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.PropertyChanging -> System.ComponentModel.PropertyChangingEventHandler?
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.ReactiveFragmentActivity() -> void
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.StartActivityForResultAsync(Android.Content.Intent! intent, int requestCode) -> System.Threading.Tasks.Task<(Android.App.Result result, Android.Content.Intent! intent)>!
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.StartActivityForResultAsync(System.Type! type, int requestCode) -> System.Threading.Tasks.Task<(Android.App.Result result, Android.Content.Intent! intent)>!
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.StartActivityForResultAsync(Android.Content.Intent! intent, int requestCode) -> System.Threading.Tasks.Task<(Android.App.Result result, Android.Content.Intent? intent)>!
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.StartActivityForResultAsync(System.Type! type, int requestCode) -> System.Threading.Tasks.Task<(Android.App.Result result, Android.Content.Intent? intent)>!
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.SuppressChangeNotifications() -> System.IDisposable!
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity.ThrownExceptions.get -> System.IObservable<System.Exception!>!
ReactiveUI.Reactive.AndroidX.ReactiveFragmentActivity<TViewModel>
Expand Down
89 changes: 89 additions & 0 deletions src/ReactiveUI.AndroidX/ActivityResultAwaiter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) 2009-2026 .NET Foundation and Contributors. All rights reserved.
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for full license information.

using Android.Content;

#if REACTIVE_SHIM
namespace ReactiveUI.Reactive.AndroidX;
#else
namespace ReactiveUI.AndroidX;
#endif

/// <summary>
/// Awaits the first activity result that matches a request code, then completes the task and detaches. A fused,
/// allocation-light replacement for <c>Where(...).Select(...).FirstAsync().ToTask()</c>: it is its own observer,
/// settles exactly once, and unsubscribes on completion. Shared by the AppCompat and Fragment reactive activities.
/// </summary>
internal sealed class ActivityResultAwaiter
: IObserver<(int requestCode, Result result, Intent? intent)>, IDisposable
{
/// <summary>The request code this awaiter is waiting for.</summary>
private readonly int _requestCode;

/// <summary>Completion source backing the returned task.</summary>
private readonly TaskCompletionSource<(Result result, Intent? intent)> _completion = new();

/// <summary>Holds the source subscription so it can be torn down once settled.</summary>
private readonly OnceDisposable _subscription = new();

/// <summary>Guards against settling more than once.</summary>
private int _settled;

/// <summary>Initializes a new instance of the <see cref="ActivityResultAwaiter"/> class.</summary>
/// <param name="requestCode">The request code to await.</param>
private ActivityResultAwaiter(int requestCode) => _requestCode = requestCode;

/// <summary>Subscribes to the activity-result stream and returns a task for the first matching result.</summary>
/// <param name="source">The activity-result stream.</param>
/// <param name="requestCode">The request code to await.</param>
/// <returns>A task that completes with the result and intent of the first matching activity result.</returns>
public static Task<(Result result, Intent? intent)> Await(
IObservable<(int requestCode, Result result, Intent? intent)> source,
int requestCode)
{
var awaiter = new ActivityResultAwaiter(requestCode);
awaiter._subscription.Disposable = source.Subscribe(awaiter);
return awaiter._completion.Task;
}

/// <inheritdoc/>
public void OnNext((int requestCode, Result result, Intent? intent) value)
{
if (value.requestCode != _requestCode || Interlocked.Exchange(ref _settled, 1) != 0)
{
return;
}

_ = _completion.TrySetResult((value.result, value.intent));
Dispose();
}

/// <inheritdoc/>
public void OnError(Exception error)
{
if (Interlocked.Exchange(ref _settled, 1) != 0)
{
return;
}

_ = _completion.TrySetException(error);
Dispose();
}

/// <inheritdoc/>
public void OnCompleted()
{
if (Interlocked.Exchange(ref _settled, 1) != 0)
{
return;
}

_ = _completion.TrySetCanceled();
Dispose();
}

/// <inheritdoc/>
public void Dispose() => _subscription.Dispose();
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,15 @@ ReactiveUI.AndroidX.ReactiveFragment<TViewModel>.ViewModel.get -> TViewModel?
ReactiveUI.AndroidX.ReactiveFragment<TViewModel>.ViewModel.set -> void
ReactiveUI.AndroidX.ReactiveFragmentActivity
ReactiveUI.AndroidX.ReactiveFragmentActivity.Activated.get -> System.IObservable<ReactiveUI.Primitives.RxVoid>!
ReactiveUI.AndroidX.ReactiveFragmentActivity.ActivityResult.get -> System.IObservable<(int requestCode, Android.App.Result result, Android.Content.Intent! intent)>!
ReactiveUI.AndroidX.ReactiveFragmentActivity.ActivityResult.get -> System.IObservable<(int requestCode, Android.App.Result result, Android.Content.Intent? intent)>!
ReactiveUI.AndroidX.ReactiveFragmentActivity.Changed.get -> System.IObservable<ReactiveUI.IReactivePropertyChangedEventArgs<ReactiveUI.AndroidX.ReactiveFragmentActivity!>!>!
ReactiveUI.AndroidX.ReactiveFragmentActivity.Changing.get -> System.IObservable<ReactiveUI.IReactivePropertyChangedEventArgs<ReactiveUI.AndroidX.ReactiveFragmentActivity!>!>!
ReactiveUI.AndroidX.ReactiveFragmentActivity.Deactivated.get -> System.IObservable<ReactiveUI.Primitives.RxVoid>!
ReactiveUI.AndroidX.ReactiveFragmentActivity.PropertyChanged -> System.ComponentModel.PropertyChangedEventHandler?
ReactiveUI.AndroidX.ReactiveFragmentActivity.PropertyChanging -> System.ComponentModel.PropertyChangingEventHandler?
ReactiveUI.AndroidX.ReactiveFragmentActivity.ReactiveFragmentActivity() -> void
ReactiveUI.AndroidX.ReactiveFragmentActivity.StartActivityForResultAsync(Android.Content.Intent! intent, int requestCode) -> System.Threading.Tasks.Task<(Android.App.Result result, Android.Content.Intent! intent)>!
ReactiveUI.AndroidX.ReactiveFragmentActivity.StartActivityForResultAsync(System.Type! type, int requestCode) -> System.Threading.Tasks.Task<(Android.App.Result result, Android.Content.Intent! intent)>!
ReactiveUI.AndroidX.ReactiveFragmentActivity.StartActivityForResultAsync(Android.Content.Intent! intent, int requestCode) -> System.Threading.Tasks.Task<(Android.App.Result result, Android.Content.Intent? intent)>!
ReactiveUI.AndroidX.ReactiveFragmentActivity.StartActivityForResultAsync(System.Type! type, int requestCode) -> System.Threading.Tasks.Task<(Android.App.Result result, Android.Content.Intent? intent)>!
ReactiveUI.AndroidX.ReactiveFragmentActivity.SuppressChangeNotifications() -> System.IDisposable!
ReactiveUI.AndroidX.ReactiveFragmentActivity.ThrownExceptions.get -> System.IObservable<System.Exception!>!
ReactiveUI.AndroidX.ReactiveFragmentActivity<TViewModel>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,15 +51,15 @@ ReactiveUI.AndroidX.ReactiveFragment<TViewModel>.ViewModel.get -> TViewModel?
ReactiveUI.AndroidX.ReactiveFragment<TViewModel>.ViewModel.set -> void
ReactiveUI.AndroidX.ReactiveFragmentActivity
ReactiveUI.AndroidX.ReactiveFragmentActivity.Activated.get -> System.IObservable<ReactiveUI.Primitives.RxVoid>!
ReactiveUI.AndroidX.ReactiveFragmentActivity.ActivityResult.get -> System.IObservable<(int requestCode, Android.App.Result result, Android.Content.Intent! intent)>!
ReactiveUI.AndroidX.ReactiveFragmentActivity.ActivityResult.get -> System.IObservable<(int requestCode, Android.App.Result result, Android.Content.Intent? intent)>!
ReactiveUI.AndroidX.ReactiveFragmentActivity.Changed.get -> System.IObservable<ReactiveUI.IReactivePropertyChangedEventArgs<ReactiveUI.AndroidX.ReactiveFragmentActivity!>!>!
ReactiveUI.AndroidX.ReactiveFragmentActivity.Changing.get -> System.IObservable<ReactiveUI.IReactivePropertyChangedEventArgs<ReactiveUI.AndroidX.ReactiveFragmentActivity!>!>!
ReactiveUI.AndroidX.ReactiveFragmentActivity.Deactivated.get -> System.IObservable<ReactiveUI.Primitives.RxVoid>!
ReactiveUI.AndroidX.ReactiveFragmentActivity.PropertyChanged -> System.ComponentModel.PropertyChangedEventHandler?
ReactiveUI.AndroidX.ReactiveFragmentActivity.PropertyChanging -> System.ComponentModel.PropertyChangingEventHandler?
ReactiveUI.AndroidX.ReactiveFragmentActivity.ReactiveFragmentActivity() -> void
ReactiveUI.AndroidX.ReactiveFragmentActivity.StartActivityForResultAsync(Android.Content.Intent! intent, int requestCode) -> System.Threading.Tasks.Task<(Android.App.Result result, Android.Content.Intent! intent)>!
ReactiveUI.AndroidX.ReactiveFragmentActivity.StartActivityForResultAsync(System.Type! type, int requestCode) -> System.Threading.Tasks.Task<(Android.App.Result result, Android.Content.Intent! intent)>!
ReactiveUI.AndroidX.ReactiveFragmentActivity.StartActivityForResultAsync(Android.Content.Intent! intent, int requestCode) -> System.Threading.Tasks.Task<(Android.App.Result result, Android.Content.Intent? intent)>!
ReactiveUI.AndroidX.ReactiveFragmentActivity.StartActivityForResultAsync(System.Type! type, int requestCode) -> System.Threading.Tasks.Task<(Android.App.Result result, Android.Content.Intent? intent)>!
ReactiveUI.AndroidX.ReactiveFragmentActivity.SuppressChangeNotifications() -> System.IDisposable!
ReactiveUI.AndroidX.ReactiveFragmentActivity.ThrownExceptions.get -> System.IObservable<System.Exception!>!
ReactiveUI.AndroidX.ReactiveFragmentActivity<TViewModel>
Expand Down
77 changes: 0 additions & 77 deletions src/ReactiveUI.AndroidX/ReactiveAppCompatActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -145,81 +145,4 @@ protected override void Dispose(bool disposing)

base.Dispose(disposing);
}

/// <summary>
/// Awaits the first <see cref="ActivityResult"/> that matches a request code, then completes the task and
/// detaches. A fused, allocation-light replacement for <c>Where(...).Select(...).FirstAsync().ToTask()</c>:
/// it is its own observer, settles exactly once, and unsubscribes on completion.
/// </summary>
private sealed class ActivityResultAwaiter
: IObserver<(int requestCode, Result result, Intent? intent)>, IDisposable
{
/// <summary>The request code this awaiter is waiting for.</summary>
private readonly int _requestCode;

/// <summary>Completion source backing the returned task.</summary>
private readonly TaskCompletionSource<(Result result, Intent? intent)> _completion = new();

/// <summary>Holds the source subscription so it can be torn down once settled.</summary>
private readonly OnceDisposable _subscription = new();

/// <summary>Guards against settling more than once.</summary>
private int _settled;

/// <summary>Initializes a new instance of the <see cref="ActivityResultAwaiter"/> class.</summary>
/// <param name="requestCode">The request code to await.</param>
private ActivityResultAwaiter(int requestCode) => _requestCode = requestCode;

/// <summary>Subscribes to the activity-result stream and returns a task for the first matching result.</summary>
/// <param name="source">The activity-result stream.</param>
/// <param name="requestCode">The request code to await.</param>
/// <returns>A task that completes with the result and intent of the first matching activity result.</returns>
public static Task<(Result result, Intent? intent)> Await(
IObservable<(int requestCode, Result result, Intent? intent)> source,
int requestCode)
{
var awaiter = new ActivityResultAwaiter(requestCode);
awaiter._subscription.Disposable = source.Subscribe(awaiter);
return awaiter._completion.Task;
}

/// <inheritdoc/>
public void OnNext((int requestCode, Result result, Intent? intent) value)
{
if (value.requestCode != _requestCode || Interlocked.Exchange(ref _settled, 1) != 0)
{
return;
}

_completion.TrySetResult((value.result, value.intent));
Dispose();
}

/// <inheritdoc/>
public void OnError(Exception error)
{
if (Interlocked.Exchange(ref _settled, 1) != 0)
{
return;
}

_completion.TrySetException(error);
Dispose();
}

/// <inheritdoc/>
public void OnCompleted()
{
if (Interlocked.Exchange(ref _settled, 1) != 0)
{
return;
}

_completion.TrySetCanceled();
Dispose();
}

/// <inheritdoc/>
public void Dispose() => _subscription.Dispose();
}
}
Loading
Loading