Building a Weather App for Multiple Form Factors

Xamarin.Forms isn't just for creating apps for phones; it can also be used to create apps for tablets and desktop computers. In this chapter, we will build an app that will work on all of these platforms. As well as using three different form factors, we are also going to be working on three different operating systems: iOS, Android, and Windows.

The following topics will be covered in this chapter:

  • How to use FlexLayout in Xamarin.Forms
  • How to use VisualStateManager
  • How to use different views for different form factors
  • How to use behaviors

Let's get started!

Technical requirements

To work on this project, we need to have Visual Studio for Mac or PC installed, as well as the Xamarin components. See Chapter 1,Introduction to Xamarin, for more details on how to set up your environment. To build an iOS app using Visual Studio for PC, you have to have a Mac connected. If you don't have access to a Mac at all, you can choose to just work on the Windows and Android parts of this project. Similarly, if you only have Windows, you can choose to work on only the iOS and Android parts of this project.

Project overview

Applications for iOS and Android can run on bothphones and tablets. Often, apps are just optimized for phones. In this chapter, we will build an app that will work on different form factors, but we aren't going to stick to just phones and tablets – we are going to target desktop computers as well. The desktop version will be for the Universal Windows Platform (UWP).

The app that we are going to build is a weather app that displays the weather forecast based on the location of the user.

Building the weather app

It's time to start building the app. Create a new blank Xamarin.Forms app using .NET Standard as the Code Sharing Strategy and select iOS,Android, and Windows (UWP) as the platforms. We will name the project Weather.

As the data source for this app, we will use an external weather API. This project will use OpenWeatherMap, a service that offers a couple of free APIs. You can find this service at https://openweathermap.org/api. We will usethe5 day / 3 hour forecast service in this project, which provides a 5-day forecast in 3-hour intervals. To use theOpenWeatherAPI, we have to create an account to get an API key. If you don't want to create an API key, you can mock the data instead.

Creating models for the weather data

Before we write the code to fetch data from the external weather service, we will create models in order to deserialize the results from the service. We will do this so that we have a common model that we can use to return data from the service.

The easiest way to generate models to use when we are deserializing results from the service is to make a call to the service either in the browser or with a tool (such as Postman) to see the structure of the JSON. We can either create classes manually or use a tool that can generate C# classes from the JSON. One tool that can be used is quicktype, which can be found at https://quicktype.io/.

If you generate them manually, make sure to set the namespace to Weather.Models.

As mentioned previously, you can also create these models manually. We will describe how to do this in the next section.

Adding the weather API models manually

If you wish to add the models manually, then go through the following instructions. We will be adding a single code file called WeatherData.cs, which will contain multiple classes:

  1. In the Weather project, create a folder called Models.
  2. Add a file called WeatherData.cs.
  3. Add the following code:
using System.Collections.Generic;

namespace Weather.Models
{
public class Main
{
public double temp { get; set; }
public double temp_min { get; set; }
public double temp_max { get; set; }
public double pressure { get; set; }
public double sea_level { get; set; }
public double grnd_level { get; set; }
public int humidity { get; set; }
public double temp_kf { get; set; }
}

public class Weather
{
public int id { get; set; }
public string main { get; set; }
public string description { get; set; }
public string icon { get; set; }
}

public class Clouds
{
public int all { get; set; }
}

public class Wind
{
public double speed { get; set; }
public double deg { get; set; }
}

public class Rain
{
}

public class Sys
{
public string pod { get; set; }
}

public class List
{
public long dt { get; set; }
public Main main { get; set; }
public List<Weather> weather { get; set; }
public Clouds clouds { get; set; }
public Wind wind { get; set; }
public Rain rain { get; set; }
public Sys sys { get; set; }
public string dt_txt { get; set; }
}

public class Coord
{
public double lat { get; set; }
public double lon { get; set; }
}

public class City
{
public int id { get; set; }
public string name { get; set; }
public Coord coord { get; set; }
public string country { get; set; }
}

public class WeatherData
{
public string cod { get; set; }
public double message { get; set; }
public int cnt { get; set; }
public List<List> list { get; set; }
public City city { get; set; }
}
}

As you can see, there are quite a lot of classes. This maps directly to the response we get from the service.

Adding the app-specific models

In this section, we will create the models that our app will translate the Weather API models into. Let's start by adding the WeatherData class (unless you created this manually in the preceding section):

  1. Create a new folder called Models in the Weather project.
  2. Add a new file called WeatherData.
  3. Paste or write the code for the classes based on the JSON. If code other than the properties is generated, ignore it and just use the properties.
  4. Rename MainClass (this is what quicktype names the root object) WeatherData.

Now, we will create models based on the data we are interested in. This will make the rest of the code more loosely coupled to the data source.

Adding the ForecastItem model

The first model we are going to add is ForecastItem, which represents a specific forecast for a point in time. We do this as follows:

  1. In the Weather project and in the Models folder, create a new class called ForecastItem.
  2. Add the following code:
using System;
using System.Collections.Generic;

namespace Weather.Models
{
public class ForecastItem
{
public DateTime DateTime { get; set; }
public string TimeAsString => DateTime.ToShortTimeString();
public double Temperature { get; set; }
public double WindSpeed { get; set; }
public string Description { get; set; }
public string Icon { get; set; }
}
}

Adding the Forecast model

Next, we'll create a model called Forecast that will keep track of a single forecast for a city. Forecast keeps a list of multiple ForeCastItem objects, each representing a forecast for a specific point in time. Let's set this up:

  1. In the Weather project, create a new class called Forecast.
  2. Add the following code:
using System;
using System.Collections.Generic;

namespace Weather.Models
{
public class Forecast
{
public string City { get; set; }
public List<ForecastItem> Items { get; set; }
}
}

Now that we have our models for both the Weather API and the app, we need to fetch data from the Weather API.

Creating a service to fetch the weather data

To make it easier to change the external weather service and to make the code more testable, we will create an interface for the service. Here's how we go about it:

  1. In the Weather project, create a new folder called Services.
  2. Create a new public interface called IWeatherService.
  3. Add a method for fetching data based on the location of the user, as shown in the following code. Name the method GetForecast:
publicinterfaceIWeatherService
{
Task<Forecast>GetForecast(doublelatitude,doublelongitude);
}

