Tuesday, June 9, 2020

Xamarin, Converting a receiver to a reusable class

My last blog "Xamarin, Android Receivers, and Intents" examined how receivers and intents work in Xamarin by looking at the battery intent and tracking the charge level. However, the architecture was poor in that the receiver had to know too much about the UI that was calling it. It would be better to encapsulate the functionality into a class and use properties and an event to notify the UI that a property had changed. Let's do that.

Forget iOS, I'm not paying their usurious developer license fee. Android forever! 

As the core functionality must be in the .Android project and we want to expose a class in the Forms project we will have to communicate between the two using a DependencyService. We could use events or messages for the notification and I've chosen to use events because I already understand them well.

Before you launch into this demonstration you should work through Xamarin, Android Receivers, and Intents first.

I'm using Visual Studio version 16.6.1. Start a new Xamarin Forms project called BatteryMonitor. Deselect the iOS option.

Xamarin Forms C# project

Blank project and deselect iOS

We have to write two classes.
  • One goes into the .Android project and does the work of getting the battery state
  • The other goes into the Forms project and encapsulates the DependencyService

Use NuGet solution manager (under Tools) to add Xamarin.Essentials to both projects.

In the Browse tab, search for Xamarin.Essentials and add it to both projects.
There is a common interface shared between the two projects. It resides in the Forms project so we need to start there. Add a new class called StateForms in the Forms project. For now, we will only define the interface and the class we use to hold the battery state. We will finish it later.

I want to make the battery state and all its properties read-only, so the properties are populated in the constructor and nowhere else. I want to add texts for some properties. I could use enums but string arrays suffice and are simpler. Here is the code.

using System;
using Xamarin.Forms;

namespace BatteryMonitor
{
    public interface IBatteryState
    {
        State state { get; }
        event EventHandler StateChanged;
    }

    public class State
    {
        public int Level { get; }
        public int Health { get; }
        public string HealthText { get; }
        public int Plugged { get; }
        public string PluggedText { get; }
        public int Status { get; }
        public string StatusText { get; }
        public string Technology { get; }
        public int Temperature { get; }
        public int Voltage { get; }

        private String[] HealthTexts = { "", "Unknown", "Good", "Overheat", "Dead", "OverVoltage", "UnspecifiedFailure", "Cold" };
        private string[] PluggedTexts = { "Unplugged", "AC Charger", "USB", "", "Wireless" };
        private string[] StatusTexts = { "", "Unknown", "Charging", "Discharging", "NotCharging", "Full" };

        public State() { }

        public State(int _level, int _health, int _plugged, int _status, string _technology, int _temperature, int _voltage)
        {
            Level = _level;
            Health = _health;
            HealthText = HealthTexts[_health];
            Plugged = _plugged;
            PluggedText = PluggedTexts[_plugged];
            Status = _status;
            StatusText = StatusTexts[_status];
            Technology = _technology;
            Temperature = _temperature;
            Voltage = _voltage;
        }
    }
}

Now we can write the .Android code that uses this interface. It declares a receiver with an intent and populates a State class. It is implemented as a Dependency Service so the Forms project can access it. When the battery state changes it raises the StateChanged event.

Add a new class file to the .Android project called StateAndroid. It will hold the BatteryState_Android class and the BatteryReceiver class.
  • BatteryState_Android is a DependencyService so it requires a parameter-less constructor but it also needs to know the current activity when it's registered which is tricky to get from scratch. It's easier to pass the current activity to the constructor which is why we have two constructors. It implements the interface's State property and StateChanged event.
  • BatteryReceiver simply populates itself in the OnReceive method and raises an event that BatteryState_Android subscribes to and passes back to the Form project.

Populate it thus.

using System;
using Android.App;
using Android.Content;
using Android.OS;
using BatteryMonitor.Droid;

[assembly: Xamarin.Forms.Dependency(typeof(BatteryState_Android))]
namespace BatteryMonitor.Droid
{
    public class BatteryState_Android : IBatteryState
    {
        static BatteryReceiver batteryReceiver = new BatteryReceiver();
       
        public BatteryState_Android() { }

        public BatteryState_Android(Activity activity)
        {
            Intent intent = activity.RegisterReceiver(batteryReceiver, new IntentFilter(Intent.ActionBatteryChanged));
        }

