β

Xamarin.Forms - Write Once, Run Everywhere, AND Be

Scott Hanselman's Blog 997 阅读
Xamarin lets you shared code across platforms

I worked in Java for a number of years at Nike, writing an order management application that would run on four platforms. We used to joke that we'd "write once, debug everywhere." Now, this was the early days of Java, but the thing was, every form and control was 'owner drawn.' That meant that a button looked the same everywhere because it wasn't a real button as far as the operating system was concerned. It was a picture of a button. We used to use Spy++ and different Windows inspector programs to explore our applications and they could never see a Java program's controls. This meant that the app pretty much worked everywhere, but the app always LOOKED like a Java App. They didn't integrated with the underlying platform.

With MVVM (Model, View, View-Model) patterns, and techniques like the Universal apps work on Windows Phone 8.1 and Windows 8.1, code sharing can get up into the high 90% for some kinds of apps. However, even for simple apps you've still got to create a custom native view for each platform. This is desirable in many cases, but for some app it's just boring, error prone, and tedious.

Xamarin announced Xamarin.Forms today which (in my words) effectively abstracts away native controls to a higher conceptual level. This, to my old eyes, is very similar to the way I wrote code in Java back in the day - it was all done in a fluent code-behind with layouts and flows. You create a control tree.

"Xamarin.Forms is a new library that enables you to build native UIs for iOS, Android and Windows Phone from a single, shared C# codebase. It provides more than 40 cross-platform controls and layouts which are mapped to native controls at runtime, which means that your user interfaces are fully native."

Xamarin uses Shared Projects in Visual StudioWhat's interesting about this, to me, is that these "control/concepts" (my term) are coded at a high level but rendered as their native counterparts. So a "tab" in my code is expressed in its most specific and native counterpart on the mobile device, rather than as a generic tab control as in my Java example. Let's see an example.

My buddy from Xamarin, James Montemagno, a fellow Chipotle lover, put together the ultimate cross-platform Hanselman application in a caffeinated late night hack to illustrate a few points for me. This little app is written in C# and runs natively on Windows Phone, Android, and iOS. It aggregates my blog and my tweets.

Here is the menu that switches between views:

WindowsPhone2Android2iPhone2

And the code that creates it. I've simplified a little for clarity, but the idea is all MVVM:

public HomeMasterView(HomeViewModel viewModel)
{
    this.Icon = "slideout.png";
    BindingContext = viewModel;
    var layout = new StackLayout { Spacing = 0 };
    var label = new ContentView {
        Padding = new Thickness(10, 36, 0, 5),
        BackgroundColor = Color.Transparent,
        Content = new Label {
            Text = "MENU",
            Font = Font.SystemFontOfSize (NamedSize.Medium)
        }
    };
    layout.Children.Add(label);
        
    var listView = new ListView ();
    var cell = new DataTemplate(typeof(ListImageCell));
    cell.SetBinding (TextCell.TextProperty, HomeViewModel.TitlePropertyName);
    cell.SetBinding (ImageCell.ImageSourceProperty, "Icon");
    listView.ItemTemplate = cell;
    listView.ItemsSource = viewModel.MenuItems;


//SNIP



    listView.SelectedItem = viewModel.MenuItems[0];
    layout.Children.Add(listView);
    Content = layout;
}

Note a few things here. See the ListImageCell? He's subclassed ImageCell, which is a TextCell with an Image, and setup data binding for the text and the icon. There's recognition that every platform will have text and an icon, but the resources will be different on each. That's why the blog and Twitter icons are unique to each platform. The concepts are shared and the implementation is native and looks native.

That's the UI side, on the logic side all the code that loads the RSS feed and Tweets is shared across all three platforms. It can use async and await for non-blocking I/O and in the Twitter example, it uses LinqToTwitter as a PCL (Portable Class Library) which is cool. For RSS parsing, it's using Linq to XML.

private async Task ExecuteLoadItemsCommand()
{
    if (IsBusy)
        return;
    IsBusy = true;
    try{
        var httpClient = new HttpClient();
        var feed = "http://feeds.hanselman.com/ScottHanselman";
        var responseString = await httpClient.GetStringAsync(feed);
        FeedItems.Clear();
        var items = await ParseFeed(responseString);
        foreach (var item in items)
        {
            FeedItems.Add(item);
        }
    } catch (Exception ex) {
        var page = new ContentPage();
        var result = page.DisplayAlert ("Error", "Unable to load blog.", "OK", null);
    }
    IsBusy = false;
}

And ParseFeed:

private async Task<List<FeedItem>> ParseFeed(string rss)
{
    return await Task.Run(() =>
        {
            var xdoc = XDocument.Parse(rss);
            var id = 0;
            return (from item in xdoc.Descendants("item")
                select new FeedItem
                {
                    Title = (string)item.Element("title"),
                    Description = (string)item.Element("description"),
                    Link = (string)item.Element("link"),
                    PublishDate = (string)item.Element("pubDate"),
                    Category = (string)item.Element("category"),
                    Id = id++
                }).ToList();
        });
}

Again, all shared. When it comes time to output the data in a List on Windows Phone, Android, and iPhone, it looks awesome (read: native) on every platform without  having to actually do anything platform specific. The controls look native because they are native. Xamarin.Forms controls are a wrapper on native controls, they aren't new controls themselves.

WindowsPhone3Android3iPhone3

Here's BlogView. Things like ActivityIndicator are from Xamarin.Forms, and it expresses itself as a native control.

public BlogView ()
{
    BindingContext = new BlogFeedViewModel ();
    var refresh = new ToolbarItem {
        Command = ViewModel.LoadItemsCommand,
        Icon = "refresh.png",
        Name = "refresh",
        Priority = 0
    };
    ToolbarItems.Add (refresh);
    var stack = new StackLayout {
        Orientation = StackOrientation.Vertical,
        Padding = new Thickness(0, 8, 0, 8)
    };
    var activity = new ActivityIndicator {
        Color = Helpers.Color.DarkBlue.ToFormsColor(),
        IsEnabled = true
    };
    activity.SetBinding (ActivityIndicator.IsVisibleProperty, "IsBusy");
    activity.SetBinding (ActivityIndicator.IsRunningProperty, "IsBusy");
    stack.Children.Add (activity);
    var listView = new ListView ();
    listView.ItemsSource = ViewModel.FeedItems;
    var cell = new DataTemplate(typeof(ListTextCell));
    cell.SetBinding (TextCell.TextProperty, "Title");
    cell.SetBinding (TextCell.DetailProperty, "PublishDate");
    cell.SetValue(TextCell.StyleProperty, TextCellStyle.Vertical);
    listView.ItemTapped +=  (sender, args) => {
        if(listView.SelectedItem == null)
            return;
        this.Navigation.PushAsync(new BlogDetailsView(listView.SelectedItem as FeedItem));
        listView.SelectedItem = null;
    };
    listView.ItemTemplate = cell;
    stack.Children.Add (listView);
    Content = stack;
}

Xamarin Forms is a very clever and one might say, elegant, solution to the Write Once, Run Anywhere, AND Don't Suck problem. What's nice about this is that you can care about the underlying platform when you want to, and ignore it when you don't. A solution that HIDES the native platform isn't native then, is it? That'd be a lowest common denominator solution. This appears to be hiding the tedious and repetitive bits of cross-platform multi-device programming.

WindowsPhone1Android1iPhone1

There's more on Xamarin and Xamarin Forms at http://xamarin.com/forms and sample code here. Check out the code for the Hanselman App(s) at https://github.com/jamesmontemagno/Hanselman.Forms.



© 2014 Scott Hanselman. All rights reserved.