Going cross-platform with Xamarin–Doing the Android port
This blog post series is a documentation for a talk I give about cross-platform development with Xamarin. This series was co-authored by Johan Lindfors at Coderox. This post details how to implement the Android version of this app.
Introduction
In this blog post we will do the Android version of this app. This includes creating the new project, adding MvvmCross, implementing device specific services and of course implement the user interface. This will be a rather long post, so let’s get started.
Setting up the Project
First, let’s create the new project. Right-click on the solution, choose, Add => New Project. Choose the Android tab and create an Android application named Runner.Droid like this:
Add a reference to the Runner.Core portable class library. Right-click on the solution, choose Manage NuGet packages for solution, click Installed Packages and select MvvmCross.
Click the checkbox for Runner.Droid and press OK.
Now we’ve added references to our portable class library as well as to MvvmCross. Also remove MainActivity.cs, Views\FirstView.cs and Resources\layout\FirstView.axml + Main.axml since we are going to build our own user interface. As usual, we get a ToDo-MvvmCross folder with instructions, but we have already done those.
Go into properties for your Android project and make sure that you have at least API Level 12, since we use fragments in this app, like this:
We are all setup, let’s start building our app!
Implementing the services
Let’s start with the bottom layer. First, create a folder named Service. We have two platform specific services that needs implementation, IPeriodicService and IPositionService. Implementing IPeriodicService is trivial, it uses System.Threading.Timer. Here is the code for PeriodicService.cs:
namespace Runner.Droid.Service
{
public class PeriodicService : IPeriodicService
{
#region Private fields
private Timer _poller = null;
#endregion
#region Public methods
public void Start()
{
_poller = new Timer(_poller_Tick, null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
}
public void Stop()
{
if (_poller != null)
{
_poller.Dispose();
_poller = null;
}
}
#endregion
#region Callbacks
void _poller_Tick(object sender)
{
if (Ticked != null)
Ticked(this, new EventArgs());
}
#endregion
#region Events
public event TickedEventHandler Ticked;
#endregion
}
}
We also have to implement IPositionService. The stock Android emulator I use when presenting doesn’t have the ability to generate location triggers, so PositionService.cs is just a fake that has a predefined set of coordinates that it loops around. Here is the code for this:
namespace Runner.Droid.Service
{
public class PositionService : IPositionService
{
public class GpsData
{
public double Latitude { get; set; }
public double Longitude { get; set; }
}
List<GpsData> GpsDataPoints = new List<GpsData> {
new GpsData{Latitude=59.2403462746883,Longitude=17.9230690210418},
new GpsData{Latitude=59.240486746198, Longitude=17.9230003559093},
new GpsData{Latitude=59.240697452377, Longitude=17.9233093490056},
new GpsData{Latitude=59.240820363714, Longitude=17.9236870072345},
new GpsData{Latitude=59.241039847142, Longitude=17.9240989980296},
new GpsData{Latitude=59.241180315795, Longitude=17.9244594899753},
new GpsData{Latitude=59.241355900797, Longitude=17.9248714807704},
new GpsData{Latitude=59.241522705712, Longitude=17.9252491389993},
new GpsData{Latitude=59.241663172376, Longitude=17.925609630945},
new GpsData{Latitude=59.241856313093, Longitude=17.9260559543064},
new GpsData{Latitude=59.242049452717, Longitude=17.9264507788183},
new GpsData{Latitude=59.242181138197, Longitude=17.9267941044809},
new GpsData{Latitude=59.242365497014, Longitude=17.9272575941254},
new GpsData{Latitude=59.242532296990, Longitude=17.9277039174868},
new GpsData{Latitude=59.242646422819, Longitude=17.9280987419988},
new GpsData{Latitude=59.242830779121, Longitude=17.9284592339445},
new GpsData{Latitude=59.242997576821, Longitude=17.9288540584564},
new GpsData{Latitude=59.243120479867, Longitude=17.9291458852697},
new GpsData{Latitude=59.243111701093, Longitude=17.9294205457997},
new GpsData{Latitude=59.242892231000, Longitude=17.9294720446491},
new GpsData{Latitude=59.242734211659, Longitude=17.9294720446491},
new GpsData{Latitude=59.242576191585, Longitude=17.9295235434985},
new GpsData{Latitude=59.242444507631, Longitude=17.9295750423479},
new GpsData{Latitude=59.242383054945, Longitude=17.9295750423479},
new GpsData{Latitude=59.242225033244, Longitude=17.9296437074804},
new GpsData{Latitude=59.242084568895, Longitude=17.9295750423479},
new GpsData{Latitude=59.241952883042, Longitude=17.9294720446491},
new GpsData{Latitude=59.241838754891, Longitude=17.9293690469503},
new GpsData{Latitude=59.241707068089, Longitude=17.9292317166853},
new GpsData{Latitude=59.241628055764, Longitude=17.929060053854},
new GpsData{Latitude=59.241478809761, Longitude=17.9289398898721},
new GpsData{Latitude=59.241364680024, Longitude=17.9288025596071},
new GpsData{Latitude=59.241312004632, Longitude=17.9287338944746},
new GpsData{Latitude=59.241268108409, Longitude=17.928390568812},
new GpsData{Latitude=59.241180315795, Longitude=17.9280644094325},
new GpsData{Latitude=59.241118860831, Longitude=17.9277039174868},
new GpsData{Latitude=59.241074964360, Longitude=17.9273262592579},
new GpsData{Latitude=59.240995950570, Longitude=17.9268971021797},
new GpsData{Latitude=59.240916936597, Longitude=17.9265194439509},
new GpsData{Latitude=59.240846701800, Longitude=17.9260902868726},
new GpsData{Latitude=59.240776466859, Longitude=17.9257641274932},
new GpsData{Latitude=59.240697452377, Longitude=17.9253693029812},
new GpsData{Latitude=59.240653555364, Longitude=17.9249744784692},
new GpsData{Latitude=59.240583320025, Longitude=17.9246139865235},
new GpsData{Latitude=59.240495525648, Longitude=17.9243393259934},
new GpsData{Latitude=59.240442848913, Longitude=17.9238758363489},
new GpsData{Latitude=59.240390172097, Longitude=17.923618342102},
new GpsData{Latitude=59.240319936215, Longitude=17.9232750164394},
new GpsData{Latitude=59.240355054174, Longitude=17.9230518547587},
};
private Timer timer;
private int counter = 0;
public void Start()
{
timer = new Timer((state) =>
{
if (PositionChanged != null)
PositionChanged(this, new PositionChangedArgs(DateTime.Now, GpsDataPoints[counter].Latitude, GpsDataPoints[counter++].Longitude, 0));
}, null, TimeSpan.Zero, TimeSpan.FromSeconds(10));
}
public void Stop()
{
if (timer != null)
{
timer.Dispose();
timer = null;
counter = 0;
}
}
public event PositionChangedEventHandler PositionChanged;
public event StatusChangedEventHandler StatusChanged;
}
}
Finally, as for the Windows Phone app, we need to help the IoC container to resolve these interfaces. Also, let’s move the event handler for PositionChanged event from app.xaml.cs to Setup.cs (since Android has no similar concept. Setup.cs then should add these lines):
protected override void InitializeLastChance()
{
base.InitializeLastChance();
Mvx.LazyConstructAndRegisterSingleton<IPositionService, PositionService>();
Mvx.LazyConstructAndRegisterSingleton<IPeriodicService, PeriodicService>();
var positionService = Mvx.Resolve<IPositionService>();
if (positionService != null)
positionService.PositionChanged += _positionService_PositionChanged;
}
void _positionService_PositionChanged(object sender, PositionChangedArgs e)
{
Mvx.Resolve<IDataPointCollectionService>().AddDataPoint(new DataPointPosition()
{
Latitude = e.Latitude,
Longitude = e.Longitude,
Altitude = e.Altitude,
Time = (e.Time.HasValue ? e.Time.Value : DateTime.Now)
});
}
Implementing the user interface
The paradigms for user interfaces in Android is different but similar as in XAML. They both have the concept of XML to describe the user interface and code-behind in C# (when using Xamarin). In XAML we have pages, in Android we have activities. One difference is that in Android the design and the code behind is loosely coupled, where as in Windows Phone they are tightly coupled together. In Windows you can do user controls to create reusable components, in Android they are called fragments.
To build the user interface, we will replace the pivot in Windows Phone with a tab in Android. For brevity, I will not implement the map control in Android. So, we will add all the files necessary and highlight key concepts. After adding all the files, our Resources\Layout folder should look like this:
and our Service and Views folders should look like this:
If you look into these files they are pretty vanilla except for the use of binding, both for data and commands. Here’s the definition for the Start button:
<Button
android:textSize="22sp"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:id="@+id/btnStart"
local:MvxBind="Click StartExerciseCommand"
android:text="Start" />
Notice the bind command that associates the Click event with the StartExerciseCommand in the view model. Also, look at this snippet from currentFragment.xaml to illustrate how we data bind properties:
<TextView
android:textSize="22sp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
local:MvxBind="Text Time" />
Here bind again, this time the Text property of the TextView to the Time property in the view model.
Tying it altogether is MainView.cs, which is the code behind for our page (or activity). The code looks like this:
namespace Runner.Droid.Views
{
[Activity(Label = "Runner")]
public class MainView : MvxTabsFragmentActivity
{
public new MainViewModel ViewModel
{
get { return (DataContext as MainViewModel); }
}
public MainView()
: base(Resource.Layout.MainView, Resource.Id.actualtabcontent)
{
}
protected override void AddTabs(Bundle args)
{
try
{
AddTab<CurrentTabFragment>("Current", "Current", args, ViewModel.Current);
AddTab<TotalTabFragment>("Total", "Total", args, ViewModel.Total);
AddTab<LapTabFragment>("Lap", "Lap", args, ViewModel.Lap);
AddTab<MapTabFragment>("Map", "Map", args, ViewModel);
}
catch (Exception ex)
{
}
}
protected override void OnStart()
{
base.OnStart();
ViewModel.CollectionService.Start();
ViewModel.PeriodicService.Start();
ViewModel.PositionService.Start();
}
protected override void OnStop()
{
ViewModel.PositionService.Stop();
ViewModel.PeriodicService.Stop();
ViewModel.CollectionService.Stop();
base.OnStop();
}
}
}
This is pretty much it. Press F5 and watch the app run in all it’s glory
Summary
I hope I have shown you how easy it is to develop a cross-platform app based on Xamarin/MvvmCross. Remember that since Xamarin keeps the UI code and paradigms native, you still need to understand the platform and it’s frameworks However, given that we have a single code base, single programming language and a single development tool we will save a lot of development time in our projects. Happy Coding!
Comments
- Anonymous
June 10, 2015
Hello, great articles, can you share your project?