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>
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.