Skip to content

Commit

Permalink
Feature Add AndroidX with new packages (#3889)
Browse files Browse the repository at this point in the history
<!-- Please be sure to read the
[Contribute](https://github.com/reactiveui/reactiveui#contribute)
section of the README -->

**What kind of change does this PR introduce?**
<!-- Bug fix, feature, docs update, ... -->

Feature

**What is the current behavior?**
<!-- You can also link to an open issue here. -->

#3851

**What is the new behavior?**
<!-- If this is a feature change -->

Functionality ported from the Mono Android TFM version that went out of
support.
Only supports Net 8.0 plus with the latest packages released on 29th
August 2024

**What might this PR break?**

No support for older targets if upgrading from the legacy package.

**Please check if the PR fulfills these requirements**
- [ ] Tests for the changes have been added (for bug fixes / features)
- [ ] Docs have been added / updated (for bug fixes / features)

**Other information**:
  • Loading branch information
ChrisPulman authored Sep 2, 2024
1 parent 60b9d48 commit de5f3fe
Show file tree
Hide file tree
Showing 24 changed files with 1,343 additions and 4 deletions.
5 changes: 5 additions & 0 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@
<PackageVersion Include="xunit.runner.console" Version="2.9.0" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageVersion Include="Xunit.StaFact" Version="1.1.11" />
<PackageVersion Include="Xamarin.AndroidX.Core" Version="1.13.1.4" />
<PackageVersion Include="Xamarin.AndroidX.Preference" Version="1.2.1.9" />
<PackageVersion Include="Xamarin.AndroidX.Legacy.Support.Core.UI" Version="1.0.0.29" />
<PackageVersion Include="Xamarin.Google.Android.Material" Version="1.11.0.2" />
<PackageVersion Include="Xamarin.AndroidX.Lifecycle.LiveData" Version="2.8.4.1" />
</ItemGroup>
<ItemGroup Condition="'$(UseMaui)' != 'true'">
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="1.5.240802000" />
Expand Down
49 changes: 49 additions & 0 deletions src/ReactiveUI.AndroidX/ControlFetcherMixin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// Copyright (c) 2024 .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.Views;

using static ReactiveUI.ControlFetcherMixin;

using Fragment = AndroidX.Fragment.App.Fragment;

namespace ReactiveUI.AndroidX;

/// <summary>
/// ControlFetcherMixin helps you automatically wire-up Activities and
/// Fragments via property names, similar to Butter Knife, as well as allows
/// you to fetch controls manually.
/// </summary>
public static class ControlFetcherMixin
{
/// <summary>
/// Wires a control to a property.
/// This should be called in the Fragment's OnCreateView, with the newly inflated layout.
/// </summary>
/// <param name="fragment">The fragment.</param>
/// <param name="inflatedView">The inflated view.</param>
/// <param name="resolveMembers">The resolve members.</param>
public static void WireUpControls(this Fragment fragment, View inflatedView, ResolveStrategy resolveMembers = ResolveStrategy.Implicit)
{
ArgumentNullException.ThrowIfNull(fragment);

foreach (var member in fragment.GetWireUpMembers(resolveMembers))
{
try
{
// Find the android control with the same name from the view
var view = inflatedView.GetControl(fragment.GetType().Assembly, member.GetResourceName());

// Set the activity field's value to the view with that identifier
member.SetValue(fragment, view);
}
catch (Exception ex)
{
throw new
MissingFieldException("Failed to wire up the Property " + member.Name + " to a View in your layout with a corresponding identifier", ex);
}
}
}
}
16 changes: 16 additions & 0 deletions src/ReactiveUI.AndroidX/GlobalUsings.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) 2024 .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.

global using global::Splat;
global using global::System;
global using global::System.Collections.Generic;
global using global::System.ComponentModel;
global using global::System.Diagnostics.CodeAnalysis;
global using global::System.Linq;
global using global::System.Reactive;
global using global::System.Reactive.Linq;
global using global::System.Reactive.Subjects;
global using global::System.Reactive.Threading.Tasks;
global using global::System.Threading.Tasks;
161 changes: 161 additions & 0 deletions src/ReactiveUI.AndroidX/ReactiveAppCompatActivity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright (c) 2024 .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.App;
using Android.Content;
using Android.Runtime;
using AndroidX.AppCompat.App;

namespace ReactiveUI.AndroidX;

/// <summary>
/// This is an Activity that is both an Activity and has ReactiveObject powers
/// (i.e. you can call RaiseAndSetIfChanged).
/// </summary>
public class ReactiveAppCompatActivity : AppCompatActivity, IReactiveObject, IReactiveNotifyPropertyChanged<ReactiveAppCompatActivity>, IHandleObservableErrors
{
private readonly Subject<Unit> _activated = new();
private readonly Subject<Unit> _deactivated = new();
private readonly Subject<(int requestCode, Result result, Intent? intent)> _activityResult = new();

/// <summary>
/// Initializes a new instance of the <see cref="ReactiveAppCompatActivity" /> class.
/// </summary>
protected ReactiveAppCompatActivity()
{
}

/// <summary>
/// Initializes a new instance of the <see cref="ReactiveAppCompatActivity" /> class.
/// </summary>
/// <param name="handle">The handle.</param>
/// <param name="ownership">The ownership.</param>
protected ReactiveAppCompatActivity(in IntPtr handle, JniHandleOwnership ownership)
: base(handle, ownership)
{
}

/// <inheritdoc/>
public event PropertyChangingEventHandler? PropertyChanging;

/// <inheritdoc/>
public event PropertyChangedEventHandler? PropertyChanged;

/// <inheritdoc/>
public IObservable<IReactivePropertyChangedEventArgs<ReactiveAppCompatActivity>> Changing => this.GetChangingObservable();

/// <inheritdoc/>
public IObservable<IReactivePropertyChangedEventArgs<ReactiveAppCompatActivity>> Changed => this.GetChangedObservable();

/// <inheritdoc/>
public IObservable<Exception> ThrownExceptions => this.GetThrownExceptionsObservable();

/// <summary>
/// Gets a signal when activated.
/// </summary>
/// <value>
/// The activated.
/// </value>
public IObservable<Unit> Activated => _activated.AsObservable();

/// <summary>
/// Gets a signal when deactivated.
/// </summary>
/// <value>
/// The deactivated.
/// </value>
public IObservable<Unit> Deactivated => _deactivated.AsObservable();

/// <summary>
/// Gets the activity result.
/// </summary>
/// <value>
/// The activity result.
/// </value>
public IObservable<(int requestCode, Result result, Intent? intent)> ActivityResult => _activityResult.AsObservable();

/// <inheritdoc/>
void IReactiveObject.RaisePropertyChanging(PropertyChangingEventArgs args) => PropertyChanging?.Invoke(this, args);

/// <inheritdoc/>
void IReactiveObject.RaisePropertyChanged(PropertyChangedEventArgs args) => PropertyChanged?.Invoke(this, args);

/// <inheritdoc/>
public IDisposable SuppressChangeNotifications() => IReactiveObjectExtensions.SuppressChangeNotifications(this);

/// <summary>
/// Starts the activity for result asynchronously.
/// </summary>
/// <param name="intent">The intent.</param>
/// <param name="requestCode">The request code.</param>
/// <returns>A task with the result and intent.</returns>
public Task<(Result result, Intent? intent)> StartActivityForResultAsync(Intent intent, int requestCode)
{
// NB: It's important that we set up the subscription *before* we
// call ActivityForResult
var ret = ActivityResult
.Where(x => x.requestCode == requestCode)
.Select(x => (x.result, x.intent))
.FirstAsync()
.ToTask();

StartActivityForResult(intent, requestCode);
return ret;
}

/// <summary>
/// Starts the activity for result asynchronously.
/// </summary>
/// <param name="type">The type.</param>
/// <param name="requestCode">The request code.</param>
/// <returns>A task with the result and intent.</returns>
public Task<(Result result, Intent? intent)> StartActivityForResultAsync(Type type, int requestCode)
{
// NB: It's important that we set up the subscription *before* we
// call ActivityForResult
var ret = ActivityResult
.Where(x => x.requestCode == requestCode)
.Select(x => (x.result, x.intent))
.FirstAsync()
.ToTask();

StartActivityForResult(type, requestCode);
return ret;
}

/// <inheritdoc/>
protected override void OnPause()
{
base.OnPause();
_deactivated.OnNext(Unit.Default);
}

/// <inheritdoc/>
protected override void OnResume()
{
base.OnResume();
_activated.OnNext(Unit.Default);
}

/// <inheritdoc/>
protected override void OnActivityResult(int requestCode, Result resultCode, Intent? data)
{
base.OnActivityResult(requestCode, resultCode, data);
_activityResult.OnNext((requestCode, resultCode, data));
}

/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
if (disposing)
{
_activated.Dispose();
_deactivated.Dispose();
_activityResult.Dispose();
}

base.Dispose(disposing);
}
}
38 changes: 38 additions & 0 deletions src/ReactiveUI.AndroidX/ReactiveAppCompatActivity{TViewModel}.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) 2024 .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.

