Surface Duo: Simple Liste mit Detail

Seit wenigen Wochen ist das neue Smartphone von Microsoft mit Android und dem Namen Surface Duo auch außerhalb den USA erhältlich. In einem Blog-Beitrag habe ich das Gerät mit den zwei Screens bereits vorgestellt. Nun dachte ich mir, dass es an der Zeit ist einmal einen Blick auf die Entwicklung für das Surface Duo zu werfen. Als Beispiel-Anwendung soll eine einfache Liste dienen, welche bei der Auswahl eines Eintrags ein entsprechendes Detail anzeigt. Das besondere dabei ist, dass wir die beiden Screens des Surface Duos verwenden wollen, aber gleichzeitig auch lauffähig sind, wenn keine zwei Screens zur Verfügung steht.

Die Ausgangslage bildet eine normale Xamarin.Forms App. Zunächst aktualisieren wir alle NuGet-Pakete auf die aktuellste Version. Da das Surface Duo im Vordergrund steht, wollen wir uns auch nur die Android-App anschauen und daher besteht unsere Xamarin.Forms App auch nur aus der .NET Standard Library und dem Android Projekt.

Nun müssen wir noch weitere NuGet-Pakete installieren, damit wir das volle Potenzial des Surface Duos in unsere Xamarin.Forms App ausschöpfen können. Beginnen wir mit dem Paket Xamarin.Forms.DualScreen. Dieses Paket muss in beide Projekte integriert werden. Außerdem brauchen wir noch das Paket Xamarin.DuoSdk, welches wir jedoch nur dem Android-Projekt hinzufügen. Nun öffnen wir die Klasse MainActivity im Android-Projekt und ergänzen den Init-Aufruf des DualScreenServices.

Xamarin.Forms.DualScreen.DualScreenService.Init(this);

Damit sind die Vorbereitungen soweit abgeschlossen und wir können uns jetzt um die Daten-Klasse kümmern. Dafür erstellen wir einen neuen Ordner Models in unserem Projekt und darin dann eine Klasse mit den Namen MyListItem. Diese Klasse besitzt zwei Properties, nämlich Title und Detail. Wir erstellen noch den Konstruktor, welcher eine Zahl entgegen nimmt und entsprechend Titel und Detail mit Inhalt füllt.

public class MyListItem
{
    public string Title { get; }
    public string Detail { get; }


    public MyListItem(int num)
    {
        Title = $"Titel {num}";
        Detail = $"Das ist das Detail mit der Nummer {num}.";
    }
}

Nun erstellen wir einen weiteren Ordner in unserem Projekt mit den Namen Views. Darin erstellen wir eine neue ContentPage mit den Namen MainPage. Hier ergänzen wir zunächst den Title, setzen ein Padding und aktivieren das Material Design.

<?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:dualscreen="clr-namespace:Xamarin.Forms.DualScreen;assembly=Xamarin.Forms.DualScreen"
             x:Class="ListDetailSample.Views.MainPage"
             Title="Surface Duo: List Detail"
             Padding="16"
             Visual="Material">

</ContentPage>

Da wir ja beim Surface Duo zwei Screens zur Verfügung haben, wollen wir die TwoPaneView verwenden, welche es uns ermöglicht für jeden Screen ein Layout zu definieren.

<?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:dualscreen="clr-namespace:Xamarin.Forms.DualScreen;assembly=Xamarin.Forms.DualScreen"
             x:Class="ListDetailSample.Views.MainPage"
             Title="Surface Duo: List Detail"
             Padding="16"
             Visual="Material">

    <dualscreen:TwoPaneView MinWideModeWidth="4000"
                            MinTallModeHeight="4000">

        <dualscreen:TwoPaneView.Pane1>
            
        </dualscreen:TwoPaneView.Pane1>

        <dualscreen:TwoPaneView.Pane2>
            
        </dualscreen:TwoPaneView.Pane2>
        
    </dualscreen:TwoPaneView>

</ContentPage>

Nun fehlt uns aktuell noch der Content, welchen wir auf den Screens anzeigen wollen. Dafür erstellen wir in unserem Views-Ordner eine neue ContentView mit dem Namen ListView.

