WordPress-Seite als Xamarin.Forms App – Teil 2

Vor mehr als zwei Jahren habe ich euch bereits WordPressXF vorgestellt. Mein Kollege Thomas Pentenrieder hat eine .NET Library geschrieben, um auf einen WordPress-Blog von einer .NET App zugreifen zu können. Er hat sich dann dann primär um eine  UWP-Version gekümmert, welche die WordPressPCL-Library in Action zeigt. Ich habe mich selbst um eine Xamarin.Forms Version gekümmert. Nun dachte ich mir, dass man doch einmal die Solution updaten könnte, um so die neusten Xamarin.Forms Features verwenden zu können. Da ich jetzt nicht einfacher nur das Projekt updaten wollte, habe ich mir gedacht, dass die Umsetzung in Form von mehreren Blog-Beiträgen passiert und ich euch so zeige, wie eine kleine App entsteht, welche in der Lage ist die Beiträge eines Blogs unter Android und iOS an zu zeigen. Im zweiten Teil wollen wir nun einen SplashScreen hinzufügen und einen Ladebildschirm integrieren, so dass die ersten 10 Blogbeiträge automatisch beim Starten der App geladen werden.

Ausgangslage bildet die Solution, welche im ersten Teil entwickelt worden ist. Ihr bekommt die Solution entweder im alten Beitrag oder ansonsten über den folgenden Download-Button.

Da wir jetzt eine  zweite Page hinzufügen wollen, sollten wir uns zunächst um den NavigationService kümmern. Wie der Name schon vermuten lässt, soll sich dieser um die Navigation kümmern. Dieser Service ist notwendig, da wir auch von den ViewModels aus gegebenenfalls navigieren wollen. Wie schon beim WordPressService beginnen wir mit der Interface-Definition.

public interface INavigationService
{
    void Initialize(object navigationPageInstance);

    void Configure(NavigationTarget key, Type type);

    Task GoBackAsync();

    Task NavigateToAsync(NavigationTarget pageKey);

    void ClearBackstack();        
}

Die Navigation erfolgt hier mit der Hilfe eines Enums, welches den Namen NavigationTarget trägt. Damit umgehen wir magische Strings und stellen sicher, dass spätere Anpassungen am Code auch sauber an allen Stellen übernommen werden. Ansonsten stehen die klassischen Methoden, wie das Navigieren, das Zurück-Navigieren und das Löschen des NavigationStacks zur Verfügung. Daher können wir uns gleich an die Implementierung des Services machen.

public class NavigationService : INavigationService
{
    private INavigation _navigation;

    private Dictionary<NavigationTarget, Type> _pages = 
            new Dictionary<NavigationTarget, Type>();

    private bool _alreadyNavigating;

    public void Initialize(object navigationPageInstance)
    {
        var navigationPage = navigationPageInstance as NavigationPage;
        _navigation = navigationPage?.Navigation;
    }

    public void Configure(NavigationTarget key, Type type)
    {
        lock (_pages)
        {
            if (_pages.ContainsKey(key))
                _pages[key] = type;
            else
                _pages.Add(key, type);
        }
    }

    public async Task NavigateToAsync(NavigationTarget pageKey)
    {
        try
        {
            if (!_alreadyNavigating)
            {
                _alreadyNavigating = true;

                var type = _pages[pageKey];
                var page = Activator.CreateInstance(type) as Page;

                if (_navigation == null)
                    throw new NullReferenceException("Please initialize " +
                        "the navigation service on the NavigationPage.");

                _alreadyNavigating = false;
                await _navigation.PushAsync(page, true);
            }
        }
        catch (NullReferenceException ex)
        {
            _alreadyNavigating = false;
            throw ex;
        }
        catch (Exception ex)
        {
            _alreadyNavigating = false;
            Debug.WriteLine($"{nameof(NavigationService)} | " +
                $"{nameof(NavigateToAsync)} | {ex}");
        }
    }

    public void ClearBackstack()
    {
        if (_navigation == null)
            throw new NullReferenceException("Please initialize " +
                "the navigation service on the NavigationPage.");

        if (_navigation.NavigationStack.Count > 1)
        {
            var existingPages = _navigation.NavigationStack.ToList();
            existingPages.Reverse();

            foreach (var page in existingPages.Skip(1))
                _navigation.RemovePage(page);                
        }
    }

    public async Task GoBackAsync()
    {
        if (_navigation == null)
            throw new NullReferenceException("Please initialize " +
                "the navigation service on the NavigationPage.");

        await _navigation.PopAsync(true);
    }        
}

Wie man der Implementation entnehmen kann, verwenden wir das Navigations-Konzept von Xamarin.Forms und wrappen dieses nur geschickt, so dass wir auch innerhalb der ViewModels navigieren können. Ansonsten findet ein bisschen Error-Handling statt, so dass geprüft wird, ob der NavigationService sauber initialisiert wurde, bevor ein Navigationsversuch unternommen wird.