        event EventHandler IBatteryState.StateChanged
        {
            add { batteryReceiver.Received += value; }
            remove { batteryReceiver.Received -= value; }
        }

        public State state
        {
            get { return new State(batteryReceiver.Level, batteryReceiver.Health, batteryReceiver.Plugged, batteryReceiver.Status, batteryReceiver.Technology, batteryReceiver.Temperature, batteryReceiver.Voltage); }
        }
    }


    public class BatteryReceiver : BroadcastReceiver
    {
        public EventHandler Received;
        public int Level;
        public int Health;
        public int Plugged;
        public int Status;
        public string Technology;
        public int Temperature;
        public int Voltage;

        public override void OnReceive(Context context, Intent intent)
        {
            Level = intent.GetIntExtra(BatteryManager.ExtraLevel, 0);
            Health = intent.GetIntExtra(BatteryManager.ExtraHealth, 0);
            Plugged = intent.GetIntExtra(BatteryManager.ExtraPlugged, 0);
            Status = intent.GetIntExtra(BatteryManager.ExtraStatus, 0);
            Technology = intent.GetStringExtra(BatteryManager.ExtraTechnology);
            Temperature = intent.GetIntExtra(BatteryManager.ExtraTemperature, 0);
            Voltage = intent.GetIntExtra(BatteryManager.ExtraVoltage, 0);
            Received?.Invoke(this, EventArgs.Empty);
        }
    }
}

We need to add a line to .Android MainActivity.cs to register the BatteryReceiver. Add the bold line below. It must be before LoadApplication or the class will not be initially populated.

using Android.App;

using Android.Content.PM;
using Android.Runtime;
using Android.OS;

namespace BatteryMonitor.Droid
{
    [Activity(Label = "BatteryMonitor", Icon = "@mipmap/icon", Theme = "@style/MainTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation)]
    public class MainActivity : global::Xamarin.Forms.Platform.Android.FormsAppCompatActivity
    {
        protected override void OnCreate(Bundle savedInstanceState)
        {
            TabLayoutResource = Resource.Layout.Tabbar;
            ToolbarResource = Resource.Layout.Toolbar;

            base.OnCreate(savedInstanceState);

            Xamarin.Essentials.Platform.Init(this, savedInstanceState);
            global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
            BatteryState_Android batteryState_Android = new BatteryState_Android(this);
            LoadApplication(new App());
        }

        public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Android.Content.PM.Permission[] grantResults)
        {
            Xamarin.Essentials.Platform.OnRequestPermissionsResult(requestCode, permissions, grantResults);
            base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
        }
    }
}

Now we can return to the Forms project, specifically StateForms.cs. It would be nice if the event that informs the UI that the battery state has changed passed the new state. We will need a new class called StateChangedEventArgs that inherits EventArgs and adds a state member. Add this to StateForms.cs

    public class StateChangedEventArgs : EventArgs
    {
        public State state;
    }

Now we will write the class the UI will use. It exposes a read-only state property. It will have two constructors.
  • The parameter-less constructor will not receive StateChanged events but can still poll properties.
  • The other constructor takes an event handler as the parameter that will receive StateChanged events. Note the UI cannot initialize the class using this constructor in the body of the class, it must be within the constructor or a method. 

    public class BatteryState : IBatteryState

    {
        public event EventHandler StateChanged;

        public BatteryState() { }

        public BatteryState(EventHandler _StateChanged)
        {
            StateChanged = _StateChanged;
            DependencyService.Get<IBatteryState>().StateChanged += OnStateChanged;
        }

        private void OnStateChanged(object sender, EventArgs e)
        {
            StateChanged?.Invoke(this, new StateChangedEventArgs() { state = DependencyService.Get<IBatteryState>().state });
        }