<?xml version="1.0" encoding="UTF-8"?>
<CollectionView xmlns="http://xamarin.com/schemas/2014/forms"
                xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
                x:Class="ListDetailSample.Views.ListView"
                SelectionMode="Single">

    <CollectionView.ItemsLayout>
        <LinearItemsLayout ItemSpacing="12"
                           Orientation="Vertical" />
    </CollectionView.ItemsLayout>

    <CollectionView.ItemTemplate>
        <DataTemplate>
            <Frame BorderColor="LightGray">
                <StackLayout Padding="5">
                    <Label Text="{Binding Title}"
                           FontSize="Title" />
                </StackLayout>
            </Frame>
        </DataTemplate>
    </CollectionView.ItemTemplate>

</CollectionView>

Nun wechseln wir in die Code-Behind Datei und tauschen auch hier ContentView durch CollectionView aus. Außerdem setzen wir im Konstruktor auch noch die ItemsSource. Hier nutzen wir die Methode Range der Klasse Enumerable und erzeugen uns so 50 Elemente von unserem MyListItem.

public partial class ListView : CollectionView
{
    public ListView()
    {
        InitializeComponent();

        ItemsSource = Enumerable.Range(1, 50)
                .Select(x => new MyListItem(x));
    }
}

Damit wechseln wir zurück auf unsere MainPage und ergänzen nun im Pane1 unsere eigene ListView.

<?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:dualscreen="clr-namespace:Xamarin.Forms.DualScreen;assembly=Xamarin.Forms.DualScreen"
             xmlns:views="clr-namespace:ListDetailSample.Views"
             x:Class="ListDetailSample.Views.MainPage"
             Title="Surface Duo: List Detail"
             Padding="16"
             Visual="Material">

    <dualscreen:TwoPaneView MinWideModeWidth="4000"
                            MinTallModeHeight="4000">

        <dualscreen:TwoPaneView.Pane1>
            <views:ListView />
        </dualscreen:TwoPaneView.Pane1>

        <dualscreen:TwoPaneView.Pane2>

        </dualscreen:TwoPaneView.Pane2>

    </dualscreen:TwoPaneView>

</ContentPage>

Damit können wir uns jetzt um das Detail kümmern. Hierfür erstellen wir eine neue ContentView im unseren Views-Ordner mit dem Namen DetailView.

Auch hier tauschen wir das Root-Objekt aus. Wir verwenden statt ContentView hier Grid und ergänzen die RowDefinitions. Außerdem fügen wir noch den Content hinzu, indem wir den Title und das Detail entsprechend ausgeben.

<?xml version="1.0" encoding="UTF-8"?>
<Grid xmlns="http://xamarin.com/schemas/2014/forms"
      xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
      x:Class="ListDetailSample.Views.DetailView"
      RowDefinitions="Auto,*"
      Padding="16">

    <Label Grid.Row="0">
        <Label.FormattedText>
            <FormattedString>
                <Span Text="Title: "
                      FontAttributes="Bold" />
                <Span Text="{Binding Title}" />
            </FormattedString>
        </Label.FormattedText>
    </Label>

    <Label Grid.Row="1">
        <Label.FormattedText>
            <FormattedString>
                <Span Text="Details: "
                      FontAttributes="Bold" />
                <Span Text="{Binding Detail}" />
            </FormattedString>
        </Label.FormattedText>
    </Label>

</Grid>

Anschließend wechseln wir in die Code-Behind Datei und tauschen auch hier ContentView mit Grid aus. Nun wechseln wir wieder zurück in die MainPage und ergänzen hier Pane2 mit unserer DetailView.