Nun können wir das BaseViewModel erweitern, so dass uns dieses den NavigationService zur Verfügung stellt und wir diesen in unseren anderen ViewModels verwenden können.

protected INavigationService NavigationService { get; } = 
    ServiceLocator.Current.GetInstance<INavigationService>();

Als nächstes wollen wir das LoadingViewModel schreiben, welches sich um das initiale Laden der ersten Blog-Beiträge kümmert. Zu einem späteren Zeitpunkt können hier dann auch weitere Einstellungen geladen werden. Dazu gibt es einen InitCommand, welcher die Blog-Beiträge lädt und anschließend auf die Übersichtsseite navigiert.

public class LoadingViewModel : BaseViewModel
{
    private readonly PostsViewModel _postsViewModel;


    private AsyncRelayCommand _initCommand;
    public AsyncRelayCommand InitCommand => 
        _initCommand ?? (_initCommand = new AsyncRelayCommand(InitAsync));


    public LoadingViewModel(PostsViewModel postsViewModel)
    {
        _postsViewModel = postsViewModel;
    }


    private async Task InitAsync()
    {
        IsLoading = true;

        try
        {
            await _postsViewModel.LoadPostsAsyncCommand.ExecuteAsync();

            await NavigationService.NavigateToAsync(NavigationTarget.PostsOverviewPage);
            NavigationService.ClearBackstack();
        }
        catch(Exception ex)
        {
            Debug.WriteLine($"{nameof(LoadingViewModel)} | {nameof(InitAsync)} | {ex}");
        }
        finally
        {
            IsLoading = false;
        }
    }
}

Während wir nun die Blog-Beiträge wollen wir auf der LoadingPage einfach einen ActivityIndicator anzeigen. Dazu erstellen wir die LoadingPage mit folgendem Content

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XFWordPress.Views.LoadingPage"
             NavigationPage.HasNavigationBar="False"
             NavigationPage.HasBackButton="False">
    
    <ContentPage.Content>
        <ActivityIndicator IsRunning="True"/>
    </ContentPage.Content>
    
</ContentPage>

In der OnAppearing-Methode der LoadingPage rufen wir nun den InitCommand vom LoadingViewModel auf.

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

    var loadingViewModel = ServiceLocator.Current.GetInstance<LoadingViewModel>();
    await loadingViewModel.InitCommand.ExecuteAsync();
}

Somit haben wir die wichtigsten Änderungen, welche ich euch in diesem Beitrag vorstellen wollte, bereits erledigt. Wir sind nun in der Lage innerhalb unserer App zu navigieren und haben einen einfachen Lade-Screen integriert und dafür gesorgt, dass initial die ersten Blog-Beiträge gleich geladen werden.

Im nächsten Schritt wollen wir uns um einen SplashScreen kümmern. Wenn wir nämlich aktuell die App starten, werden wir einfach nur von einem weißen Screen unter Android bzw. einem blauen Screen unter iOS begrüßt, bevor die Navigation auf unsere neue LoadingPage durchgeführt wird.

Beginnen wir zunächst mit Android. Hier fügen wir einen neue Activity mit dem Namen SpashActivity hinzu und setzen ein paar Attribute, wie der folgende Code-Ausschnitt zeigt.

[Activity(Label = "XFWordPress", Icon = "@mipmap/appicon", 
    Theme = "@style/Theme.Splash", MainLauncher = true, NoHistory = true)]
public class SplashActivity : Activity
{
    protected override void OnCreate(Bundle savedInstanceState)
    {
        base.OnCreate(savedInstanceState);

        Window.SetStatusBarColor(Android.Graphics.Color.ParseColor("#004d40"));

        StartActivity(typeof(MainActivity));
    }
}

Wir setzen zunächst die Farbe der Statusbar und starten im Anschluss die eigentliche MainActivity. Da wir nun die SplashActivity als MainLauncher definiert haben, passen wir die Attribute in der MainActivity entsprechend an.

[Activity(Theme = "@style/MainTheme", NoHistory = true, 
    ConfigurationChanges = ConfigChanges.ScreenSize | 
    ConfigChanges.Orientation | ConfigChanges.UiMode | 
    ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize)]
public class MainActivity : FormsAppCompatActivity
{
    // Code
}

Nun müssen wir noch den Style für die SplashActivity definieren. Dazu öffnen wir die Datei styles.xml, welche sich im Ordner values im Ordner Resources des Android Projekts befindet. Hier fügen wir nun folgende Definition hinzu.

<style name="Theme.Splash" parent="android:Theme">
	<item name="android:windowBackground">@drawable/splashscreen</item>
	<item name="android:windowNoTitle">true</item>
	<item name="android:windowFullscreen">true</item>
</style>

