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

Eure erste Android-App, Teil 2: Jetzt geht's richtig los!

Nachdem wir in Teil 1 dieser Reihe zunächst ein Projekt erstellt haben, geht es uns heute darum, die News für unseren Newsreader einzubinden. 

20150119 IMGL7042
Kein Entwickler ist je vom Himmel gefallen. Wir helfen nach. / © ANDROIDPIT

Hierzu müssen wir einige neue Konzepte verstehen, weshalb Teil 2 weitaus komplexer sein wird als Teil 1. Wenn Ihr die hier erklärten Schritte jedoch erst mal verstanden und verinnerlicht habt, dürft Ihr stolz auf Euch sein!

Wenn Ihr das letzte Tutorial noch nicht durch habt und es überspringen wollt, könnt ihr den Code hier direkt von meinem Github Repository als ZIP herunterladen - oder wie auf Github üblich - das Projekt forken - und von dort weitermachen.

Den XML-Feed herunterladen

Die meisten Apps heutzutage sind nicht sehr nützlich, so lange sie nicht mit dem Internet verbunden sind. Unsere App ist da keine Ausnahme. Wir müssen auf den AndroidPIT-RSS-Feed zugreifen, und der ist öffentlich zugänglich.

Zu diesem Zweck müssen wir einige Dinge lernen. Sobald Ihr die folgenden Konzepte gelernt habt, könnt Ihr sie immer wieder in Euren zukünftigen Android-Apps anwenden.

Zunächst brauchen wir die URL des RSS-Feeds. Hier kriegt Ihr sie.

Wenn Ihr mehr über RSS wissen wollt, lest Euch die entsprechende Wikipedia-Seite durch, denn sie ist wirklich hilfreich.

Zunächst werden wir die Android-Standardmethode zum Zugriff auf HTTP-Verbindungen (bspw. Herunterzuladen des Feeds) lernen, danach zeige ich Euch, wie ich es mache.

HttpUrlConnection

public class MainActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);


        TextView textView = new TextView(this);
        setContentView(textView);

        URL url;
        HttpURLConnection urlConnection = null;
        try {
            url = new URL("http://www.androidpit.com/feed/main.xml");
            urlConnection = (HttpURLConnection) url.openConnection();
            InputStream in = new BufferedInputStream(urlConnection.getInputStream());
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buffer = new byte[1024];
            for (int count; (count = in.read(buffer)) != -1; ) {
                baos.write(buffer, 0, count);
            }
            textView.setText(new String(baos.toByteArray(), "UTF-8"));
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
        }
    }
    
}






















Wenn Ihr die App nun ausführt, wird sie mit der folgenden Ausnahme abstürzen:

E/AndroidRuntime( 1689): java.lang.RuntimeException: 
Unable to start activity ComponentInfo{
me.henriquerocha.androidpitnews/
me.henriquerocha.androidpitnews.MainActivity
}: android.os.NetworkOnMainThreadException



Den Eintrag zu dem Absturz könnt Ihr im Logcat-Fenster in Android Studio nachlesen, und den Quellcode zu diesem Teil des Tutorials hier herunterladen.

Der Grund, warum die App abstürzt, ist, dass Ihr nicht im Haupt-Thread (oft auch User-Interface-Thread genannt) auf das Netzwerk (WLAN, 3G, 4G oder was auch immer) zugreifen dürft. Solltet Ihr nicht wissen, was Multi-Threading ist, lest bitte auf dieser Wikipedia-Seite mehr dazu.

Um das Problem nun zu beheben, müssen wir jenen Code ausführen, der auf das Netzwerk zugreift, allerdings muss das unbedingt in einem Hintergrund-Thread passieren, weil wir die App sonst blockieren, während der Code läuft.