<?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:dualscreen="clr-namespace:Xamarin.Forms.DualScreen;assembly=Xamarin.Forms.DualScreen"
             xmlns:views="clr-namespace:ListDetailSample.Views"
             x:Class="ListDetailSample.Views.MainPage"
             Title="Surface Duo: List Detail"
             Padding="16"
             Visual="Material">

    <dualscreen:TwoPaneView MinWideModeWidth="4000"
                            MinTallModeHeight="4000">

	        <dualscreen:TwoPaneView.Pane1>
	            <views:ListView />
        </dualscreen:TwoPaneView.Pane1>

        <dualscreen:TwoPaneView.Pane2>
            <views:DetailView />
        </dualscreen:TwoPaneView.Pane2>

    </dualscreen:TwoPaneView>

</ContentPage>

Wir öffnen nun die App.xaml.cs Datei und tauschen die MainPage durch unsere neue MainPage aus und in diesem Zusammenhang können wir dann auch die Default-MainPage löschen.

MainPage = new NavigationPage(new MainPage());

Nun können wir auch zum ersten Mal die App ausprobieren. Sofern die App nur auf einem Screen ausgeführt wird, sehen wir die Liste mit unseren Demo-Daten und wenn wir nun die App auf zwei Screens aufspannen, dann sehen wir links die Liste und rechts das Detail, welches sich jedoch aktuell noch nicht aktualisiert. Darum wollen wir uns jetzt kümmern.

Wir öffnen wieder unsere MainPage und vergeben unserer ListView und unserer DetailView jeweils einen Namen, so dass wir über die Code Behind Datei darauf zugreifen können.

<?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:dualscreen="clr-namespace:Xamarin.Forms.DualScreen;assembly=Xamarin.Forms.DualScreen"
	             xmlns:views="clr-namespace:ListDetailSample.Views"
             x:Class="ListDetailSample.Views.MainPage"
             Title="Surface Duo: List Detail"
             Padding="16"
             Visual="Material">

    <dualscreen:TwoPaneView MinWideModeWidth="4000"
                            MinTallModeHeight="4000">

        <dualscreen:TwoPaneView.Pane1>
            <views:ListView x:Name="ListViewPage" />
        </dualscreen:TwoPaneView.Pane1>

        <dualscreen:TwoPaneView.Pane2>
            <views:DetailView x:Name="DetailViewPage" />
        </dualscreen:TwoPaneView.Pane2>

    </dualscreen:TwoPaneView>

</ContentPage>

In der Code Behind Datei erweitern wir den Konstruktor und registrieren das SelectionChanged-Event.

public MainPage()
{
    InitializeComponent();

    ListViewPage.SelectionChanged += OnSelectionChanged;
}

In dem Event überprüfen wir, ob ein Element ausgewählt wurde und rufen dann eine Methode SetBindingContext auf.

private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (e.CurrentSelection == null || e.CurrentSelection.Count == 0)
        return;

    SetBindingContext();
}

In der SetBindingContext holen wir uns das SelectedItem unserer Liste und setzen den BindingContext unserer DetailView.

private void SetBindingContext()
{
    var selectedItem = ListViewPage.SelectedItem 
	        ?? (ListViewPage.ItemsSource as IEnumerable<MyListItem>).FirstOrDefault();

    DetailViewPage.BindingContext = selectedItem;
}

Wenn wir die App nun erneut ausprobieren, stellen wir fest, dass im Single Screen noch nicht viel passiert, aber wenn wir die App nun über beide Screens aufspannen, dann aktualisiert sich jetzt auch das Detail entsprechend. Aber wenn die App auf einem Gerät mit nur einem Screen ausgeführt wird, ist die App noch nicht wirklich funktional. Daher kehren wir zurück zu unserem Code, um auch hier dieses Problem zu lösen.

Um nun die App auch im Single Screen betreiben zu können, erstellen wir eine neue ContentPage im Order Views und nennen diese DetailPage. Wir setzen einen Title, das entsprechende Padding und auch wieder Material als Visual. Anschließend fügen wir als Content einfach unsere DetailView hinzu.

<?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:views="clr-namespace:ListDetailSample.Views"
             x:Class="ListDetailSample.Views.DetailPage"
             Title="Detail"
             Padding="16"
             Visual="Material">
    
    <ContentPage.Content>
        <views:DetailView />
    </ContentPage.Content>
    
</ContentPage>