When we have an interface, we can create an implementation for it, as follows:

  1. In the Services folder, create a new class called OpenWeatherMapWeatherService.
  2. Implement the interface and add the async keyword to the GetForecast method.
  3. The code should look as follows:
using System;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;
using Weather.Models;

namespace Weather.Services
{
publicclassOpenWeatherMapWeatherService:IWeatherService
{
publicasyncTask<Forecast>GetForecast(doublelatitude,
doublelongitude)
{
}
}
}

Before we call the OpenWeatherMap API, we need to build a URI for the call to the Weather API. This will be a GET call, and the latitude and longitude of the position will be added as query parameters. We will also add the API key and the language that we would like the response to be in. Let's set this up:

  1. In WeatherProject, open the OpenWeatherMapWeatherService class.
  1. Add the code marked in bold in the following code snippet:
public class OpenWeatherMapWeatherService : IWeatherService
{
public async Task<Forecast> GetForecast(double latitude, double
longitude)
{
varlanguage=
CultureInfo.CurrentUICulture.TwoLetterISOLanguageName;

var apiKey = "{AddYourApiKeyHere}";
varuri=
$"https://api.openweathermap.org/data/2.5/forecast?
lat={latitude}&lon={longitude}&units=metric&lang=
{language}&appid={apiKey}"
;
}
}

In order to deserialize the JSON that we will get from the external service, we will use Json.NET, the most popular NuGet package for serializing and deserializing JSON in .NET applications. We can install it like so:

  1. Open the NuGet Package Manager.
  2. Install the Json.NET package. The ID of the package is Newtonsoft.Json.

To make a call to the Weather service, we will use the HttpClient class and the GetStringAsyncmethod, as follows:

  1. Create a new instance of the HttpClient class.
  2. Call GetStringAsync and pass the URL as the argument.
  3. Use the JsonConvert class and the DeserializeObject method from Json.NET to convert the JSON string into an object.
  4. Map the WeatherData object to a Forecast object.
  5. The code for this should look like the bold code shown in the following snippet:
public async Task<Forecast> GetForecast(double latitude, double  
longitude)
{
var language =
CultureInfo.CurrentUICulture.TwoLetterISOLanguageName;
var apiKey = "{AddYourApiKeyHere}";
var uri = $"https://api.openweathermap.org/data/2.5/forecast?
lat={latitude}&lon={longitude}&units=metric&lang=
{language}&appid={apiKey}";

varhttpClient=newHttpClient();
varresult=awaithttpClient.GetStringAsync(uri);

vardata=JsonConvert.DeserializeObject<WeatherData>(result);

varforecast=newForecast()
{
City=data.city.name,
Items=data.list.Select(x=>newForecastItem()
{
DateTime=ToDateTime(x.dt),
Temperature=x.main.temp,
WindSpeed=x.wind.speed,
Description=x.weather.First().description,
Icon=
$"http://openweathermap.org/img/w/{
x.weather.First().icon}.png"
}).ToList()
};
return forecast;

}
To optimize the performance of the app, we can use HttpClient as a singleton and reuse it for all network calls in the application. The following information is from Microsoft's documentation: HttpClient is intended to be instantiated once and reused throughout the life of an application. Instantiating an HttpClient class for every request will exhaust the number of sockets available under heavy loads. This will result in SocketException errors. This can be found at https://docs.microsoft.com/en-gb/dotnet/api/system.net.http.httpclient?view=netstandard-2.0.

In the preceding code, we have a call to a ToDateTime method, which is a method that we will need to create. This method converts the date from a Unix timestamp into a DateTime object, as shown in the following code:

privateDateTimeToDateTime(doubleunixTimeStamp)
{
DateTimedateTime=newDateTime(1970,1,1,0,0,0,0,
DateTimeKind.Utc);
dateTime=dateTime.AddSeconds(unixTimeStamp).ToLocalTime();
returndateTime;
}
By default, HttpClient uses the Mono implementation of HttpClient (iOS and Android). To increase performance, we can use a platform-specific implementation instead. For iOS, use NSUrlSession. This can be set in the project settings of the iOS project under the iOS Build tab. For Android, useAndroid. This can be set in the project settings of the Android project under Android Options |Advanced.

Configuring the applications so they use location services

To be able to use location services, we need to carry out some configurations on each platform. We will use Xamarin.Essentials and the classes it contains. Ensure that you have installed Xamarin.Essentials from NuGet for all the projects in the solution before going through the steps in the following sections.

Configuring the iOS app so that it uses location services

To use location services in an iOS app, we need to add a description to indicate why we want to use the location in the info.plist file. In this app, we only need to get the location when we are using the app, so we only need to add a description for this. Let's set this up:

  1. Open info.plist in Weather.iOS with the XML (Text) Editor.
  2. Add the NSLocationWhenInUseUsageDescription key using the following code:
<key>NSLocationWhenInUseUsageDescription</key>
<string>We are using your location to find a forecast for you</string>

Configuring the Android app so that it uses location services

For Android, we need to set up the app so that it requires the following two permissions:

  • ACCESS_COARSE_LOCATION
  • ACCESS_FINE_LOCATION

We can set this in the AndroidManifest.xml file, which can be found in the Properties folder in the Weather.Android project. However, we can also set this in the project properties on the Android Manifest tab, as shown in the following screenshot:

When we request permissions in an Android app, we also need to add the following code to the MainActivity.cs file in the Android project:

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);
}

For Android, we also need to initialize Xamarin.Essentials. We will do this in the OnCreate method of MainActivity:

protectedoverridevoid OnCreate(Bundle savedInstanceState)
{
TabLayoutResource = Resource.Layout.Tabbar;
ToolbarResource = Resource.Layout.Toolbar;

base.OnCreate(savedInstanceState);

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

Configuring the UWP app so that it uses location services

Since we will be using location services in the UWP app, we need to add the Location capability under Capabilities in the Package.appxmanifest file of the Weather.UWP project, as shown in the following screenshot:

Creating the ViewModel class

Now that we have a service that's responsible for fetching weather data from the external weather source, it's time to create a ViewModel. First, however, we will create a base view model where we can put the code that can be shared between all the ViewModels of the app. Let's set this up:

  1. Create a new folder called ViewModels.
  2. Create a new class called ViewModel.
  1. Make the new class public and abstract.
  2. Add and implement the INotifiedPropertyChanged interface. This is necessary because we want to use data bindings.
  3. Add a Set method. This will make it easier to raise the PropertyChanged event from the INotifiedPropertyChanged interface. The method will check whether the value has changed. If it has, it will raise the event:
public abstract class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protectedvoidSet<T>(refTfield,TnewValue,
[CallerMemberName]stringpropertyName=null)
{
if(!EqualityComparer<T>.Default.Equals(field,
newValue))
{
field=newValue;
PropertyChanged?.Invoke(this,new
PropertyChangedEventArgs(propertyName));
}
}
}
The CallerMemberName attribute can be used in a method body if you want the name of the method or the property that made the call to the method to be a parameter. Note that we can always override this by simply passing a value to it. The default value of the parameter is required when you are using the CallerMember attribute.

We now have a base view model. We can use this for the view model that we are creating now, as well as for all of the other view models that we will add later.

Now, it's time to create MainViewModel, which will be the ViewModel for our MainView in the app. Perform the following steps to do so:

  1. In the ViewModels folder, create a new class called MainViewModel.
  2. Add the abstract ViewModel class as a base class.
  3. Because we are going to use constructor injection, we will add a constructor with the IWeatherService interface as a parameter.
  1. Create a read-only private field. We will use this to store the IWeatherService instance:
publicclassMainViewModel:ViewModel
{
privatereadonlyIWeatherServiceweatherService;

publicMainViewModel(IWeatherServiceweatherService)
{
this.weatherService=weatherService;
}
}

MainViewModel takes any object that implements IWeatherService and stores a reference to that service in a field. We will be adding functionality that will fetch weather data in the next section.

Getting the weather data

Now, we will create a new method for loading the data. This will be a three-step process. First, we will get the location of the user. Once we have this, we can fetch data related to that location. The final step is to prepare the data that the views can consume to create a user interface for the user.

To get the location of the user, we will use Xamarin.Essentials, which we installed earlier as a NuGet package, and the Geolocation class, which exposes methods that can fetch the location of the user. Perform the following steps:

  1. Create a new method called LoadData. Make it an asynchronous method that returns a Task.
  2. Use the GetLocationAsync method on the Geolocation class to get the location of the user.
  3. Pass the latitude and longitude from the result of the GetLocationAsync call and pass it to the GetForecast method on the object that implements IWeatherService using the following code:
publicasyncTaskLoadData()
{
varlocation=awaitGeolocation.GetLocationAsync();
varforecast=awaitweatherService.GetForecast
(location.Latitude,location.Longitude);
}

Grouping the weather data

When we present the weather data, we will group it by day so that all of the forecasts for one day will be under the same header. To do this, we will create a new model called ForecastGroup. To make it possible to use this model with the Xamarin.Forms CollectionView, it has to have an IEnumerable type as the base class. Let's set this up:

  1. Create a new class called ForecastGroup in the Models folder.
  2. Add List<ForecastItem> as the base class for the new model.
  3. Add an empty constructor and a constructor that has a list of ForecastItem instances as a parameter.
  4. Add a Date property.
  5. Add a property, DateAsString, that returns the Date property as a short date string.
  6. Add a property, Items, that returns the list of ForecastItem instances, as shown in the following code:
using System;
using System.Collections.Generic;

namespace Weather.Models
{
publicclassForecastGroup:List<ForecastItem>
{
publicForecastGroup(){}
publicForecastGroup(IEnumerable<ForecastItem>items)
{
AddRange(items);
}

publicDateTimeDate{get;set;}
publicstringDateAsString=>Date.ToShortDateString();
publicList<ForecastItem>Items=>this;
}
}

When we have done this, we can update MainViewModel with two new properties, as follows:

  1. Create a property called City for the name of the city that we are fetching the weather data for.
  2. Create a property called Days that will contain the grouped weather data.
  1. The MainViewModel class should look like the bold code shown in the following snippet:
public class MainViewModel : ViewModel
{
privatestringcity;
publicstringCity
{
get=>city;
set=>Set(refcity,value);
}

privateObservableCollection<ForecastGroup>days;
publicObservableCollection<ForecastGroup>Days
{
get=>days;
set=>Set(refdays,value);
}

// Rest of the class is omitted for brevity
}

Now, we are ready to group the data. We will do this in the LoadData method. We will loop through the data from the service and add items to various groups, as follows:

  1. Create an itemGroups variable of the List<ForecastGroup>type.
  2. Create a foreach loop that loops through all the items in the forecast variable.
  3. Add an if statement that checks whether the itemGroups property is empty. If it is empty, add a new ForecastGroup to the variable and continue to the next item in the item list.
  4. Use the SingleOrDefault method (this is an extension method from System.Linq) on the itemGroups variable to get a group based on the date of the current ForecastItem. Add the result to a new variable, group.
  5. If the group property is null, then there is no group with the current day in the list of groups. If this is the case, a new ForecastGroup should be added to the list in the itemGroups variable. The code will continue executing until it gets to the next forecast item in the forecast.Items list. If a group is found, it should be added to the list in the itemGroups variable.
  6. After the foreach loop, set the Days property with a new ObservableCollection<ForecastGroup>and use the itemGroups variable as an argument in the constructor.
  7. Set the City property to the City property of the forecast variable.
  1. The LoadData method should now look as follows:
public async Task LoadData()
{
varitemGroups=newList<ForecastGroup>();

foreach (variteminforecast.Items)
{
if (!itemGroups.Any())
{
itemGroups.Add(newForecastGroup(
newList<ForecastItem>(){item})
{Date=item.DateTime.Date});
continue;
}

vargroup=itemGroups.SingleOrDefault(x=>x.Date==
item.DateTime.Date);

if (group==null)
{
itemGroups.Add(newForecastGroup(
newList<ForecastItem>(){item})
{Date=item.DateTime.Date});

continue;
}

group.Items.Add(item);
}

Days=newObservableCollection<ForecastGroup>(itemGroups);
City=forecast.City;
}
Don't use the Add method on ObservableCollection when you want to add more than a couple of items. It is better to create a new instance of ObservableCollection and pass a collection to the constructor. The reason for this is that every time you use the Add method, you will have a binding to it from the view, which will cause the view to be rendered. We will get better performance if we avoid using the Add method.

