Pokédex – Kleines Xamarin.Forms Projekt – Teil 5

Ich habe bereits vier Teile dieser kleinen Xamarin.Forms Serie in meinem Blog veröffentlicht. Im ersten Beitrag haben wir begonnen unsere Pokédex-App zu erstellen und ein generelles Grundsetup durchgeführt. Im zweiten Beitrag ging es mit der Entwicklung weiter. So haben wir die ersten Services und auch die ersten Calls in Richtung der öffentliche API gemacht und diese Informationen in der App anzeigen. Im dritten Beitrag haben wir begonnen die Pokémon-Details abzurufen und per inkrementellem Laden auf der Übersichtsseite anzuzeigen. Im vierten Beitrag haben wir ein kleines Popup entwickelt, welches uns weitere Details anzeigt, sofern der Nutzer einen Eintrag in der Übersicht angeklickt hat. Im nun vorliegenden fünften Teil dieser Serie wollen wir noch ein paar Anpassungen vornehmen und unseren Code ein wenig aufräumen bzw. optimieren.

Den bisherigen Stand könnt ihr über folgenden Download-Button herunterladen.

Mir ist leider noch ein kleiner Fehler aufgefallen, was dazu führen kann, dass Pokémons doppelt angezeigt werden. Wir öffnen daher unsere PokemonDetail-Klasse und überschreiben die beiden Methoden Equals und GetHashCode.

public class PokemonDetail
{
    [JsonProperty("id")]
    public int Id { get; set; }

    [JsonProperty("name")]
    public string Name { get; set; }

    [JsonProperty("height")]
    public int Height { get; set; }

    [JsonProperty("weight")]
    public int Weight { get; set; }

    [JsonProperty("sprites")]
    public OtherSprite Sprite { get; set; }

    [JsonProperty("types")]
    public IList<TypeDetails> Types { get; set; }


    public override bool Equals(object obj)
    {
        if (obj == null || !(obj is PokemonDetail pokemonDetail))
            return false;

        return Id == pokemonDetail.Id;
    }

    public override int GetHashCode()
    {
        return Id.GetHashCode();
    }
}

Nun öffnen wir unseren PokemonService und in der Methode GetPokemonDetailsAsync nutzen wir im Upsert-Aufruf die Distinct-Methode, so dass jedes Pokémon nur einmal in der Liste auftaucht.

_databaseService.Upsert(new PokemonDetailDatabaseObject 
{ 
    Pokemons = pokemons.Distinct().OrderBy(p => p.Id).ToList()
}, Statics.PokemonCollectionName);

Damit haben wir diesen kleinen Fehler behoben und können mit dem Hinzufügen des NuGet-Packages Xamarin.FFImageLoading.Forms zu all unseren Projekten fortfahren. Hiermit erhalten wir nämlich die Möglichkeit Bilder zu cachen. Dadurch verringern wir die Anzahl an Requests und unsere App wird dadurch auch schneller. Wir klicken also mit der rechten Maustaste auf unsere Solution und wählen den Eintrag Manage NuGet Packages for Solution aus. Nun klicken wir auf den Reiter Browse und suchen nach Xamarin.FFImageLoading.Forms, welches wir allen Projekten hinzufügen.

Nun öffnen wir die Datei MainActivity.cs in unserem Android-Projekt und rufen die Init-Methode auf dem CachedImageRenderer auf, um das Package zu initialisieren.

FFImageLoading.Forms.Platform.CachedImageRenderer.Init(true);

Als nächstes öffnen wir die Datei AppDelegate.cs im iOS-Projekt und rufen auch hier die Init-Methode entsprechend auf.

FFImageLoading.Forms.Platform.CachedImageRenderer.Init();

Die gleiche Code-Zeile fügen wir nun auch noch in der App.xaml.cs-Datei des UWP-Projekts hinzu.

Da sich das Caching von Bildern sowohl in der Übersicht als auch im Detail Sinn ergibt, öffnen wir zunächst unser PokemonOverviewControl und ersetzen das Image-Control durch ein CachedImage-Control. Außerdem setzen wir noch die beiden Properties CacheDuration auf 14, was 14 Tagen entspricht und CacheType auf Disk, so dass die Bilder tatsächlich im File-System abgelegt werden.

<ffImage:CachedImage Source="{Binding PokemonDetail.Sprite.Image.Artwork.ImagePath
    , Source={x:Reference root}}"
                     CacheDuration="14"
                     CacheType="Disk"
                     WidthRequest="75"
                     HeightRequest="75"
                     Aspect="AspectFit"
                     Grid.Row="1" />

Anschließend öffnen wir die PokemonDetailDialogPage und ersetzen auch hier das PokemonDetailSprite-Image durch ein CachedImage.

<ffImage:CachedImage x:Name="PokemonDetailSprite"
                     Style="{StaticResource PokemonDetailImageStyle}"
                     Grid.Row="2"
                     Grid.Column="0"
                     Grid.ColumnSpan="2" />

Da das Image hier über einen Style verfügt, müssen wir unsere Styles.xaml-Datei öffnen und den Style entsprechend anpassen. Den TargetType wechseln wir von Image auch CachedImage und wir ergänzen die beiden Properties, wie oben bereits beschrieben.

