reactiveui / ReactiveUI

An advanced, composable, functional reactive model-view-viewmodel framework for all .NET platforms that is inspired by functional reactive programming. ReactiveUI allows you to abstract mutable state away from your user interfaces, express the idea around a feature in one readable place and improve the testability of your application.
https://www.reactiveui.net
MIT License
7.99k stars 1.12k forks source link

[Bug]: WireUpControls throws exception in net8.0-android #3851

Open gerardvanderkruijs opened 2 weeks ago

gerardvanderkruijs commented 2 weeks ago

Describe the bug 🐞

When calling ReactiveUI.ControlFetcherMixin.WireUpControls() in a .NET 8 Android app (net8.0-android) in non-debug configuration, even though the correct resource ID is specified, a MissingFieldException is thrown.

This issue was fixed with bug report #3714 , but only for Debug builds by the looks of it. When deploying Release builds the exception is still present. I narrowed it down to one specific .csproj property, that is this one:

<AndroidLinkMode>SdkOnly</AndroidLinkMode>

When I change this to <AndroidLinkMode>None</AndroidLinkMode> it all works perfectly.

Stracktrace:

07-08 10:38:08.523 I/MonoDroid( 5668): UNHANDLED EXCEPTION:
07-08 10:38:08.525 I/MonoDroid( 5668): Android.Runtime.JavaProxyThrowable: Exception_WasThrown, Android.Runtime.JavaProxyThrowable
07-08 10:38:08.525 I/MonoDroid( 5668): 
07-08 10:38:08.525 I/MonoDroid( 5668):   --- End of managed Android.Runtime.JavaProxyThrowable stack trace ---
07-08 10:38:08.525 I/MonoDroid( 5668): android.runtime.JavaProxyThrowable: [System.MissingFieldException]: Failed to wire up the Property XXXXXXX to a View in your layout with a corresponding identifier
07-08 10:38:08.525 I/MonoDroid( 5668):  at ReactiveUI.ControlFetcherMixin.WireUpControls(Unknown Source:0)
07-08 10:38:08.525 I/MonoDroid( 5668):  at XXXXXXXX.StartActivity.OnCreate(Unknown Source:0)
07-08 10:38:08.525 I/MonoDroid( 5668):  at Android.App.Activity.n_OnCreate_Landroid_os_Bundle_(Unknown Source:0)
07-08 10:38:08.525 I/MonoDroid( 5668):  at Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPL_V(Unknown Source:0)
07-08 10:38:08.525 I/MonoDroid( 5668):  at crc64763ea4d78b8a71b3.StartActivity.n_onCreate(Native Method)
07-08 10:38:08.525 I/MonoDroid( 5668):  at crc64763ea4d78b8a71b3.StartActivity.onCreate(StartActivity.java:42)
07-08 10:38:08.525 I/MonoDroid( 5668):  at android.app.Activity.performCreate(Activity.java:8595)
07-08 10:38:08.525 I/MonoDroid( 5668):  at android.app.Activity.performCreate(Activity.java:8573)
07-08 10:38:08.525 I/MonoDroid( 5668):  at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1456)
07-08 10:38:08.525 I/MonoDroid( 5668):  at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3764)
07-08 10:38:08.525 I/MonoDroid( 5668):  at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3922)
07-08 10:38:08.525 I/MonoDroid( 5668):  at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:103)
07-08 10:38:08.525 I/MonoDroid( 5668):  at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:139)
07-08 10:38:08.525 I/MonoDroid( 5668):  at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:96)
07-08 10:38:08.525 I/MonoDroid( 5668):  at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2443)
07-08 10:38:08.525 I/MonoDroid( 5668):  at android.os.Handler.dispatchMessage(Handler.java:106)
07-08 10:38:08.525 I/MonoDroid( 5668):  at android.os.Looper.loopOnce(Looper.java:205)
07-08 10:38:08.525 I/MonoDroid( 5668):  at android.os.Looper.loop(Looper.java:294)
07-08 10:38:08.525 I/MonoDroid( 5668):  at android.app.ActivityThread.main(ActivityThread.java:8177)
07-08 10:38:08.525 I/MonoDroid( 5668):  at java.lang.reflect.Method.invoke(Native Method)
07-08 10:38:08.525 I/MonoDroid( 5668):  at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
07-08 10:38:08.525 I/MonoDroid( 5668):  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:971)
07-08 10:38:08.525 I/MonoDroid( 5668): 