Creating a Resolver

Now, we need to create a helper class for Inversion of Control (IoC). This will help us create types based on a configured IoC container. In this project, we will use Autofac as the IoC library. Let's set this up:

  1. Install the Autofac NuGet package in the Weather project.
  2. Create a new class called Resolver in the Weather project.
  3. Add a private static field called container of the IContainer type (from Autofac).
  4. Add a public static method called Initialize with IContainer as a parameter. Set the value of the parameter to the container field.
  5. Add a generic public static method called Resolve<T>, which will return an instance of an object of the type specified with the T parameter. The Resolve<T> method will then call the Resolve<T> method on the IContainer instance that was passed to it during initialization.
  6. The code should now look as follows:
using Autofac;

namespace Weather
{
publicclassResolver
{
privatestaticIContainercontainer;

publicstaticvoidInitialize(IContainercontainer)
{
Resolver.container=container;
}

publicstaticTResolve<T>()
{
returncontainer.Resolve<T>();
}
}
}

Creating a bootstrapper

In this section, we will create a Bootstrapper class. We will use this to set up the common configurations that we need in the startup phase of the app. Usually, there is one part of the bootstrapper for each target platform and one that is shared for all platforms. In this project, we only need the shared part. Let's set this up:

  1. In the Weather project, create a new class called Bootstrapper.
  2. Add a new public static method called Init.
  3. Create a new ContainerBuilder and register the types to container.
  4. Create a Container by using the Build method of ContainerBuilder. Create a variable called container that contains the instance of Container.
  5. Use the Initialize method on Resolver and pass the container variable as an argument.
  6. The Bootstrapper class should now look as follows:
using Autofac;
using TinyNavigationHelper.Forms;
using Weather.Services;
using Weather.ViewModels;
using Weather.Views;
using Xamarin.Forms;

namespace Weather
{
publicclassBootstrapper
{
publicstaticvoidInit()
{
varcontainerBuilder=newContainerBuilder();
containerBuilder.RegisterType
<OpenWeatherMapWeatherService>().As
<IWeatherService>();
containerBuilder.RegisterType<MainViewModel>();

varcontainer=containerBuilder.Build();

Resolver.Initialize(container);
}
}
}

Call the Init method of Bootstrapper in the constructor in the App.xaml.cs file after the call to the InitializeComponent method. Also, set the MainPage property to MainView, as shown in the following code:

publicApp()
{
InitializeComponent();
Bootstrapper.Init();
MainPage = new NavigationPage(new MainView());

}

Creating the view for tablets and desktop computers

The next step is to create the view that we will use when the app is running on a tablet or a desktop computer. Let's set this up:

  1. Create a new folder in the Weather project called Views.
  2. Create a new Content Page with XAML called MainView.
  3. Use Resolver in the constructor of the view to set BindingContext to MainViewModel, as shown in the following code:
public MainView ()
{
InitializeComponent ();
BindingContext = Resolver.Resolve<MainViewModel>();
}

To trigger the LoadDatamethod inMainViewModel,call the LoadData method by overriding theOnAppearing method on the main thread. We need to make sure that the call is executed on the UI thread since it will interact with the user interface.

To do this, perform the following steps:

  1. In the Weather project, open the MainView.xaml.cs file.
  2. Create an override of the OnAppearing method.
  1. Add the code shown in bold in the following snippet:
protected override void OnAppearing()
{
base.OnAppearing();

if (BindingContext is MainViewModel viewModel)
{
MainThread.BeginInvokeOnMainThread(async () =>
{
await viewModel.LoadData();
});
}
}

In the XAML, add a binding for the Title property of ContentPage to the City property in ViewModel, as follows:

  1. In the Weather project, open the MainView.xaml file.
  2. Add the Title binding to the ContentPage element, as highlighted in bold in the following code snippet:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Weather.Controls"
x:Class="Weather.Views.MainView"
Title
="{Binding City}">

Using FlexLayout

In Xamarin.Forms, we can use CollectionView or ListView if we want to show a collection of data. Using both CollectionView and ListView works great in most cases, and we will use CollectionView later in this chapter, but ListView can only show data vertically. In this app, we want to show data in both directions. In the vertical direction, we will have the days (we group forecasts based on days), while in the horizontal direction, we will have the forecasts within a particular day. We also want the forecasts within a day to wrap if there is not enough space for all of them in one row. CollectionView can show data in a horizontal direction, but it will not wrap. With FlexLayout, we are able to add items in both directions and we can use BindableLayout to bind items to it. When we are using BindableLayout, we will use ItemSource and ItemsTemplate as attached properties.

Perform the following steps to build the view:

  1. Add a Grid as the root view of the page.
  2. Add a ScrollView to Grid. We need this to be able to scroll if the content is higher than the height of the page.
  3. Add a FlexLayout to ScrollView and set the direction to Column so that the content will be in a vertical direction.
  4. Add a binding to the Days property in MainViewModel using BindableLayout.ItemsSource.
  5. Set a DataTemplate to the content of ItemsTemplate, as shown in the following code:
<Grid>
<ScrollViewBackgroundColor="Transparent">
<FlexLayout BindableLayout.ItemsSource="{Binding Days}"
Direction="Column">
<BindableLayout.ItemTemplate>
<DataTemplate>
<!--Content will be added here -->
</DataTemplate>
</BindableLayout.ItemTemplate>
</FlexLayout>
</ScrollView>
</Grid>

The content for each item will be a header with the date and a horizontal FlexLayout with the forecasts for the day. Let's set this up:

  1. In the Weather project, open the MainView.xaml file.
  2. Add StackLayout so that the children we add to it will be placed in a vertical direction.
  3. Add ContentView to StackLayoutwith Padding set to 10 and BackgroundColor set to #9F5010. This will be the header. The reason we need ContentView is that we want to have padding around the text.
  4. Add Labelto ContentView with TextColor set to White and FontAttributes set to Bold.
  5. Add a binding to DateAsString for the Text property of Label.
  6. The code should be placed at the <!-- Content will be added here --> comment and should look as follows:
<StackLayout>
<ContentView Padding="10" BackgroundColor="#9F5010">
<Label Text="{Binding DateAsString}" TextColor="White"
FontAttributes="Bold" />
</ContentView>
</StackLayout>

Now that we have the date in the user interface, we need to add a FlexLayout that will repeat through any Items in MainViewModel. Perform the following steps to do so:

  1. Add a FlexLayout after the </ContentView> tag, but before the </StackLayout> tag.
  2. Set JustifyContent to Startto set the Items so that they're added from the left-hand side, without distributing them over the available space.
  3. Set AlignItems to Start to set the content to the left of each item in FlexLayout, as shown in the following code:
<FlexLayout BindableLayout.ItemsSource="{BindingItems}"Wrap="Wrap"
JustifyContent="Start"AlignItems="Start">

After defining FlexLayout, we need to provide an ItemsTemplate that defines how each item in the list should be rendered. Continue adding the XAML directly under the <FlexLayout> tag you just added. as follows:

  1. Set the ItemsTemplate property to DataTemplate.
  2. FillDataTemplate with elements, as shown in the following code:
If we want to add formatting to a binding, we can use StringFormat. In this case, we want to add the degree symbol after the temperature. We can do this by using the {Binding Temperature, StringFormat='{0}° C'} phrase. With the StringFormat property of the binding, we can format data with the same arguments that we would use if we were to do this in C#. This is the same as string.Format("{0}° C", Temperature) in C#. We can also use it to format a date; for example, {Binding Date, StringFormat='yyyy'}. In C#, this would look like Date.ToString("yyyy").
<BindableLayout.ItemTemplate>
<DataTemplate>
<StackLayout Margin="10" Padding="20" WidthRequest="150"
BackgroundColor="#99FFFFFF">
<Label FontSize="16" FontAttributes="Bold"
Text="{Binding TimeAsString}"
HorizontalOptions="Center" />
<Image WidthRequest="100" HeightRequest="100"
Aspect="AspectFit" HorizontalOptions="Center"
Source="{Binding Icon}" />
<Label FontSize="14" FontAttributes="Bold"
Text="{Binding Temperature, StringFormat='{0}° C'}"
HorizontalOptions="Center" />
<Label FontSize="14" FontAttributes="Bold"
Text="{Binding Description}"
HorizontalOptions="Center" />
</StackLayout>
</DataTemplate>
</BindableLayout.ItemTemplate>
The AspectFill phrase, as a value of the Aspect property for Image, means that the whole image willalways be visible and that the aspects will not be changed. The AspectFitphrase will also keep the aspect of an image, but the image can be zoomed into and out of and cropped so that it fills the wholeImageelement. The last value thatAspectcan be set to,Fill, means that the image can be stretched or compressed to match the Image view to ensure that the aspect ratio is kept.

Adding a toolbar item to refresh the weather data

To be able to refresh the data without restarting the app, we will add a Refresh button to the toolbar. MainViewModel is responsible for handling any logic that we want to perform, and we must expose any action as an ICommand that we can bind to.

Let's start by creating the Refresh command property on MainViewModel:

  1. In the Weather project, open the MainViewModel class.
  2. Add an ICommand property called Refresh and a get method that returns a new Command.
  3. Add an action as an expression to the constructor of the Commandthat calls the LoadData method, as shown in the following code:
publicICommandRefresh=>newCommand(async()=>
{
awaitLoadData();
});

Now that we have definedCommand, we need to bind it to the user interface so that when the user clicks the toolbar button, the action will be executed.

To do this, perform the following steps:

  1. In the Weather app, open the MainView.xaml file.
  2. Add a new ToolbarItem with the Text property set to Refresh to the ToolbarItems property of ContentPage and set the Icon property to refresh.png (the icon can be downloaded from GitHub; see https://github.com/PacktPublishing/Xamarin.Forms-Projects/tree/master/Chapter-5).
  3. Bind the Command property to the Refresh property in MainViewModel, as shown in the following code:
<ContentPage.ToolbarItems>
<ToolbarItemIcon="refresh.png"Text="Refresh"Command="{Binding
Refresh}"/>
</ContentPage.ToolbarItems>

That's all for refreshing the data. Now, we need some kind of indicator that the data is loading.

Adding a loading indicator

When we refresh the data, we want to show a loading indicator so that the user knows that something is happening. To do this, we will add ActivityIndicator, which is what this control is called in Xamarin.Forms. Let's set this up:

  1. In the Weather project, open the MainViewModel class.
  2. Add a Boolean property called IsRefreshing to MainViewModel.
  3. Set the IsRefreshing property to true at the beginning of the LoadData method.
  4. At the end of the LoadData method, set the IsRefreshing property to false, as shown in the following code:
private bool isRefreshing;
public bool IsRefreshing
{
get => isRefreshing;
set => Set(ref isRefreshing, value);
}

public async Task LoadData()
{
IsRefreshing = true;
.... // The rest of the code is omitted for brevity
IsRefreshing = false;
}

Now that we have added some code to MainViewModel, we need to bind the IsRefreshing property to a user interface element that will be displayed when the IsRefreshing property is true, as shown in the following code:

  1. In MainView.xaml, add a Frame after ScrollView as the last element in Grid.
  2. Bind the IsVisible property to the IsRefreshing method that we created in MainViewModel.
  3. Set HeightRequest and WidthRequest to 100.
  4. Set VerticalOptions and HorizontalOptions to Center so that Frame will be in the middle of the view.
  5. Set BackgroundColor to #99000000 to set the background to white with a little bit of transparency.
  6. Add ActivityIndicator to Frame with Color set to Black and IsRunning set to True, as shown in the following code:
<FrameIsVisible="{BindingIsRefreshing}"
BackgroundColor="#99FFFFFF"
WidthRequest="100"HeightRequest="100"
VerticalOptions="Center"
HorizontalOptions="Center">
<ActivityIndicatorColor="Black"IsRunning="True"/>
</Frame>

This will create a spinner that will be visible while data is loading, which is a really good practice when creating any user interface. Now, we'll add a background image to make the app look a bit nicer.

Setting a background image

