Diese Website verwendet Cookies, um Ihnen ein besseres Nutzungserlebnis bieten zu können. OK
7 Min Lesezeit 13 mal geteilt 19 Kommentare

Eure erste Android-App, Teil 3: News-Einträge anzeigen

Bisher haben wir in unserer Serie gelernt, wie wir Berechtigungen in Android vergeben sowie einen RSS-Feed nativ oder mithilfe einer externen Bibliothek abgreifen. In Teil 3 werden wir Informationen aus dem Feed herauslesen und in einer Listenansicht anzeigen.

Welches Foto könnte von Dir kommen?

Wähle Variante 'geht so' oder Variante 'o la la'.

close
Du hast undefined gewählt!
Was würden deine Freunde wählen?
Teilen
VS
  • 524
    Stimmen
    Ooops! Etwas ist schiefgelaufen. Aktualisieren sollte helfen.
    Variante 'geht so'
  • 998
    Stimmen
    Ooops! Etwas ist schiefgelaufen. Aktualisieren sollte helfen.
    Variante 'o la la'
20150119 IMGL7042
Kein Entwickler ist je vom Himmel gefallen. Wir helfen nach. / © ANDROIDPIT

Wer Teil 1 und 2 verpasst hat, findet diese hier:

Wie benutze ich die Listen-Ansicht, ListView

Android bringt von Haus aus die Ansicht ListView mit sich. Wie der Name verrät, können hier Objekte auf dem Bildschim in einer scrollbaren Listenansicht wiedergegeben werden.

Um diese Funktion zu nutzen, ändern wir unsere Layoutdatei: activity_main.xml

<ListView

   android:id="@+id/feed_listview"

   android:scrollbars="vertical"

   android:layout_width="match_parent"

   android:layout_height="match_parent"/>

Danach ändern wir in unserer MainActivity die onCreate Methode zu:

   @Override

   protected void onCreate(Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);

 

       setContentView(R.layout.activity_main);

 

       ListView listView = (ListView) findViewById(R.id.feed_listview);

 

       ArrayList<String> items = new ArrayList<>();

       for (int i = 0; i < 20; i++) {

           items.add("Article " + i);

       }

 

       listView.setAdapter(new ArrayAdapter<String>(this, R.layout.rss_feed_item, items));

   }

Unsere App sieht nun wie folgt aus:

code henrique
Noch sieht es nicht nach viel aus... / © ANDROIDPIT

Jetzt können wir die Platzhalter (Article 0 - 19) durch unsere Feed-Objekte austauschen.

Hierzu müssen wir den RSS-Feed parsen und verwenden den XMLPullParser aus dem Android Entwicklungsbausatz (SDK).

Folgende Attribute des Feeds werden benötigt:

item, title, link, description und pubDate.

Dies ist die Struktur unseres RSS-feeds:

<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
 <channel>
   <title>Android News + App Reviews + Hardware Reviews - AndroidPIT</title>
   <link>http://www.androidpit.com/</link>
   <description>This feed includes text previews of the latest news articles and app and hardware reviews on www.androidpit.com. Click on the article title to read the entire article.</description>
   <language>en</language>
   <pubDate>Wed, 14 Jan 2015 16:06:05 GMT</pubDate>
   <dc:creator>AndroidPIT</dc:creator>
   <dc:date>2015-01-14T16:06:05Z</dc:date>
   <dc:language>en</dc:language>
   <item>
     <title>How to transfer WhatsApp conversations to new devices: the easy way</title>
     <link>http://www.androidpit.com/how-to-transfer-whatsapp-conversations-to-new-devices</link>
     <description>&lt;p&gt;If you've got a new handset and want to pick up where you left off in WhatsApp, you can find out here how to transfer all of your WhatsApp conversations to a new device.&lt;/p&gt;&lt;p&gt;&lt;img src="fileserver:///blog/x00/229500.jpg"&gt;&lt;/p&gt;&lt;p&gt;&lt;i&gt;(This is a preview - click &lt;a href="http://www.androidpit.com/how-to-transfer-whatsapp-conversations-to-new-devices"&gt;here&lt;/a&gt; to read the entire entry.)&lt;/i&gt;&lt;/p&gt;</description>
     <pubDate>Wed, 14 Jan 2015 16:00:00 GMT</pubDate>
     <guid isPermaLink="false">urn:androidpit:news:article:5a14f05f6c6c5b482ddcc0eb21f0e20b</guid>
     <dc:creator>Scott Adam Gordon</dc:creator>
     <dc:date>2015-01-14T16:00:00Z</dc:date>
   </item>
   <item>
     <title>Google Translate brings instant sign translation to your Android</title>
     <link>http://www.androidpit.com/google-translate-instant-translation</link>
     <description>&lt;p&gt;Google Translate for Android has been updated to bring instant sign translation to Android handsets such as the Nexus 6 and Samsung Galaxy S5&lt;/p&gt;&lt;p&gt;&lt;img src="fileserver:///blog/x06/229506.jpg"&gt;&lt;/p&gt;&lt;p&gt;&lt;i&gt;(This is a preview - click &lt;a href="http://www.androidpit.com/google-translate-instant-translation"&gt;here&lt;/a&gt; to read the entire entry.)&lt;/i&gt;&lt;/p&gt;</description>
     <pubDate>Wed, 14 Jan 2015 14:24:53 GMT</pubDate>
     <guid isPermaLink="false">urn:androidpit:news:article:2138c5c46a286e520153d14893a4ff15</guid>
     <dc:creator>Mark Wilson</dc:creator>
     <dc:date>2015-01-14T14:24:53Z</dc:date>
   </item>



























