Search code examples
androidxamarinxamarin.formsxamarin.androidmvvmcross

MvvmCross & Android notifications


I'm in the middle of creation of an app built using Xamarin.Forms and MvvmCross 6.1.2 and came across a necessity to show a notification to User. After the notification is clicked a particular screen needs to be shown. Currently I'm trying to implement this functionality on android. I use Notification.Builder in order to create and show a notification and upon doing that an intent to be run needs to be provided.

Creating of the intent:

var request = MvxViewModelRequest<SomeViewModel>.GetDefaultRequest();
var translator = Mvx.Resolve<IMvxAndroidViewModelRequestTranslator>();
var intent = translator.GetIntentFor(request);

Unfortunately, it crashes saying Type doesn't inherit from Java.Lang.Object. I stuck and cannot find any example or explanation how to navigate to a particular screen/viewmodel when the notification is clicked. None of MvvmCross samples contain such functionality.

Really need your help!

Also, in order to demonstrate the problem I've created a simple project on github: https://github.com/oleg-savoskin/mvvmcross-sample


Update 1 (the stack trace):

07-07 13:04:32.734 I/MonoDroid(15618): UNHANDLED EXCEPTION:
07-07 13:04:32.785 I/MonoDroid(15618): System.ArgumentException: type
07-07 13:04:32.785 I/MonoDroid(15618): Parameter name: Type is not derived from a java type.
07-07 13:04:32.785 I/MonoDroid(15618):   at Java.Lang.Class.FromType (System.Type type) [0x00012] in <263adecfa58f4c449f1ff56156d886fd>:0 
07-07 13:04:32.785 I/MonoDroid(15618):   at Android.Content.Intent..ctor (Android.Content.Context packageContext, System.Type type) [0x00000] in <263adecfa58f4c449f1ff56156d886fd>:0 
07-07 13:04:32.785 I/MonoDroid(15618):   at MvvmCross.Platforms.Android.Views.MvxAndroidViewsContainer.GetIntentFor (MvvmCross.ViewModels.MvxViewModelRequest request) [0x0003f] in <17df0d0bdae848b7a8a12b58d710f763>:0 
07-07 13:04:32.785 I/MonoDroid(15618):   at Sample.Droid.Services.NotificationService.GetContentIntent () [0x0000d] in D:\TEMP\Repositories\Sample\Sample.Android\Services\NotificationService.cs:31 
07-07 13:04:32.785 I/MonoDroid(15618):   at Sample.Droid.Services.NotificationService.ShowNotification () [0x00001] in D:\TEMP\Repositories\Sample\Sample.Android\Services\NotificationService.cs:15 
07-07 13:04:32.785 I/MonoDroid(15618):   at Sample.Core.ViewModels.HomeViewModel.ShowNotification () [0x00001] in D:\TEMP\Repositories\Sample\Sample.Core\ViewModels\HomeViewModel.cs:25 
07-07 13:04:32.786 I/MonoDroid(15618):   at MvvmCross.Commands.MvxCommand.Execute (System.Object parameter) [0x00009] in <17df0d0bdae848b7a8a12b58d710f763>:0 
07-07 13:04:32.786 I/MonoDroid(15618):   at Xamarin.Forms.Button.SendClicked () [0x00008] in D:\a\1\s\Xamarin.Forms.Core\Button.cs:132 
07-07 13:04:32.786 I/MonoDroid(15618):   at Xamarin.Forms.Platform.Android.AppCompat.ButtonRenderer+ButtonClickListener.OnClick (Android.Views.View v) [0x0000b] in D:\a\1\s\Xamarin.Forms.Platform.Android\AppCompat\ButtonRenderer.cs:291 
07-07 13:04:32.786 I/MonoDroid(15618):   at Android.Views.View+IOnClickListenerInvoker.n_OnClick_Landroid_view_View_ (System.IntPtr jnienv, System.IntPtr native__this, System.IntPtr native_v) [0x0000f] in <263adecfa58f4c449f1ff56156d886fd>:0 
07-07 13:04:32.786 I/MonoDroid(15618):   at (wrapper dynamic-method) System.Object.3ef246c1-103e-48ba-ba77-3d677f97466e(intptr,intptr,intptr)