The last thing we will do to this view, for the moment, is add a background image. The image we will be using in this example is a result of a Google search for images that are free to use. Let's set this up:

  1. In the Weather project, open the MainView.xaml file.
  2. Set the Background property of ScrollView to Transparent.
  1. Add an Image element in Grid with UriImageSource as the value of the Source property.
  2. Set the CachingEnabled property to true and CacheValidity to 5. This means that the image will be cached in 5 days.
  3. The XAML should now look as follows:
<ContentPage 
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Weather.Controls"
x:Class="Weather.Views.MainView" Title="{Binding
City}">
<ContentPage.ToolbarItems>
<ToolbarItem Icon="refresh.png" Text="Refresh" Command="
{Binding Refresh}" />
</ContentPage.ToolbarItems>

<Grid>
<ImageAspect="AspectFill">
<Image.Source>
<UriImageSource
Uri="https://upload.wikimedia.org/wikipedia/commons/7/79/
Solnedg%C3%A5ng_%C3%B6ver_Laholmsbukten_augusti_2011.jpg"
CachingEnabled="true"CacheValidity="1"/>

</Image.Source>
</Image>
<ScrollViewBackgroundColor="Transparent">
<!-- The rest of the code is omitted for brevity -->

We can also set the URL directly in the Source property by using <Image Source="https://ourgreatimage.url" />. However, if we do this, we can't specify caching for the image.

Creating the view for phones

Structuring content on a tablet and on a desktop computer is very similar in many ways. On phones, however, we are much more limited in what we can do. Therefore, in this section, we will create a specific view for this app when it's used on phones. To do so, perform the following steps:

  1. Create a new XAML-based Content Page in the Views folder.
  2. Call the new view MainView_Phone.
  1. Use Resolver in the constructor of the view to set BindingContext to MainViewModel, as shown in the following code:
publicMainView_Phone()
{
InitializeComponent();
BindingContext=Resolver.Resolve<MainViewModel>();
}

To trigger the LoadData method in MainViewModel, call the LoadData method by overriding the OnAppearing method on the main thread. To do this, perform the following steps:

  1. In the Weather project, open the MainView_Phone.xaml.cs file.
  2. Add the override of the OnAppearing method, as shown in the following code:
protectedoverridevoidOnAppearing()
{
base.OnAppearing();

if(BindingContextisMainViewModelviewModel)
{
MainThread.BeginInvokeOnMainThread(async()=>
{
awaitviewModel.LoadData();
});
}
}

In the XAML, add a binding for the Title property of ContentPageto theCityproperty in ViewModel, as follows:

  1. In the Weather project, open the MainView_Phone.xaml file.
  2. Add the Title property with a binding to the City property of MainViewModel, as shown in the following code:
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Weather.Controls"
x:Class="Weather.Views.MainView_Phone"
Title
="{Binding City}">

Using a grouped CollectionView

We could use FlexLayout for the phone's view, but because we want our user experience to be as good as possible, we will use CollectionView instead. To get the headers for each day, we will use grouping for CollectionView. For FlexLayout, we had ScrollView, but for CollectionView, we don't need this because CollectionView can handle scrolling by default.

Let's continue creating the user interface for the phone's view:

  1. In the Weather project, open the MainView_Phone.xaml file.
  2. Add a CollectionView to the root of the page.
  3. Set a binding to the Days property in MainViewModel for the ItemSource property.
  4. Set IsGrouped to True to enable grouping in CollectionView.
  5. Set BackgroundColor to Transparent, as shown in the following code:
<CollectionViewItemsSource="{BindingDays}" IsGrouped="True"
BackgroundColor="Transparent">
</CollectionView>

To format how each header will look, we will create a DataTemplate, as follows:

  1. Add a DataTemplate to the GroupHeaderTemplate property of CollectionView.
  2. Add the content for the row to DataTemplate, as shown in the following code:
<CollectionView ItemsSource="{Binding Days}" IsGrouped="True" 
BackgroundColor="Transparent">
<CollectionView.GroupHeaderTemplate>
<DataTemplate>
<ContentViewPadding="15,5"
BackgroundColor="#9F5010">
<LabelFontAttributes="Bold"TextColor="White"
Text="{BindingDateAsString}"
VerticalOptions="Center"/>
</ContentView>
</DataTemplate>
</CollectionView.GroupHeaderTemplate>

</CollectionView>

To format how each forecast will look, we will create a DataTemplate, as we did with the group header. Let's set this up:

  1. Add a DataTemplate to the ItemTemplate property of CollectionView.
  2. In DataTemplate, add a Grid that contains four columns. Use the ColumnDefinition property to specify the width of the columns. The second column should be 50; the other three will share the rest of the space. We will do this by setting Width to *.
  3. Add the following content to Grid:
<CollectionView.ItemTemplate>
<DataTemplate>
<GridPadding="15,10"ColumnSpacing="10"
BackgroundColor="#99FFFFFF">
<Grid.ColumnDefinitions>
<ColumnDefinitionWidth="*"/>
<ColumnDefinitionWidth="50"/>
<ColumnDefinitionWidth="*"/>
<ColumnDefinitionWidth="*"/>
</Grid.ColumnDefinitions>
<LabelFontAttributes="Bold"Text="{Binding
TimeAsString}"VerticalOptions="Center"/>
<ImageGrid.Column="1"HeightRequest="50"
WidthRequest="50"Source="{BindingIcon}"
Aspect="AspectFit"VerticalOptions="Center"/>
<LabelGrid.Column="2"Text="{BindingTemperature,
StringFormat='{0}° C'}"
VerticalOptions="Center"/>
<LabelGrid.Column="3"Text="{BindingDescription}"
VerticalOptions="Center"/>
</Grid>
</DataTemplate>
</CollectionView.ItemTemplate>

Adding pull to refresh functionality

For the tablet and desktop versions of the view, we added a button to the toolbar to refresh the weather forecast. In the phone version of the view, however, we will add pull to refresh functionality, which is a common way to refresh content in a list of data. CollectionView in Xamarin.Forms has no built-in support for pull to refresh like ListView has.