Hierzu erstellen wir eine Klasse mit dem Namen AndroidPitRssParser und für jedes Attribut eine Lese-Methode, unrelevante Attribute ignorieren wir - mit der skip Methode.

So sieht die Klasse aus, ich denke dass sich der Code von selbst erklärt:

public class AndroidPitRssParser {

 

   // We don't use namespaces

   private static final String ns = null;

 

   public List<Item> parse(InputStream in) throws XmlPullParserException, IOException {

       try {

           XmlPullParser parser = Xml.newPullParser();

           parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);

           parser.setInput(in, null);

           parser.nextTag();

           return readRss(parser);

       } finally {

           in.close();

       }

   }

 

   private List<Item> readRss(XmlPullParser parser) throws IOException, XmlPullParserException {

       List<Item> entries = new ArrayList<>();

 

       parser.require(XmlPullParser.START_TAG, null, "rss");

       while (parser.next() != XmlPullParser.END_TAG) {

           if (parser.getEventType() != XmlPullParser.START_TAG) {

               continue;

           }

           String name = parser.getName();

           // Starts by looking for the channel tag

           if (name.equals("channel")) {

               return readChannel(parser);

           } else {

               skip(parser);

           }

       }

       return entries;

   }

 

   private void skip(XmlPullParser parser) throws XmlPullParserException, IOException {

       if (parser.getEventType() != XmlPullParser.START_TAG) {

           throw new IllegalStateException();

       }

       int depth = 1;

       while (depth != 0) {

           switch (parser.next()) {

               case XmlPullParser.END_TAG:

                   depth–;

                   break;

               case XmlPullParser.START_TAG:

                   depth++;

                   break;

           }

       }

   }

 

   private List<Item> readChannel(XmlPullParser parser)

           throws IOException, XmlPullParserException {

 

       List<Item> entries = new ArrayList<>();

 

       parser.require(XmlPullParser.START_TAG, null, "channel");

       while (parser.next() != XmlPullParser.END_TAG) {

           if (parser.getEventType() != XmlPullParser.START_TAG) {

               continue;

           }

           String name = parser.getName();

           // Starts by looking for the entry tag

           if (name.equals("item")) {

               entries.add(readItem(parser));

           } else {

               skip(parser);

           }

       }

       return entries;

   }

 

   private Item readItem(XmlPullParser parser) throws IOException, XmlPullParserException {

       parser.require(XmlPullParser.START_TAG, ns, "item");

       String title = null;

       String link = null;

       String description = null;

       String pubDate = null;

 

       while (parser.next() != XmlPullParser.END_TAG) {

           if (parser.getEventType() != XmlPullParser.START_TAG) {

               continue;

           }

           String name = parser.getName();

           switch (name) {

               case "title":

                   title = readTitle(parser);

                   break;

               case "link":

                   link = readLink(parser);

                   break;

               case "description":

                   description = readDescription(parser);

                   break;

               case "pubDate":

                   pubDate = readPubDate(parser);

                   break;

               default:

                   skip(parser);

                   break;

           }

       }

 

       return new Item(title, link, description, pubDate);

   }

 

   private String readPubDate(XmlPullParser parser) throws IOException, XmlPullParserException {

       return readRequiredTag(parser, "pubDate");

   }

 

   private String readDescription(XmlPullParser parser)

           throws IOException, XmlPullParserException {

       return readRequiredTag(parser, "description");

   }

 

   private String readLink(XmlPullParser parser) throws IOException, XmlPullParserException {

       return readRequiredTag(parser, "link");

   }

 

   private String readRequiredTag(XmlPullParser parser, String tag)

           throws IOException, XmlPullParserException {

       parser.require(XmlPullParser.START_TAG, ns, tag);

       String result = readText(parser);

       parser.require(XmlPullParser.END_TAG, ns, tag);

       return result;

   }

 

   private String readTitle(XmlPullParser parser) throws IOException, XmlPullParserException {

       return readRequiredTag(parser, "title");

   }

 

   private String readText(XmlPullParser parser) throws IOException, XmlPullParserException {

       String result = "";

       if (parser.next() == XmlPullParser.TEXT) {

           result = parser.getText();

           parser.nextTag();

       }

       return result;

   }

 

   public static class Item {

       public final String title;

       public final String link;

       public final String description;

       public final String pubDate;

 

       public Item(String title, String link, String description, String pubDate) {

           this.title = title;

           this.link = link;

           this.description = description;

           this.pubDate = pubDate;

       }

 

       @Override

       public String toString() {

           return "{" + title + "," + link + "," + description + "," + pubDate + "}";

       }

   }

}