<Style x:Key="PokemonDetailImageStyle"
       TargetType="ffImage:CachedImage">
    <Setter Property="WidthRequest"
            Value="150" />
    <Setter Property="HeightRequest"
            Value="150" />
    <Setter Property="Aspect"
            Value="AspectFit" />
    <Setter Property="VerticalOptions"
            Value="Center" />
    <Setter Property="HorizontalOptions"
            Value="Center" />
    <Setter Property="CacheDuration"
            Value="14" />
    <Setter Property="CacheType"
            Value="Disk" />
</Style>

Damit werden die Bildern nun lokal vorgehalten und werden damit bei jedem Starten der App deutlich schneller geladen, da diese nicht mehr vom Webserver heruntergeladen werden müssen.

Derzeit haben wir noch keine Limitierung auf die maximale Anzahl von Pokémons. Es gibt in der API zum jetzigen Zeitpunkt jedoch nur 898 verschiedene Pokémons. Daher öffnen wir die Klasse Statics im Ordner Common und fügen die statische Variable MaxPokemonCount mit entsprechend dieser Zahl ein.

public static int MaxPokemonCount = 898;

Nun öffnen wir das OverviewPageViewModel und ergänzen die LoadMorePokemonsAsync-Methode, um die Überprüfung auf die neue Variable durchzuführen. Sollten wir nämlich das Limit erreicht haben, können wir einfach per return die Methode beenden.

private async Task LoadMorePokemonsAsync()
{
    if (Pokemons == null)
        return;

    if (Pokemons.Count >= Statics.MaxPokemonCount)
        return;

    IsLoadingData = true;

    var pokemons = await _pokemonService
                .GetPokemonDetailsAsync(Pokemons.Count, Statics.DefaultLimit);
    if (pokemons != null)
        Pokemons.AddRange(pokemons);

    IsLoadingData = false;
}

Wir öffnen nun noch einmal die Statics-Klasse, denn wir wollen noch eine kleine Anpassung vornehmen. Sofern wir die UWP-Version unserer App starten, haben wir natürlich einen größeren Monitor zur Verfügung. Daher macht es Sinn, dass das DefaultLimit unter UWP auf 25 erhöht wird. Dafür überprüfen wir per RuntimePlatform, ob wir uns auf UWP befinden und geben dann entsprechend 25 zurück und ansonsten 10.

public static int DefaultLimit = Device.RuntimePlatform == Device.UWP ? 25 : 10;

Bisher haben wir auf der OverviewPage ja noch einen Button, welchen wir drücken müssen, damit die Pokémons abgerufen werden. Dies wollen wir jetzt noch ändern, so dass direkt beim Navigieren auf die Seite die ersten Pokémons abgerufen werden. Dafür öffnen wir die OverviewPage und entfernen das Grid, welches den Button und die CollectionView beinhaltet. Außerdem entfernen wir auch noch den Button. Damit erhalten wir jetzt den folgenden Code für die OverviewPage.

<?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:xct="http://xamarin.com/schemas/2020/toolkit"
             xmlns:controls="clr-namespace:Pokedex.Controls"
             x:Class="Pokedex.Views.OverviewPage"
             Title="{xct:Translate OverviewPageTitle}"
             BindingContext="{Binding OverviewPageViewModel, 
                Source={StaticResource ViewModelLocator}}">

    <ContentPage.Content>
        <Grid>
            <CollectionView ItemsSource="{Binding Pokemons}"
                            RemainingItemsThreshold="1"
                            RemainingItemsThresholdReachedCommand="{Binding LoadMorePokemonsCommand}">
                <CollectionView.ItemsLayout>
                    <GridItemsLayout HorizontalItemSpacing="12"
                                     VerticalItemSpacing="12"
                                     Orientation="Vertical"
                                     Span="{OnPlatform Default=2, UWP=3}" />
                </CollectionView.ItemsLayout>

                <CollectionView.ItemTemplate>
                    <DataTemplate>
                        <controls:PokemonOverviewControl PokemonDetail="{Binding .}" />
                    </DataTemplate>
                </CollectionView.ItemTemplate>
            </CollectionView>

            <controls:LoadingControl IsVisible="{Binding IsLoading}" />
        </Grid>
    </ContentPage.Content>

</ContentPage>

Nun wechseln wir in die Code-Behind-Datei und überschreiben die OnAppearing-Methode. Hier rufen wir nun den LoadPokemonsCommand auf.

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

    await ((OverviewPageViewModel)BindingContext)
        .LoadPokemonsCommand.ExecuteAsync();
}

Schauen wir uns das Ergebnis einmal an. Wir stellen zwar äußerlich keine großen Änderungen fest, aber trotzdem haben wir den Code weiter optimiert. Hier ein aktueller Screenshot der UWP-Version.

Uns fehlen jetzt noch ein App-Icon, sowie ein kleiner SplashScreen. Diese Dinge würde ich aber nächste Woche in einem sechsten Beitrag bearbeiten. Daher bleibt mir an dieser Stelle nur noch den Download-Link für die aktuelle Version anzugeben.

Fluent Terminal: UWP-Terminal für Windows Gruppierte Liste in einer Xamarin.Forms App AppCenter.Analytics: Eigene Events tracken