Instead, we can use RefreshView. RefreshView can be used to add pull to refresh behavior to any control. Let's set this up:

  1. Go to MainView_Phone.xaml.
  2. Wrap CollectionView inside RefreshView.
  3. Bind the Refresh property in MainViewModel to the Command property of RefreshView to trigger a refresh when the user performs a pull-to-refresh gesture.
  4. To show a loading icon when the refresh is in progress, bind the IsRefreshing property in MainViewModel to the IsRefreshing property of RefreshView. When we are setting this up, we will also get a loading indicator when the initial load is running, as shown in the following code:
<RefreshView Command="{Binding Refresh}" IsRefreshing="{Binding IsRefreshing}>
<CollectionView ItemsSource="{Binding Days}" IsGrouped="True"
BackgroundColor="Transparent">
....
</CollectionView>
</RefreshView>

Navigating to different views based on the form factor

We now have two different views that should be loaded in the same place in the app. MainView should be loaded if the app is running on a tablet or on a desktop, while MainView_Phone should be loaded if the app is running on a phone.

The Device class in Xamarin.Forms has a staticIdiom property that we can use to check which form factor the app is running on. The value of Idiom can be Phone, Table, Desktop, Watch, or TV. Because we only have one view in this app, we could have used an if statement when we were setting MainPage in App.xaml.cs and checked what the Idiom value was. Here, however, we are going to build a solution that we can alsouse for a bigger app.

One solution is to build a navigation service that we can use to navigate to different views based on a key. Which view will be loaded for which key will be configured when we start the app. With this solution, we can configure different views on the same key on different types of devices. An open source navigation service that we can use for this purpose is TinyNavigationHelper, which can be found at https://github.com/TinyStuff/TinyNavigationHelper. It was created by the authors of this book.

There is also an MVVM library called TinyMvvm that includes TinyNavigationHelper as a dependency. The TinyMvvm library is a library that contains helper classes so that you can get started quickly with MVVM in a Xamarin.Forms app. We created TinyMvvm because we wanted to avoid writing the same code again and again. You can read more about this at https://github.com/TinyStuff/TinyMvvm.

Perform the following steps to add TinyNavigationHelper to the app:

  1. Install the TinyNavigationHelper.Forms NuGet package in the Weather project.
  2. Go to Bootstrapper.cs.
  3. At the start of the Execute method, create a FormsNavigationHelper and pass the current application to the constructor.
  4. Add an if statement to check whether Idiom is Phone. If this is true, the MainView_Phone view should be registered for the MainView key.
  5. Add an else statement that registers MainView for the MainView key.
  6. The Bootstrapper class should now look as follows, with the new code marked in bold:
public class Bootstrapper
{
public static void Init()
{
var navigation = new FormsNavigationHelper();

if (Device.Idiom == TargetIdiom.Phone)
{
navigation.RegisterView("MainView",
typeof(MainView_Phone));

}
else
{
navigation.RegisterView("MainView", typeof(MainView));
}

var containerBuilder = new ContainerBuilder();
containerBuilder.RegisterType<OpenWeatherMapWeatherService>
().As<IWeatherService>();
containerBuilder.RegisterType<MainViewModel>();

var container = containerBuilder.Build();

Resolver.Initialize(container);
}
}

Now, we canuse theNavigationHelperclass to set the root view of the app in the constructor of theAppclass, as follows:

  1. In the Weather app, open the App.xaml.cs file.
  2. Locate the constructor of the App class.
  3. Remove the assignment of the MainPage property.
  4. Add the code to set the root view via NavigationHelper.
  5. The constructor should now look like the bold code shown in the following snippet:
publicApp()
{
InitializeComponent();
Bootstrapper.Init();
NavigationHelper.Current.SetRootView("MainView",true);
}

If we want to load different views on different operating systems, we can use the static RuntimePlatform method on the Xamarin.FormsDeviceclass – for example, if(Device.RuntimePlatform == Device.iOS).

Handling states with VisualStateManager

VisualStateManager was introduced in Xamarin.Forms 3.0. It is a way to make changes in the UI from the code. We can define states and set values for selected properties to apply for a specific state. VisualStateManager can be really useful in cases where we want to use the same view for devices with different screen resolutions. It was first introduced in UWP to make it easier to create Windows 10 applications for multiple platforms. This was because Windows 10 could run on Windows Phone, as well as on desktops and tablets (the OS was called Windows 10 Mobile). However, Windows Phone has now been depreciated. VisualStateManager is really interesting for us as Xamarin.Forms developers, especially when both iOS and Android can run on both phones and tablets.

In this project, we will use it to make a forecast item bigger when the app is running in landscape mode on a tablet or on a desktop. We will also make the weather icon bigger. Let's set this up:

  1. In the Weather project, open the MainView.xaml file.
  2. In the first FlexLayout and in DataTemplate, insert a VisualStateManager.VisualStateGroups element into the first StackLayout:
<StackLayoutMargin="10"Padding="20"WidthRequest="150"
BackgroundColor="#99FFFFFF">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</StackLayout>

Regarding VisualStateGroup, we should add two states, as follows:

  1. Add a new VisualState called Portrait to VisualStateGroup.
  2. Create a setter in VisualState and set WidthRequest to 150.
  3. Add another VisualState called Landscape to VisualStateGroup.
  4. Create a setter in VisualStateand set WidthRequestto200, as shown in the following code:
<VisualStateGroup>
<VisualStateName="Portrait">
<VisualState.Setters>
<SetterProperty="WidthRequest"Value="150"/>
</VisualState.Setters>
</VisualState>
<VisualStateName="Landscape">
<VisualState.Setters>
<SetterProperty="WidthRequest"Value="200"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>

We also want the icons in a forecast item to be bigger when the item itself is bigger. To do this, we will use VisualStateManager again. Let's set this up:

  1. Insert a VisualStateManager.VisualStateGroups element into the second FlexLayout and in the Image element in DataTemplate.
  2. Add VisualState for both Portrait and Landscape.
  3. Add setters to the states to set WidthRequest and HeightRequest. The value should be 1oo in the Portrait state and 150 in the Landscape state, as shown in the following code:
<ImageWidthRequest="100"HeightRequest="100"Aspect="AspectFit"HorizontalOptions="Center"Source="{BindingIcon}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualStateName="Portrait">
<VisualState.Setters>
<SetterProperty="WidthRequest"Value="100"/>
<SetterProperty="HeightRequest"Value="100"/>
</VisualState.Setters>
</VisualState>
<VisualStateName="Landscape">
<VisualState.Setters>
<SetterProperty="WidthRequest"Value="150"/>
<SetterProperty="HeightRequest"Value="150"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Image>