Feed anzeigen

Nachdem wir mithilfe der Parse-Methode eine Liste von Newseinträgen haben, müssen wir diese dem ArrayAdapter zur Verfügung stellen.

Hierzu passen wir die onCreate - Methode wie folgt an:

   @Override

   protected void onCreate(Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);

 

       setContentView(R.layout.activity_main);

 

       ListView listView = (ListView) findViewById(R.id.feed_listview);

 

       adapter = new ArrayAdapter<>(this, R.layout.rss_feed_item, items);

       listView.setAdapter(new ArrayAdapter<>(this, R.layout.rss_feed_item, items));

 

       new GetFeedTask().execute();

   }

Und unsere GetFeedTask Klasse:

   private class GetFeedTask extends AsyncTask<Void, Void, List<AndroidPitRssParser.Item>> {

 

       @Override

       protected List<AndroidPitRssParser.Item> doInBackground(Void... params) {

           try {

               Request request = new Request.Builder()

                       .url("http://www.androidpit.com/feed/main.xml")

                       .build();

 

               Response response = new OkHttpClient().newCall(request).execute();

               List<AndroidPitRssParser.Item> items = null;

               try {

                   items = new AndroidPitRssParser().parse(response.body().byteStream());

               } catch (XmlPullParserException e) {

                   e.printStackTrace();

               }

               return items;

 

           } catch (IOException e) {

               e.printStackTrace();

               return null;

           }

       }

 

       @Override

       protected void onPostExecute(List<AndroidPitRssParser.Item> items) {

           if (items != null) {

               adapter.clear();

               adapter.addAll(items);

           }

       }

   }

Jetzt sollten alle Feed-Objekte korrekt angezeigt werden.

code henrique 2
So sollte es aussehen. / © ANDROIDPIT

News-Eintrag auf einem Neuen Screen anzeigen

Unsere App ändern wir nun wie folgt, beim Klick auf einen Eintrag soll sich ein neues Fenster öffnen und alle Informationen des jeweiligen News-Eintrags anzeigen.

Hierzu implementieren wir einen EventListener. Beim Klick darauf erstellen wir eine neue Activity. Die ItemDetailActivity und übergeben die jeweilige News-ID.

       listView.setOnItemClickListener(this);

 

   @Override

   public void onItemClick(AdapterView<?> parent, View view, int position, long id) {

 

   }

Intents

Zur Activity-übergreifenden Kommunikation stellt Android Intents zur Verfügung. Eine Art Absichtserklärung. In unserem Fall soll nach dem Clickereignis eine neue Activity erstellt werden.