Update 2
I managed to make it working somehow by providing the main activity as a target type. It opens the app, but doesn't actually navigate to the 'page 2'.

    public class NotificationService : INotificationService
    {
        public void ShowNotification()
        {
            var notification = new NotificationCompat.Builder(Application.Context)
                .SetContentTitle("Sample app")
                .SetContentText("Click here to navigate to page 2")
                .SetSmallIcon(Resource.Drawable.ic_notification)
                .SetContentIntent(GetContentIntent())
                .SetShowWhen(false)
                .Build();

            var notificationManager = NotificationManagerCompat.From(Application.Context);
            notificationManager.Notify(1, notification);
        }

        private PendingIntent GetContentIntent()
        {
            var request = MvxViewModelRequest<Page2ViewModel>.GetDefaultRequest();

            var converter = Mvx.Resolve<IMvxNavigationSerializer>();
            var requestText = converter.Serializer.SerializeObject(request);

            var intent = new Intent(Application.Context, typeof(MainActivity)) // <-- It's here;
            intent.PutExtra("MvxLaunchData", requestText);

            return PendingIntent.GetActivity(Application.Context, 0, intent, 0);
        }
    }

But it still craches sometimes with an error System.NotSupportedException: Unable to activate instance of type Xamarin.Forms.Platform.Android.PageRenderer from native handle 0x7fc0da6d44.

Any ideas what I'm doing wrong here?


Solution

  • You were on the right track with your Update 2. The problem with the first, is it appears that MvvmCross uses Intents internally for Android, but some of these cannot be passed to Android methods because the referred types don't inherit from Java.Lang.Object.

    This code is working, and is in https://github.com/curtisshipley/mvvmcross-sample

    It handles both the case when the main activity is running and where it is not. Depending on the case, there will be a different entry point for the intent: OnCreate if the MainActivity is not running. OnNewIntent if it is. In the sample code, these just call NavigateToRequestIfPresent.

    Also, I made a small tweak to the NavigationService so that it will work with any ViewModel that derives from IMvxViewModel.

    public void ShowNotification<VM>() where VM : IMvxViewModel
    {
        var notification = 
            new NotificationCompat.Builder(Application.Context, SampleApplication.NOTIFICATION_CHANNEL)
            .SetContentTitle("Sample app")
            .SetContentText("Click here to navigate to page 2")
            .SetSmallIcon(Resource.Drawable.ic_notification)
            .SetContentIntent(GetContentIntent<VM>())
            .SetShowWhen(false)
            .Build();
    
        var notificationManager = NotificationManagerCompat.From(Application.Context);
        notificationManager.Notify(1, notification);
    }
    
    private PendingIntent GetContentIntent<VM>() where VM : IMvxViewModel
    {
        var request = MvxViewModelRequest<VM>.GetDefaultRequest();
    
        var converter = Mvx.Resolve<IMvxNavigationSerializer>();
        var requestText = converter.Serializer.SerializeObject(request);
    
        var intent = new Intent(Application.Context, typeof(MainActivity)); 
    
        // We only want one activity started
        intent.AddFlags(ActivityFlags.SingleTop);
    
        intent.PutExtra("MvxLaunchData", requestText);
    
        // Create Pending intent, with OneShot. We're not going to want to update this.
        return PendingIntent.GetActivity(Application.Context, 0, intent, PendingIntentFlags.OneShot);
    }
    

    To respond we need to allow the MainActivity to know about the intent. This method checks the Intent for the serialized request for the view model. If present, we deserialize and then navigate.

    protected void NavigateToRequestIfPresent(Intent intent)
    {
        // If MvxLaunchData is present, we then know we should navigate to that intent
        var requestText = intent.GetStringExtra("MvxLaunchData");
    
        if (requestText == null)
            return;
    
        var viewDispatcher = Mvx.Resolve<IMvxViewDispatcher>();
    
        var converter = Mvx.Resolve<IMvxNavigationSerializer>();
        var request = converter.Serializer.DeserializeObject<MvxViewModelRequest>(requestText);
    
        viewDispatcher.ShowViewModel(request);
    }
    

    A final note, that as of Android Oreo, this particular way of creating notifications requires a NotificationChannel.