Der Trick ist nun, dass wir überprüfen, ob wir auf einem einzelnen Screen laufen und dann entsprechend auf die DetailPage navigieren. Dazu öffnen wir wieder unsere Code-Behind Datei der MainPage und passen diese entsprechend an. Zunächst fügen wir zwei Variablen hinzu. Die eine Variable überprüft, ob unsere App aktuell über zwei Screens aufgespannt wurde und die andere beinhaltet eine Referenz auf unsere neue DetailPage.

private readonly DetailPage _detailPage;

private bool IsSpanned 
    => DualScreenInfo.Current.SpanMode != TwoPaneViewMode.SinglePane;

Im Konstruktor setzen wir nun die Variable _detailPage.

public MainPage()
{
    InitializeComponent();

    ListViewPage.SelectionChanged += OnSelectionChanged;
    _detailPage = new DetailPage();
}

Nun implementieren wir die beiden Methoden OnAppearing und OnDisappearing. Hier registrieren wir das PropertyChanged-Event, um auf Änderung am Screen zu reagieren.

protected override void OnAppearing()
{
    base.OnAppearing();

    if (!IsSpanned)
        ListViewPage.SelectedItem = null;

    DualScreenInfo.Current.PropertyChanged 
        += OnDualScreenInfoPropertyChanged;
}

protected override void OnDisappearing()
{
    base.OnDisappearing();

    DualScreenInfo.Current.PropertyChanged 
        -= OnDualScreenInfoPropertyChanged;
}

In dem EventHandler überprüfen wir nun, ob sich der SpanMode oder IsLandscape geändert hat. Wenn dies der Fall ist, so rufen wir eine Methode SetupViews auf.

private void OnDualScreenInfoPropertyChanged(object sender, 
    PropertyChangedEventArgs e)
{
    if (e.PropertyName == nameof(DualScreenInfo.Current.SpanMode)
        || e.PropertyName == nameof(DualScreenInfo.Current.IsLandscape))
    {
        SetupViews();
    }
}

Die Methode SetupViews übernimmt nun die ganze Magie, denn wenn die App nicht über zwei Screens aufgespannt ist, dann navigieren wir einfach auf unsere neue DetailPage.

private async void SetupViews()
{
    if (IsSpanned && !DualScreenInfo.Current.IsLandscape)
        SetBindingContext();

    if (DetailViewPage.BindingContext == null)
        return;

    if (!IsSpanned)
    {
        if (!Navigation.NavigationStack.Contains(_detailPage))
            await Navigation.PushAsync(_detailPage);
    }
}

In der SetBindingContext-Methode ergänzen wir noch das Setzen des BindingContext für unsere DetailPage.

private void SetBindingContext()
{
    var selectedItem = ListViewPage.SelectedItem
        ?? (ListViewPage.ItemsSource as IEnumerable<MyListItem>).FirstOrDefault();

    DetailViewPage.BindingContext = selectedItem;
    _detailPage.BindingContext = selectedItem;
}

Und in der Methode OnSelectionChanged noch den Aufruf von SetupViews.

private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
    if (e.CurrentSelection == null || e.CurrentSelection.Count == 0)
        return;

    SetBindingContext();
    SetupViews();
}

Damit können wir die App nun ein weiteres Mal testen und wir sehen nun, dass die App sowohl mit einem Screen als auch mit zwei Screens nutzbar ist.

In diesem Beitrag habe ich euch nun gezeigt, wie wir eine erste App entwickelt haben, welche Gebrauch von den zwei Screens eines Surface Duos macht, aber auch weiterhin auf Geräten mit nur einem Screen lauffähig ist. Ich habe die gleiche App auch in einem meiner letzten Videos programmiert.

Den gesamten Code findet ihr hier auf GitHub. Aber natürlich ist dies nicht die einzige Option, um eine App für das Surface Duo zu entwickeln. Daher seid doch auch beim nächsten Beitrag zu diesem Thema wieder mit dabei.

Pokédex – Kleines Xamarin.Forms Projekt – Teil 7 XF: Custom Control – LabeledSwitch XFWeather – Kleine Xamarin.Forms-App