Dies geht folgendermaßen:

   @Override

   public void onItemClick(AdapterView<?> parent, View view, int position, long id) {

       startActivity(ItemDetailActivity.getStartIntent(this, adapter.getItem(position)));

   }

Die getStartIntent Methode wurde von uns erstellt und sieht so aus::

   public static Intent getStartIntent(Context context, AndroidPitRssParser.Item item) {

       Intent intent = new Intent(context, ItemDetailActivity.class);

       intent.putExtra(ITEM_EXTRA, item);

       return intent;

   }

Um putExtra ausführen zu können, müssen wir die News-Einträge serialisierbar machen, für den sequentiellen Zugriff. Mehr Infos dazu auf Wikipedia

Hierzu bietet Android mehrere Möglichkeiten ein. Wir entscheiden uns der Einfachheit halber für die Serializable Methode gegenüber Parcelable.

Wir ändern unsere onCreate Methode wie folgt:

   private static final String ITEM_EXTRA = "item";

 

   @Override

   protected void onCreate(Bundle savedInstanceState) {

       super.onCreate(savedInstanceState);

       setContentView(R.layout.activity_item_detail);

 

       TextView itemDetail = (TextView) findViewById(R.id.item_detail);

 

       AndroidPitRssParser.Item item =

               (AndroidPitRssParser.Item) getIntent().getSerializableExtra(ITEM_EXTRA);

       if (item != null) {

           itemDetail.setText(item.toString());

       }

   }

Nun müssen wir nur noch die ItemDetailActivity Klasse im Manifest festhalten:

       <activity android:name=".ItemDetailActivity" />

Führt man die Anwendung nun aus, erhält man beim Klick auf den News-Eintrag ein neues Fenster mit allen relevanten News-Informationen.

Zum Abschluss kann man festhalten, dass die genannten Infos bisher relativ umfangreich sind - allerdings werden diese Bascis in so gut wie jeder App verwendet und sind damit wiederverwertbar.

Habt ihr die Konzepte einmal verstanden hilft dies ungemein bei künftigen Apps.

Der nächste Teil der Reihe wird sich um die GUI drehen und wir werden lernen wie wir unsere App mithilfe der Material Design Guidelines anpassen.  Freut euch drauf. 

 

13 mal geteilt

Top-Kommentare der Community

  • androidlover 17.01.2015

    Als nicht Programmierer hab ich den Eindruck, dass man hierbei nicht wirklich was ,,lernen,, kann, vielmehr kann man das Programm einfach nur abschreiben, ohne irgendwelchen lernzweck. Programmieren lernen kann man hier wirklich nicht. schadr

19 Kommentare