Creating a behavior to set state changes

With Behavior, we can add functionality to controls without having to subclass them. With behaviors, we can also create more reusable code than we could if we subclassed a control. The more specific the Behavior we create, the more reusable it willbe. For example, a Behavior that inherits fromBehavior<View>could be used on all controls, but a Behavior that inherits from a Button can onlybe used for buttons. Because of this, we always want to create behaviors with a less specific base class.

When we create a Behavior, we need to override two methods: OnAttached and OnDetachingFrom. It is really important to remove event listeners in the OnDeattached method if we have added them to the OnAttached method. This will make the app use less memory. It is also important to set values back to the values that they had before the OnAppearing method ran; otherwise, we might see some strange behavior, especially if the behavior is in a CollectionView or a ListView that is reusing cells.

In this app, we will create a Behavior for FlexLayout. This is because we can't set the state of an item in FlexLayout from the code behind. We could have added some code to check whether the app runs in portrait or landscape in FlexLayout, but if we use Behavior instead, we can separate that code from FlexLayout so that it will be more reusable. Perform the following steps to do so:

  1. In the Weather project, create a new folder called Behaviors.
  2. Create a new class called FlexLayoutBehavior.
  3. Add Behavior<FlexLayoutView> as a base class.
  4. Create a private field of the FlexLayout type called view.
  5. The code should look as follows:
using System;
using Weather.Controls;
using Xamarin.Essentials;
using Xamarin.Forms;

namespace Weather.Behaviors
{
publicclassFlexLayoutBehavior:Behavior<FlexLayout>
{
privateFlexLayoutview;
}
}

FlexLayout is a class that inherits from the Behavior<FlexLayout> base class. This will give us the ability to override some virtual methods that will be called when we attach and detach the behavior from a FlexLayout.

But first, we need to create a method that will handle the change in state. Perform the following steps to do so:

  1. In the Weather project, open the FlexlayoutBehavior.csfile.
  2. Create a private method called SetState. This method will have a VisualElement and a string argument.
  3. Call VisualStateManager.GoToState and pass the parameters to it.
  4. If the view is of the Layout type, it is possible for there to be child elements that also need to get the new state. To do that, we will loop through all the children of the layout. Instead of just setting the state directly to the children, we will call the SetState method, which is the method that we are already inside. The reason for this is that it is possible that some of the children have their own children:
private void SetState(VisualElement view, string state)
{
VisualStateManager.GoToState(view, state);

if (view is Layout layout)
{
foreach (VisualElement child in layout.Children)
{
SetState(child, state);
}
}
}

Now that we have created the SetState method, we need to write a method that uses it and also determines what state to set. Perform the following steps to do so:

  1. Create a private method called UpdateState.
  2. Run the code on MainThread to check whether the app is running in portrait or landscape mode.
  3. Create a variable called page and set its value to Application.Current.MainPage.

  1. Check whether Width is larger than Height. If this is true, set the VisualState property on the view variable to Landscape. If this is not true, set the VisualState property on the view variable to Portrait, as shown in the following code:
privatevoidUpdateState()
{
MainThread.BeginInvokeOnMainThread(()=>
{
varpage=Application.Current.MainPage;

if(page.Width>page.Height)
{
SetState(view,"Landscape");
return;
}

SetState(view, "Portrait");
});
}

With that, the UpdateState method has been added. Now, we need to override theOnAttachedTomethod, which will be called when the behavior is added toFlexLayout. When it is, we want to update the state by calling this method and also hook it up to theSizeChangedevent ofMainPageso that when the size changes, we will update the state again.

Let's set this up:

  1. In the Weather project, open the FlexLayoutBehaviorfile.
  2. Override the OnAttachedTo method from the base class.
  3. Set the view property to the parameter from the OnAttachedTo method.
  4. Add an event listener to Application.Current.MainPage.SizeChanged. In the event listener, add a call to the UpdateState method, as shown in the following code:
protectedoverridevoidOnAttachedTo(FlexLayoutview)
{
this.view=view;

base.OnAttachedTo(view);

UpdateState();

Application.Current.MainPage.SizeChanged+=
MainPage_SizeChanged;
}

voidMainPage_SizeChanged(objectsender,EventArgse)
{
UpdateState();
}

When we remove behaviors from a control, it's very important to also remove any event handlers from it in order to avoid memory leaks, and in the worst case, the app crashing. Let's do this:

  1. In the Weather project, open the FlexLayoutBehavior.csfile.
  2. Override OnDetachingFromfrom the base class.
  3. Remove the event listener from Application.Current.MainPage.SizeChanged.
  4. Set the view field to null, as shown in the following code:
protectedoverridevoidOnDetachingFrom(FlexLayoutview)
{
base.OnDetachingFrom(view);

Application.Current.MainPage.SizeChanged -=
MainPage_SizeChanged;
this.view=null;
}

Perform the following steps to add the behavior to the view:

  1. In the Weather project, open the MainView.xaml file.
  2. Import the Weather.Behaviors namespace, as shown in the following code:
<ContentPage
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:controls="clr-namespace:Weather.Controls"
xmlns:behaviors="clr-
namespace:Weather.Behaviors"
x:Class="Weather.Views.MainView" Title="{
BindingCity}">

The last thing we will do is add FlexLayoutBehavior to the second FlexLayout, as shown in the following code:

<FlexLayoutItemsSource="{BindingItems}"Wrap="Wrap"
JustifyContent="Start"AlignItems="Start">
<FlexLayout.Behaviors>
<behaviors:FlexLayoutBehavior/>
</FlexLayout.Behaviors>
<FlexLayout.ItemsTemplate>

Summary

In this chapter, we successfully created an app for three different operating systems – iOS, Android, and Windows – and three different form factors – phones, tablets, and desktop computers. To create a good user experience on all platforms and form factors, we used FlexLayout and VisualStateManager. We also learned how to handle to use different views for different form factors, as well as how to use Behaviors.

The next app we will build will be a chat app with real-time communication. In the next chapter, we will take a look at how we can use the SignalR service in Azure as the backend for the chat app.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.26.176