project .csproj file

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFramework>net8.0-android</TargetFramework>
        <SupportedOSPlatformVersion>24</SupportedOSPlatformVersion>
        <RootNamespace>ReactiveUI_WireUpControls_Issue</RootNamespace>
        <OutputType>Exe</OutputType>
        <Nullable>enable</Nullable>
        <ImplicitUsings>enable</ImplicitUsings>
        <ApplicationId>com.companyname.ReactiveUI_WireUpControls_Issue</ApplicationId>
        <ApplicationVersion>1</ApplicationVersion>
        <ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
        <Configurations>Debug;Release</Configurations>
        <WarningLevel>4</WarningLevel>
        <ReleaseVersion>1.0.0</ReleaseVersion>
        <RuntimeIdentifiers>android-arm;android-arm64;android-x86;android-x64</RuntimeIdentifiers>
        <AndroidDexTool>d8</AndroidDexTool>
        <EnableLLVM>false</EnableLLVM>
        <AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
        <BundleAssemblies>false</BundleAssemblies>
    </PropertyGroup>
    <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
        <DebugSymbols>true</DebugSymbols>
        <DebugType>full</DebugType>
        <Optimize>false</Optimize>
        <ErrorReport>prompt</ErrorReport>
        <ConsolePause>false</ConsolePause>
        <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
        <AndroidUseAapt2>true</AndroidUseAapt2>
        <AndroidUseSharedRuntime>true</AndroidUseSharedRuntime>
        <AndroidEnableSGenConcurrent>false</AndroidEnableSGenConcurrent>
        <AndroidLinkMode>None</AndroidLinkMode>
    </PropertyGroup>
    <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
        <DebugSymbols>false</DebugSymbols>
        <DebugType>None</DebugType>
        <Optimize>true</Optimize>
        <AndroidLinkMode>SdkOnly</AndroidLinkMode>
    </PropertyGroup>
    <ItemGroup>
        <PackageReference Include="ReactiveUI" Version="19.6.12" />
    </ItemGroup>
</Project>

Step to reproduce

  1. Install .NET 8. (I installed 8.0.302)
  2. Create a new net8.0-android project using dotnet new android command.
  3. Add reference to ReactiveUI version 19.6.12
  4. Open MainActivity.cs and change it so that MainActivity inherits ReactiveUI.ReactiveActivity
    public class MainActivity : ReactiveActivity
  5. Assign an ID(android:id) to the TextView in activity_main.xml and add a member to MainActivity.cs to wire it up.
    <TextView 
        android:id="@+id/app_text"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="@string/app_text"
    />
    [WireUpResource("app_text")]
    public TextView AppText { get; private set; }
  6. Add a call to WireUpControls() after SetContentView() in OnCreate().
    this.WireUpControls();
  7. Build and launch application with Release configuration
  8. MissingFieldException is thrown from WireUpControls().
  9. Also you could change AndroidLinkMode to SdkOnly in the Debug config, this will result in same exception.

Reproduction repository

https://github.com/gerardvanderkruijs/ReactiveUI-WireUpControls-Issue

Expected behavior

After launching the application In Release configuration, the TextView should be wired up correctly, and the initial screen should be displayed. AndroidLinkMode set to SdkOnly should not throw a MissingFieldException.

Screenshots πŸ–ΌοΈ

No response

IDE

Rider macOS

Operating system

Android

Version

13

Device

Emulator

ReactiveUI Version

19.6.12

Additional information ℹ️

Since this report is almost identical to #3714 I mainly used all the info and sample project provided by @tommy10344, so thank you for that :-)

gerardvanderkruijs commented 1 day ago

@ChrisPulman Do you perhaps have any clue about this? You fixed the same issue in #3714, so maybe your magic can solve this issue too πŸ˜‡

ChrisPulman commented 1 day ago

Hi @gerardvanderkruijs I will try to take a look this weekend, unfortunately I am away until Friday evening so don't think I'll get much time until then.

gerardvanderkruijs commented 1 day ago

Hi @ChrisPulman , no worries that’s perfect! Thank you so much πŸ™