        public State state
        {
            get { return DependencyService.Get<IBatteryState>().state; }
        }
    }

It is now time to reap that we have just sowed. Let's get stuck into the UI. We will simply instantiate the BatteryState class passing an event handler. The event handler will populate the screen with the state properties.

Note I've assumed the battery temperature is in tenths of a degree Celsius. I can't find any documentation and this is the only assumption that makes any sense. I also assumed the voltage is in millivolts.

Here's the XAML for MainPage.xaml.

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:d="http://xamarin.com/schemas/2014/forms/design"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             mc:Ignorable="d"
             x:Name="This"
             x:Class="BatteryMonitor.MainPage">
    <ContentPage.BindingContext>
        <x:Reference Name="This"/>
    </ContentPage.BindingContext>
    <StackLayout>
        <Label Text="{Binding Path=BatteryLevel, StringFormat='Level {0}%' }" HorizontalOptions="Center"/>
        <Label Text="{Binding Path=BatteryHealth, StringFormat='Health {0}' }" HorizontalOptions="Center"/>
        <Label Text="{Binding Path=BatteryPlugged, StringFormat='Power {0}' }" HorizontalOptions="Center"/>
        <Label Text="{Binding Path=BatteryStatus, StringFormat='Status {0}' }" HorizontalOptions="Center"/>
        <Label Text="{Binding Path=BatteryTechnology, StringFormat='Technology {0}' }" HorizontalOptions="Center"/>
        <Label Text="{Binding Path=BatteryTemperature, StringFormat='Temperature {0:0.0}C' }" HorizontalOptions="Center"/>
        <Label Text="{Binding Path=BatteryVoltage, StringFormat='Voltage {0:0.000}v' }" HorizontalOptions="Center"/>
    </StackLayout>
</ContentPage>

The code behind is quite simple. Most of it is just defining the properties. I have highlighted the important bits. Note in StateChanged I could also have pulled the details out of the EventArgs by casting e to StateChangedEventArgs.


using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Xamarin.Essentials;
using Xamarin.Forms;

namespace BatteryMonitor
{
    [DesignTimeVisible(false)]
    public partial class MainPage : ContentPage
    {
        BatteryState batteryState;

        private int _BatteryLevel = 0;
        public int BatteryLevel
        {
            get { return _BatteryLevel; }
            set { SetProperty(ref _BatteryLevel, value); }
        }

        private string _BatteryHealth = "";
        public string BatteryHealth
        {
            get { return _BatteryHealth; }
            set { SetProperty(ref _BatteryHealth, value); }
        }

        private string _BatteryPlugged = "";
        public string BatteryPlugged
        {
            get { return _BatteryPlugged; }
            set { SetProperty(ref _BatteryPlugged, value); }
        }

        private string _BatteryStatus = "";
        public string BatteryStatus
        {
            get { return _BatteryStatus; }
            set { SetProperty(ref _BatteryStatus, value); }
        }

        private string _BatteryTechnology = "";
        public string BatteryTechnology
        {
            get { return _BatteryTechnology; }
            set { SetProperty(ref _BatteryTechnology, value); }
        }

        private int _BatteryTemperature = 0;
        public int BatteryTemperature
        {
            get { return _BatteryTemperature; }
            set { SetProperty(ref _BatteryTemperature, value); }
        }

        private int _BatteryVoltage = 0;
        public int BatteryVoltage
        {
            get { return _BatteryVoltage; }
            set { SetProperty(ref _BatteryVoltage, value); }
        }

        public MainPage()
        {
            batteryState = new BatteryState(StateChanged);
            InitializeComponent();
        }

        private void StateChanged(object sender, EventArgs e)
        {
            BatteryLevel = batteryState.state.Level;
            BatteryHealth = batteryState.state.HealthText;
            BatteryPlugged = batteryState.state.PluggedText;
            BatteryStatus = batteryState.state.StatusText;
            BatteryTechnology = batteryState.state.Technology;
            BatteryTemperature = batteryState.state.Temperature;
            BatteryVoltage = batteryState.state.Voltage;
        }

        public bool SetProperty<T>(ref T Storage, T value, [CallerMemberName] string propertyName = null)
        {
            if (Object.Equals(Storage, value)) return false;
            Storage = value;
            OnPropertyChanged(propertyName);
            return true;
        }
    }
}

So to use this new BatteryState class you do the following.
  • Add StateAndroid to the .Android project
  • Add a line to MainActivity.
  • Add StateForm to the Forms project.
  • Instantiate BatteryState in the UI and write a StateChanged event handler.
I don't claim this is the best way to do this. I'm sure there are ways it could be improved. Here are the results.

Emulator

Moto g7 Power

No comments:

Post a Comment