Eine ganz einfache Lösung sieht folgendermaßen aus:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        final TextView textView = new TextView(this);
        setContentView(textView);

        new Thread() {
            @Override
            public void run() {
                URL url;
                HttpURLConnection urlConnection = null;
                try {
                    url = new URL("http://www.androidpit.com/feed/main.xml");
                    urlConnection = (HttpURLConnection) url.openConnection();
                    InputStream in = new BufferedInputStream(urlConnection.getInputStream());
                    ByteArrayOutputStream baos = new ByteArrayOutputStream();
                    byte[] buffer = new byte[1024];
                    for (int count; (count = in.read(buffer)) != -1; ) {
                        baos.write(buffer, 0, count);
                    }
                    
                    textView.setText(new String(baos.toByteArray(), "UTF-8"));














                } catch (MalformedURLException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    if (urlConnection != null) {
                        urlConnection.disconnect();
                    }
                }
            }
        }.start();










    }

Wenn Ihr die App nun ausführt, wird sie noch mal abstürzen und folgende Fehlermeldung anzeigen:

E/AndroidRuntime( 2303): FATAL EXCEPTION: Thread-101
E/AndroidRuntime( 2303): java.lang.SecurityException: 
Permission denied (missing INTERNET permission?)

Berechtigungen

Um Zugang zum Internet zu kriegen, muss die App explizit angeben, dass sie diesen benötigt. Das bewerkstelligt Ihr, indem Ihr folgenden Code zu Eurer AndroidManifest-Datei hinzufügt:

    <uses-permission android:name="android.permission.INTERNET" />

Kommunikation mit dem UI-Thread

Wenn Ihr die App nun noch einmal ausführt, wird es wieder zu einem Absturz kommen, hier die Fehlermeldung:

E/AndroidRuntime( 2367): FATAL EXCEPTION: Thread-104
E/AndroidRuntime( 2367): android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

Das Problem ist, dass

textView.setText(new String(baos.toByteArray(), "UTF-8"));

im UI-Thread laufen sollte, doch wir führen den Code im Hintergrund-Thread aus.

Die Activity liefert eine runOnUiThread()-Methode, welche ein Runnable erhält, das im UI-Thread läuft. Dort kann der aus dem Netzwerk heruntergeladene Inhalt des RSS-Feeds nun angezeigt werden. Dazu definiert Ihr die TextView als eben jenen vom Netzwerk erhaltenen Text.

Hierzu erweitern wir die Activity um ein Laufzeitobjekt (Runnable), welches im UI-Thread ausgeführt wird.  

                    final String rssFeed = new String(baos.toByteArray(), "UTF-8");
                    runOnUiThread(
                            new Runnable() {
                                @Override
                                public void run() {
                                    textView.setText(rssFeed);
                                }
                            });






Ersetzt also einfach die oben genannte Zeile mit diesem Code und führt die App erneut aus. Sie zeigt nun den RSS-Feed an. Herzlichen Glückwunsch!

Jetzt geht’s ans Scrollen

Ich weiß, noch haben wir hier kein Kunstwerk vor uns, doch immerhin haben wir jetzt Inhalte, mit denen wir arbeiten können.

Layouts in XML

In Android werden Layouts überlichweise in XML spezifiziert. Das ist ein deutlich besserer Ansatz, als die Widgets per Hand im Java Code herzustellen, wie wir es mit TextView getan haben.

Um das Scrollen innerhalb des RSS Feeds zu ermöglichen, müssen wir die TextView in eine ScrollView einbetten. Zu diesem Zweck erstellen wir eine XML-Datei im res/layout-Verzeichnes namens activity_main.xml.

Klickt nun mit rechts auf den Res-Ordner und wählt New>New resource directory und tippt layout als den Verzeichnisnamen ein. Innerhalb dieses Verzeichnisses erstellt Ihr dann eine neue Datei mit dem Namen activity_main.xml. Android Studio wird nun eine Vorschau des Layouts für Euch erstellen.

Ich ziehe es meistens vor, die XML direkt zu tippen, da ich das gewohnt bin. Zu dem Zweck wechselt aus dem Design-Tab ins Text-Tab und ersetzt den Inhalt der Datei mit:

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">



    <TextView
        android:id="@+id/rss_feed"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
    