ChrisPulman commented 1 day ago

@gerardvanderkruijs Just wondering if there is a specific requirement to use V19.6.12

ChrisPulman commented 1 day ago

@gerardvanderkruijs I did some research and as we are using reflection within ReactiveUI using linkers with AOT Compilation will not work as desired, the _Microsoft.Android.Resource.Designer.dll gets stripped of its code by being trimmed. I will see if I can find a way to do the same thing without using reflection. It seems that the _Microsoft.Android.Resource.Designer library may also not AOT friendly so may need to see what can be done about that first.

If the legacy AndroidLinkMode setting is used, both SdkOnly and Full will default to equivalent linker settings:

<PublishTrimmed>true</PublishTrimmed>
<TrimMode>link</TrimMode>

With AndroidLinkMode=SdkOnly only BCL and SDK assemblies marked with %(Trimmable) will be linked at the member level. AndroidLinkMode=Full will set %(TrimMode)=link on all .NET assemblies similar to the example in the trimming documentation.

It is recommended to migrate to the new linker settings, as AndroidLinkMode will eventually be deprecated.

For the time being in your project I would set the following:

<IsTrimmable>True</IsTrimmable>
<TrimMode>link</TrimMode>
<PublishTrimmed>false</PublishTrimmed> // will remove the resources if true
<RunAOTCompilation>false</RunAOTCompilation> // will remove the resources if true

Then set the following:

[DynamicDependency(DynamicallyAccessedMemberTypes.All, "ResourceConstant", "_Microsoft.Android.Resource.Designer")]
protected override void OnCreate(Bundle? savedInstanceState)

Further Documentation can be found here Linking with .NET Android

ChrisPulman commented 1 day ago

Trimming incompatibilities Some further pointers to why Trimming can fail

gerardvanderkruijs commented 21 hours ago

Hi @ChrisPulman ,

Thanks for the super clear explanation and for the (temp) solution! I've been playing around this morning and everything seems to be working alright again now πŸ™Œ

The reason we're still using 19.6.12 is because 20.x.x results in another error in our Android project. Of course this is not the thread for it, but it throws the following exception:

System.MethodAccessException: Method `ReactiveUI.ControlFetcherMixin.GetWireUpMembers(object,ReactiveUI.ControlFetcherMixin/ResolveStrategy)' is inaccessible from method `ReactiveUI.AndroidX.ControlFetcherMixin.WireUpControls(AndroidX.Fragment.App.Fragment,Android.Views.View,ReactiveUI.ControlFetcherMixin/ResolveStrategy)'
   at ReactiveUI.AndroidX.ControlFetcherMixin.WireUpControls(Fragment fragment, View inflatedView, ResolveStrategy resolveMembers) in /_/src/ReactiveUI.AndroidX/ControlFetcherMixin.cs:line 35
   at ***.OnViewCreated(View view, Bundle savedInstanceState) in ***.cs:line 84
   at AndroidX.Fragment.App.Fragment.n_OnViewCreated_Landroid_view_View_Landroid_os_Bundle_(IntPtr jnienv, IntPtr native__this, IntPtr native_view, IntPtr native_savedInstanceState) in C:\a\_work\2\s\generated\androidx.fragment.fragment\obj\Release\net7.0-android\generated\src\AndroidX.Fragment.App.Fragment.cs:line 2742
   at Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPLL_V(_JniMarshal_PPLL_V callback, IntPtr jnienv, IntPtr klazz, IntPtr p0, IntPtr p1) in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:line 198

Also, there is no 20.x.x version of the ReactiveUI.AndroidX package, the last version is 19.6.12, I assume we still need this package to wire up our controls in our .NET Android project?

ChrisPulman commented 19 hours ago

ReactiveUI.AndroidX was aimed at pre net6.0 targeted versions of Android, as far as I am aware you should be able to achieve the same with just the ReactiveUI base package. I will certainly check again though.

gerardvanderkruijs commented 18 hours ago

Ah okay! Good to know. When I remove the ReactiveUI.AndroidX package there's a lot of errors, which is not necessarily a big issue if I would be able to fix them. For example, where did the class ReactiveRecyclerViewViewHolder go? Or ReactiveRecyclerViewAdapter, ReactiveAppCompatActivity and so on.