Nun fehlt nur noch eine weitere XML-Datei, welche wir im Ordner drawable mit den Namen splashscreen.xml anlegen. Denn diese beinhaltet nun das eigentliche Layout des SplashScreens. Es ist sehr simpel, da nur eine Hintergrundfarbe gesetzt wird und in der Mitte ein Logo angezeigt wird.

<?xml version="1.0" encoding="utf-8" ?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
    <item>
        <color android:color="#004d40"/>
    </item>
    <item android:width="150dp"
          android:height="150dp"
          android:gravity="center">
        <bitmap android:src="@drawable/splashscreenlogo"
                android:tileMode="disabled"/>
    </item>
</layer-list>

Damit haben wir den SplashScreen unter Android bereits implementiert und können uns jetzt um iOS kümmern. Hierfür gibt es bereits eine Storyboard-Datei im iOS-Projekt. Wir öffnen die Datei LaunchScreen.storyboard im Ordner Resources, indem wir mit einem Rechts-Klick Open With und dann XML Editor auswählen. Anschließend können wir die Hintergrundfarbe durch Ändern der Werte in Farbe backgroundColor anpassen. Ebenso können wir noch ein Icon angeben, welches ebenfalls in der Mitte des Screens angezeigt wird. Die gesamte XML-Struktur könnt ihr dem folgenden Code entnehmen.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document
    type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB"
    version="3.0"
    toolsVersion="6245"
    systemVersion="13F34"
    targetRuntime="iOS.CocoaTouch"
    propertyAccessControl="none"
    useAutolayout="YES"
    useTraitCollections="YES"
    initialViewController="X5k-f2-b5h">
    <dependencies>
        <plugIn
            identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin"
            version="6238" />
    </dependencies>
    <scenes>
        <!--View Controller-->
        <scene
            sceneID="gAE-YM-kbH">
            <objects>
                <viewController
                    id="X5k-f2-b5h"
                    sceneMemberID="viewController">
                    <layoutGuides>
                        <viewControllerLayoutGuide
                            type="top"
                            id="Y8P-hJ-Z43" />
                        <viewControllerLayoutGuide
                            type="bottom"
                            id="9ZL-r4-8FZ" />
                    </layoutGuides>
                    <view
                        key="view"
                        contentMode="scaleToFill"
                        id="yd7-JS-zBw">
                        <rect
                            key="frame"
                            x="0.0"
                            y="0.0"
                            width="414"
                            height="736" />
                        <autoresizingMask
                            key="autoresizingMask"
                            widthSizable="YES"
                            heightSizable="YES" />
                        <subviews>
                            <imageView
                                userInteractionEnabled="NO"
                                contentMode="scaleToFill"
                                misplaced="YES"
                                image="splash.png"
                                translatesAutoresizingMaskIntoConstraints="NO"
                                id="23">
                                <rect
                                    key="frame"
                                    x="270"
                                    y="270"
                                    width="100"
                                    height="100" />
                                <rect
                                    key="contentStretch"
                                    x="0.0"
                                    y="0.0"
                                    width="0.0"
                                    height="0.0" />
                            </imageView>
                        </subviews>
                        <color
                            key="backgroundColor"
                            red="0"
                            green="0.27843137254901962"
                            blue="0.25098039215686274"
                            alpha="1"
                            colorSpace="calibratedRGB" />
                        <constraints>
                            <constraint
                                firstItem="23"
                                firstAttribute="centerY"
                                secondItem="yd7-JS-zBw"
                                secondAttribute="centerY"
                                priority="1"
                                id="39" />
                            <constraint
                                firstItem="23"
                                firstAttribute="centerX"
                                secondItem="yd7-JS-zBw"
                                secondAttribute="centerX"
                                priority="1"
                                id="41" />
                        </constraints>
                    </view>
                </viewController>
                <placeholder
                    placeholderIdentifier="IBFirstResponder"
                    id="XAI-xm-WK6"
                    userLabel="First Responder"
                    sceneMemberID="firstResponder" />
            </objects>
            <point
                key="canvasLocation"
                x="349"
                y="339" />
        </scene>
    </scenes>
    <resources>
        <image
            name="splash.png"
            width="100"
            height="100" />
    </resources>
</document>

Damit sind wir unter iOS bereits fertig und haben auch hier den SplashScreen entsprechend angepasst.

An dieser Stelle würde ich dann auch diesen Blog-Beitrag beenden, da wir nun die Beiträge direkt beim Starten der App abrufen und auch einen entsprechenden SplashScreen angelegt haben. Das aktuelle Projekt lässt sich über den folgenden Link herunterladen:

Freut euch auf den nächsten Beitrag, wo wir uns die Navigation zur Detail-Seite anschauen werden, so dass wir den gesamten Blog-Beitrag in unserer App lesen können.

Rock, Paper, Scissors in Python Xamarin Dokumenation unter neuer Location Xamarin.Forms Controls: RepeaterView mit alternativen Zeilenfarbe