</ScrollView>




Beachtet, dass unsere TextView eine ID hat. Diese werden wir verwenden, um auf die TextView innerhalb unserer Activity zuzugreifen. Unser bisheriger Code, der so aussah:

        final TextView textView = new TextView(this);
        setContentView(textView);

Sieht nun so aus:

        setContentView(R.layout.activity_main);
        final TextView textView = (TextView) findViewById(R.id.rss_feed);

Wenn Ihr die App nun erneut ausführt, könnt Ihr scrollen und den gesamten RSS Feed lesen. Das ist schon ganz nett, doch wir können das besser.

OkHttp

Obwohl Android die HttpUrlConnection bereitstellt, empfinde ich den Code immer als hässlich. Glücklicherweise haben die Jungs und Mädels bei Square die OkHttp-Bibliothek hergestellt, die den Code deutlich einfacher aussehen lässt.

Als erstes müsst Ihr die OkHttp-Bibliothek den Depencendies Eures Projekts in der build.gradle-Datei hinzufügen. Öffnet hierzu den Gradle-Scripts-Abschnitt in Eurer Projektstruktur und dann build.gradle (Module: app).

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:21.0.3'
    compile 'com.squareup.okhttp:okhttp:2.2.0'
}



So sollte es aussehen.

Jetzt könnt Ihr Euren Code so verändern, damit er wie folgt aussieht:

        new Thread() {
            @Override
            public void run() {
                try {
                    Request request = new Request.Builder()
                            .url("http://www.androidpit.com/feed/main.xml")
                            .build();





                    OkHttpClient client = new OkHttpClient();
                    Response response = client.newCall(request).execute();
                    final String rssFeed = response.body().string();

                    runOnUiThread(
                            new Runnable() {
                                @Override
                                public void run() {
                                    textView.setText(rssFeed);
                                }
                            });
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }.start();











Das ist definitiv einfacher zu lesen.

AsyncTask

Es ist beim Android-Programmieren so weit verbreitet, Dinge im Hintergrund laufen zu lassen, dass das SDK ein nettes Muster bereitstellt, dank dessen Ihr Euch nicht direkt mit Threads herumschlagen müsst. Ganz zu schweigen von den Problemen mit unserem aktuellen Code, zum Beispiel, wenn der Nutzer den Bildschirm rotiert, während der Network Call durchgeführt wird.

Das AsyncTask, das Android mit sich bringt, ist nicht wirklich eine gute Lösung für den Umgang mit Hintergrundprozessen, doch es ist derart weit verbreitet, dass ich es für relevant genug halte, um es Euch hier beizubringen, da Euch das Thema sicherlich an anderer Stelle begegnen wird.

Hierzu erstellen wir eine Klasse GetFeedTasks und erweitern diese mit der AsyncTask Klasse.  Diese bringt eine Methode DoInBackground und OnPostexecute mit sich. 

Diese passen wir wie folgt an. 

ublic class MainActivity extends Activity {

    private TextView rssFeedTextView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activity_main);

        rssFeedTextView = (TextView) findViewById(R.id.rss_feed);

        new GetFeedTask().execute();
    }

    private class GetFeedTask extends AsyncTask<Void, Void, String> {

        @Override
        protected String doInBackground(Void... params) {
            try {
                Request request = new Request.Builder()
                        .url("http://www.androidpit.com/feed/main.xml")
                        .build();




                return new OkHttpClient().newCall(request).execute().body().string();

            } catch (IOException e) {
                e.printStackTrace();
                return null;
            }
        }



        @Override
        protected void onPostExecute(String s) {
            if (s != null) {
                rssFeedTextView.setText(s);
            }
        }
    }
}






Sieht doch schon mal viel besser aus!

Und damit wären wir am Ende von Teil 2 dieser Serie. Hier geht’s weiter.

22 mal geteilt