Neuen Kommentar schreiben:
  • Danke für den Artikel, ich bin noch nicht ganz durch, möchte aber schon auf einige Probleme und Fehler hinweisen. Ich habe selber erst einen anderen First-Android-App Artikel durchgearbeitet und bin daher noch blutiger Anfänger, also bitte Rücksicht nehmen! :)

    1) Alle Code-Elemente sin in zentrierter Schrift gesetzt, was das lesen und abtippeln nicht grade vereinfacht und auch ziemlich unüblich ist.

    2) ist das Github repository verschoben, es ist jetzt unter [github com]/[iambnz]/[AndroidPITNews] zu finden. Alle Repository-links im Artikel lassen sich durch ändern des Githubnutzernamens in "iambnz" fixen.

    3) Der Umbau auf OkHttp funktioniert bei mir nicht so leicht, da Android-Studio bei Alt+Enter (PS: das könnte im Artikel auch erwähnt werden.) automatisch die fehlenden Pakete einbindet, aber speziell beim Wort "Request" erst mal an seinen eigenen DownloadManager denkt und diesen einbindet. Hier müssen händisch die Pakete von OkHttp in der Importsektion eingetragen werden. Siehe dazu das Beispiel von OkHttp: HaTeTePeEs-Doppelpunkt//raw.githubusercontent.com/square/okhttp/master/samples/guide/src/main/java/com/squareup/okhttp/guide/GetExample.java

    4) In Teil 3 am Anfang st nicht klar ob das ListView den TextView ersetzen oder ergänzen soll

    5) Im zweiten Codeblock von Teil 3 soll das "rss_feed_item" verwendet werden, was aber noch unbekannt ist, erst eine Suche im GH resposity danach zeigt eine Datei "rss_feed_item.xml" welche unter res>layout erstellt werden und mit dem Inhalt von GH gefüllt werden muss, damit der Block überhaupt wie im darunter angezeigten screenshot laufen kann. Ausserdem müssen TextView und ScrollView aus der activity_main.xml entfernt werden (siehe auch Punkt 3). Dann rennt das Programm auch wieder.

    6) Im Teil 3 soll die Klasse AndroidPitRssParser {} erzeugt werden. Worin und ob dafür eine neue Datei erzeugt werden soll oder so steht nicht da. Nach ein bisschen rumspielen und entsprechenden Kompiler-Fehlermeldungen kann die Funktion new->"Java Class" enteckt werden.

    7) In der Klasse wird mehrmals "List<Item>" verwendet, aber das Symbol Item ist unbekannt und Android-Studio fragt immer ob "Android.content.ClipData" importiert oder davor geschrieben werden soll. Es wäre daher für Leute wie mich, die lieber alles abtippen statt zu kopieren, um was zu lernen, erst die Item-Klasse zu erzeugen.


    8) Wird die Klasse AndroidPitRssParser { } einfach nur aus der webseite kopiert fehlen die packages und "depth–;" muss durch "depth--;" ersetzt werden.


    9) Das source-file für die Klasse "GetFeedTask{}" fehlt im GH repository. Was daran liegt, wie sich wieder nur durch Github herausfinden lies, dass die Klasse komischer Weise in die MainActivity.java geschrieben und nicht extra dafür eine neue Klassendatei erzeugt wurde, wie es eben noch AndroidPitRssParser {} nötig war.

    10) Durch reines Abtippen des Codes ist ab der Hälfte des drittenTeils Endstation weil nichts funktioniert. Beim verzweifelten Versuch durch copy-pasten aus dem Repository die Klasse GetFeedTask {} in die MainActivity zu bekommen es auch zu Fehlern. Aus dem Repo müssen mindestens noch die zwei wichtigen Zeilen gezogen und der Main Activity hinzugefügt werden:

    private List<AndroidPitRssParser.Item> items = new ArrayList<>();
    private ArrayAdapter<AndroidPitRssParser.Item> adapter;

    dennoch sagt der Kompiler:
    "Error:(42, 13) error: MainActivity.GetFeedTask is not abstract and does not override abstract method doInBackground(Void...) in AsyncTask"

    Und 4 weitere Fehler. Die durch mich als Anfänger nicht zu bewältigen sind.

    Selbst ein vollständiges copy-past der MainActivity compiliert nicht. Hier endet dann auch die Hilfe durch das Repository denn da sind noch "OnItemClickListener" und "onItemClick" enthalten welche aber im Tutorial noch nicht eingeführt wurden. Auch ein entfernen der Teile ändert nichts an den Fehlern. Schade, das ich hier den Kurs abbrechen muss. Auch wenn ich bis dahin Spass am Fehler finden und beheben hatte.



    Autocomplete Hinweis: Ab dem Zweiten Teil erscheinen in Android-Studio keine Autovervollständigungen mehr. Dafür könnt ihr wahrscheinlich nichts. Aber als Tipp für andere die das Problem haben. Einfach mal das Projekt schließen und neu öffnen.

  • Folgt man der Schritt für Schritt Anleitung ist es ziemlich frustrierend. Es wird nirgends darauf hingewiesen, dass man eine neue layout Datei mit dem rss_feed_item anlegen soll. Im ersten Teil sind auch schon solche Fehler. Den RSS Feed konnte ich erst laden als ich Teil 2 komplett aus git geladen habe.

  • So schnell wird es hier wohl nicht weitergehen: "[...] Die angesprochenen Magazin-Reihe ""Eure erste Android App" hat einer unser Android-Entwickler in seiner Freizeit geschrieben und da er aktuell eingespannt ist, wird das nichts mehr. Für so eine App-Strecke kann man allerdings auch keinen Redakteur hinsetzen, da diese keine Apps programmieren. Natürlich haben wir auch Entwickler angeschrieben, doch Rückmeldungen diesbezüglich haben wir selten erhalten oder es bestand kein Interesse." - Zitat von Sophia Brennecke aus dem "Masse statt Klasse in den Artikeln?"-Thread (Url: http://www.androidpit.de/forum/615534/masse-statt-klasse-in-den-artikeln ), Seite 16, erster Beitrag.

  • Wo bleibt der nächste Teil?
    Die Reihe hat so gut angefangen, es wäre schade wenn das jetzt einfach so aufhört. Aber anscheinend sind "Die besten abc von xyz" Artikel wichtiger oder lukrativer als ausnahmsweise mal fundierte Artikel.

  • warte auf das nächste Tutorial (GUI) oder war das wie so bei vielen Blogs - eine gut angefangene Tutorial Reihe, aber nach kurzer Zeit schon wieder ENDE ???

  • Wann kommt das Tutorial zur GUI? :D
    Ich hab absolut keinen Plan von Appprogrammierung und co. Hab früher spiele in Javascript programmiert... Aber das hilft mir hier nicht besonders weiter...

  • Also ich weiß ja nicht obs an der späten Uhrzeit liegt aber: im Tutorial wird nicht gezeigt wie das rss_feed_item in der activity_main.xml angelegt werden soll. Es wird aber im listView Adapter verwendet. Oder sehe ich das falsch?

    • die rss_feed_items werden durch den ArrayAdapter hinzugefügt. Der ArrayAdapter kümmert sich darum, die Items in das feed_view zu packen.

      Im Tutorial gibt es allerdings einen Fehler: der Adapter wird in der onCreate 2x angelegt. Der erste wird in die Member-Variable "adapter" erzeugt und der zweite als anonyme Instanz direkt an den ListView übergeben. Falls man später mit dem Adapter arbeiten möchte, wird die Member-Variable nicht funktionieren. So zum Beispiel wenn man neue Feeds zum Adapter hinzufügen möchte. Die Zeile muss also listView.setAdapter(adapter) heißen.

      Btw.: ListViews sind zwar weiterhin erlaubt, sollten aber in Zukunft durch die wesentlich flexibleren RecyclerViews ersetzt werden.

  • Als nicht Programmierer hab ich den Eindruck, dass man hierbei nicht wirklich was ,,lernen,, kann, vielmehr kann man das Programm einfach nur abschreiben, ohne irgendwelchen lernzweck. Programmieren lernen kann man hier wirklich nicht. schadr

    • Wie im ersten Teil beschrieben ist, wird Programmiererfahrung vorausgesetzt. Demnach soll man hier auch nicht das Programmieren an sich lernen sondern eher wie man, wenn man Programmieren kann, seine erste App schreibt. Um Programmieren zu lernen ist etwas mehr nötig als diese Serie und vorallem sollte man da mit etwas viel simpleren anfangen.

    • Soll man auch nicht, wie Max richtig erklärt. Das Programmieren zu erklären wäre ein großes, langfristiges Projekt, über das wir auch nachgedacht haben. Aber ich glaube nicht, dass das hier richtig aufgehoben wäre.

    • Hi androidlover,

      Danke für dein offenes Feedback.

      Ein wenig Erfahrung bezüglich Objektorientierung, Java / Android, Git ist von Vorteil.

      Ich vergleiche derartige Tutorials manchmal mit dem Backen eines Kuchens. Du hast hier das Rezept (Quellcode), die Zutaten (SDKs, Bibliotheken) und Android Studio als Backofen. Damit kannst du unseren Kuchen erstmal backen, strikt nach Rezept.

      Willst du das Rezept anpassen, erfordert dies Erfahrung.

      ---

      Ziel der Serie ist, dass die Leser zum Programmieren animiert werden.

      Für jeden der einen Blog und damit meistens auch einen RSS-Feed besitzt, kann diese Serie relevant sein. Letztlich kann jeder nur durch striktes Copy & Paste seine erste eigenen App erstellen.

      ---

      VG + schönen Sonntag,
      Erik

  • coole Sache

  • Eine hervorragende Reihe. Gratulation und weiter so!!

  • weiter so

Diese Website verwendet Cookies, um Ihnen ein besseres Nutzungserlebnis bieten zu können. Mehr dazu

Alles klar!