namespace ReactiveUI.AndroidX;

/// <summary>
/// This is an Activity that is both an Activity and has ReactiveObject powers
/// (i.e. you can call RaiseAndSetIfChanged).
/// </summary>
/// <typeparam name="TViewModel">The view model type.</typeparam>
public class ReactiveAppCompatActivity<TViewModel> : ReactiveAppCompatActivity, IViewFor<TViewModel>, ICanActivate
where TViewModel : class
{
private TViewModel? _viewModel;

/// <summary>
/// Initializes a new instance of the <see cref="ReactiveAppCompatActivity{TViewModel}"/> class.
/// </summary>
protected ReactiveAppCompatActivity()
{
}

/// <inheritdoc/>
public TViewModel? ViewModel
{
get => _viewModel;
set => this.RaiseAndSetIfChanged(ref _viewModel, value);
}

/// <inheritdoc/>
object? IViewFor.ViewModel
{
get => _viewModel;
set => _viewModel = (TViewModel?)value;
}
}
83 changes: 83 additions & 0 deletions src/ReactiveUI.AndroidX/ReactiveDialogFragment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (c) 2024 .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.

namespace ReactiveUI.AndroidX;

/// <summary>
/// This is a Fragment that is both an Activity and has ReactiveObject powers
/// (i.e. you can call RaiseAndSetIfChanged).
/// </summary>
public class ReactiveDialogFragment : global::AndroidX.Fragment.App.DialogFragment, IReactiveNotifyPropertyChanged<ReactiveDialogFragment>, IReactiveObject, IHandleObservableErrors
{
private readonly Subject<Unit> _activated = new();
private readonly Subject<Unit> _deactivated = new();

/// <summary>
/// Initializes a new instance of the <see cref="ReactiveDialogFragment"/> class.
/// </summary>
protected ReactiveDialogFragment()
{
}

/// <inheritdoc/>
public event PropertyChangingEventHandler? PropertyChanging;

/// <inheritdoc/>
public event PropertyChangedEventHandler? PropertyChanged;

/// <inheritdoc/>
public IObservable<Exception> ThrownExceptions => this.GetThrownExceptionsObservable();

/// <summary>
/// Gets a observable that signals when the fragment is activated.
/// </summary>
public IObservable<Unit> Activated => _activated.AsObservable();

/// <summary>
/// Gets a observable that signals when the fragment is deactivated.
/// </summary>
public IObservable<Unit> Deactivated => _deactivated.AsObservable();

/// <inheritdoc />
public IObservable<IReactivePropertyChangedEventArgs<ReactiveDialogFragment>> Changing => this.GetChangingObservable();

/// <inheritdoc/>
public IObservable<IReactivePropertyChangedEventArgs<ReactiveDialogFragment>> Changed => this.GetChangedObservable();

/// <inheritdoc/>
void IReactiveObject.RaisePropertyChanging(PropertyChangingEventArgs args) => PropertyChanging?.Invoke(this, args);

/// <inheritdoc/>
void IReactiveObject.RaisePropertyChanged(PropertyChangedEventArgs args) => PropertyChanged?.Invoke(this, args);

/// <inheritdoc />
public IDisposable SuppressChangeNotifications() => IReactiveObjectExtensions.SuppressChangeNotifications(this);

/// <inheritdoc/>
public override void OnPause()
{
base.OnPause();
_deactivated.OnNext(Unit.Default);
}

/// <inheritdoc/>
public override void OnResume()
{
base.OnResume();
_activated.OnNext(Unit.Default);
}

/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
if (disposing)
{
_activated.Dispose();
_deactivated.Dispose();
}

base.Dispose(disposing);
}
}
Loading

0 comments on commit de5f3fe

Please sign in to comment.