Top-Kommentare der Community

  • Thomas H. 16.01.2015

    Erst einmal Hut ab, dass ihr diesen Schritt wagt und auch teilweise bestimmt unbequeme Rückfragen und Kommentare in Kauf nimmt!

    Und jetzt gleich mal eine konstruktiv gemeinte Kritik zu den ersten gezeigten Codezeilen, als Tipp für alle Entwickler. Tut euch den Gefallen und nennt eine Variable NIE exakt wie den zugrunde liegenden Datentyp. Zum einen verwirrt das eventuell bei der Verwendung im Quelltext (ist das jetzt die Variable oder ein Cast?) und zum anderen wird es sehr unübersichtlich, wenn alle URLs url1, url2 usw. heißen.

    Über 20 Jahre Entwicklungserfahrung haben mir gezeigt, dass es eine gute Idee ist den Anfangsbuchstaben des Datentypen als Anfangsbuchstaben des Variablennamens zu nutzen und danach mit einer möglichst sprechenden Bezeichnung im CamelCasing fortzufahren. Für obigen Code zum Beispiel: URL uFeedLocation.

    Das macht den Code nicht nur für andere lesbarer, sondern auch für einen selbst, wenn man nach längerer Pause ein Projekt wieder aufnimmt.

  • Max Mustermann 15.01.2015

    Sehr geil, danke! Bitte weitermachen :)

13 Kommentare

Neuen Kommentar schreiben:
  • Im Zweiten teil, wo zum teufel schreibe ich das alles rein? hat jemand den sourcecode?

  • Danke du machst das echt gut und es gibt wirklich kaum jemdánden, der das überhaupt so wirklich von 0 aufzieht aber, bei mir kommt es leider schon beim ersten code dieses Teils zu Fehlern und zwar fängt es damit an, dass er die fehlermeldung: "connot find symbol bundle" und das selbe mit text view, url und httpurlconnection

  • wow - heidenei ... gibts nicht schon eine visualAndroid++ Entwicklungsumgebung die den ganzen Kram im Hintergrund erledigt?

  • Sehr schönes Tutorial! Weiter so! Würde mich freuen, wenn das nächste Tutorial so bald wie möglich kommt... :-)

  • Erst einmal Hut ab, dass ihr diesen Schritt wagt und auch teilweise bestimmt unbequeme Rückfragen und Kommentare in Kauf nimmt!

    Und jetzt gleich mal eine konstruktiv gemeinte Kritik zu den ersten gezeigten Codezeilen, als Tipp für alle Entwickler. Tut euch den Gefallen und nennt eine Variable NIE exakt wie den zugrunde liegenden Datentyp. Zum einen verwirrt das eventuell bei der Verwendung im Quelltext (ist das jetzt die Variable oder ein Cast?) und zum anderen wird es sehr unübersichtlich, wenn alle URLs url1, url2 usw. heißen.

    Über 20 Jahre Entwicklungserfahrung haben mir gezeigt, dass es eine gute Idee ist den Anfangsbuchstaben des Datentypen als Anfangsbuchstaben des Variablennamens zu nutzen und danach mit einer möglichst sprechenden Bezeichnung im CamelCasing fortzufahren. Für obigen Code zum Beispiel: URL uFeedLocation.

    Das macht den Code nicht nur für andere lesbarer, sondern auch für einen selbst, wenn man nach längerer Pause ein Projekt wieder aufnimmt.

  • Super, mehr davon!
    Vielleicht noch parallel ein kleines ein Video dazu um die Sache zu komplettieren.

  • Wow ich bin mir echt nicht ob das ein richtiger Einstiegspunkt für Anfänger ist...

    • Hallo Mete,
      Danke fürs Feedback. Wo wäre dieser für Dich?
      Sollte dies zu umfrangreich sein, schau mal nach PhoneGap / Cordova.
      VG,
      Erik

  • Vielen Dank! Kann ich sehr gut gebrauchen das Tutorial :)

  • Super Tutorial!

  • Sehr geil, danke! Bitte weitermachen :)

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

Alles klar!