- Forum-Beiträge: 1.793
04.09.2013, 18:31:25 via Website
04.09.2013 18:31:25 via Website
Da hier wirklich oft nachgefragt wird, wie man eine Webseite herunterlädt, werde ich hier mal ein (mehr oder weniger) kurzes Tutorial dazu posten!
Wer den KOMPLETTEN CODE lesen will, der steht weiter unten. Wer das Tut ließt, lernt aber mehr
Das Problem:
Das Problem beim Download einer Webseite/beliebigen Datei aus dem Internet ist, dass dieser nicht in wenigen Millisekunden durchzuführen ist. Das liegt daran, dass, bis man die Datei von dem Webserver bekommt, schon mehrere Sekunden vergehen können. In Gebieten mit schlechter Internetverbindung sogar noch länger.
Während wir nun also auf die Antwort des Webservers warten, kann unsere App nichts anderes tun. Das bedeuted, dass der User z.B. keine Knöpfe anklicken kann (z.B. zum Abbrechen), die Anzeige nicht aktualisiert werden kann (z.B. Status des Downloads) und nach 5 Sekunden Android die App automatisch mit einer "Die App reagiert nicht mehr"-Meldung beendet.
Man muss also bewerkstelligen, dass die App zwei Aufgaben gleichzeitig ausführt: Einmal warten auf den Download der Datei, andererseits muss sich die App um die Anzeige für den User kümmern. Um diese parallele Abarbeitung von Aufgaben zu bewerkstelligen, muss man Threads nutzen.
Jeder Thread steht dabei für eine Aufgabe, um die er sich kümmert. Das Betriebssystem (also Android) gibt dem Thread ein paar Millisekunden Zeit den Prozessor zu benutzen und so seine Aufgabe fortzuführen, dann wird der aktuelle Zustand gespeichert und der nächste Thread bekommt das Recht, für ein paar Millisekunden den Prozessor zu nutzen und somit seine Aufgaben auszuführen.
Einen Thread "hat" eine App schon: Den UI-Thread. UI (engl.) steht dabei für "user interface" also "Benutzeroberfläche" und kümmert sich dementsprechend genau darum. Dieser Thread wacht z.B. auf Tippen auf Buttons, zeichnet den Text aus den TextViews auf den Bildschirm usw. usf.
Für den Download müssen wir also einen Download-Thread erstellen, der (z.B. auf Tippen eines Buttons) vom UI-Thread gestartet wird, und ab da automatisch parallel zum UI-Thread ausgeführt wird.
Übrigens, wer den UI-Thread für Netzwerkverbindungen benutzt (also nicht einen neuen Thread erstellt), wird ab Android 3+ mit einer NetworkOnMainThreadException bestraft, die App stürzt also mit der bekannten "Tut uns leid..."-Meldung ab.
Ein wichtiger Hinweis:
Aus bestimmten Gründen dürfen Views (also Button's, TextView's, FrameLayout's usw.) nur von/in dem Thread verändert werden, der sie erstellt hat. Das ist immer der UI-Thread. Der Text eines TextViews zur Ausgabe der Webseite beispielsweise darf nicht von dem Download-Thread durch setText(...) geändert werden. Das darf nur der UI-Thread tun. Haltet ihr euch nicht daran, stürzt eure App mit folgender Fehlermeldung ab: Only the original thread that created a view hierarchy can touch its views.
Wer mehr über Threads lernen möchte oder meine Erklärung nicht verstanden hat, kann z.B. hier weiter lesen.
Die praktische Umsetzung:
Wir müssen als erstes einen neuen Thread erstellen. Am einfachsten ist es dabei, eine neue Klasse zu erstellen, die entweder von java.lang.Thread oder android.os.AsyncTask erbt. AsyncTask ist auf Android zugeschnitten und etwas komfortable zu benutzen.
Daher verwende ich den AsyncTask.
Als erstes erstellt man also eine neue Klasse:
2}
Die zweite Klasse ist erstmal unwichtig für dieses Tutorial. Unten findet ihr einen kleinen "Exkurs".
Der letzte Parameter gibt an, welches Ergebnis wir am Ende der Aufgabe (bzw. des Threades/AsyncTaskes) zurückliefern wollen: Für den Quelltext einer Webseite eignet sich natürlich String.
Ergänzen wir unserer AsyncTask-Klasse folgende Methode:
2protected String doInBackground(String... urls) {
3String response = "";
4for (String url : urls) {
5response += downloadWebPage(url);
6}
7return response;
8}
Wenn wir den AsyncTask starten, erstellt dieser automatisch einen neuen Thread, der die doInBackground-Funktion ausführt.
Der Rückgabewert der Funktion (String) war die dritte Klasse, die wir oben angegeben haben. Der Parameter (String... urls) wurde durch die erste Klasse oben spezifiert. (Für alle, die die drei Punkte nicht kennen.)
Unser Downloader kann also auch mehrere URLs laden und gibt deren Quelltexte dann in einem String hintereinander aus. (Der Sinn sei mal dahingestellt.) Es ist aber natürlich problemlos möglich nur eine URL anzugeben. (s.u.)
Die Schleife geht alle URLs einzeln durch und ruft die Methode downloadWebPage() auf und ergänzt deren Rückgabe zum response. Am Ende wird response zurückgegeben.
Hier könnte man auch einen StringBuilder benutzen, da das Verketten von Strings damit performanter ist. Aus Gründen der Verständlichkeit habe ich dies aber gelassen. Weitere Informationen zur Verwendung z.B. hier oder hier.
Doch nun zur interessanten Methode downloadWebPage(String url):
2try {
3 HttpClient client = new DefaultHttpClient();
4 HttpGet get = new HttpGet(url);
5 HttpResponse response = client.execute(get);
6 InputStream in = response.getEntity().getContent();
7
8 BufferedReader reader = new BufferedReader(
9 new InputStreamReader(in));
10
11 String source = "";
12 String tmp;
13 while ((tmp = reader.readLine()) != null) {
14 source += tmp;
15 }
16
17 return source;
18 } catch (IOException io) {
19 Log.e("Downloader", "Couldn't downlaod "+url);
20 io.printStackTrace();
21 return "Error when downloading Webpage "+url;
22 }
23}
Zuerst erstellen wir einen client(HttpClient) -vergleichbar mit einem Browser wie IE oder FireFox-, dann eine Anfrage (HttpGet) -vergleichbar mit der Eingabe einer URL in einem Browser. Dann lassen wir den client die Anfrage ausführen und bekommen dafür die Antwort (response) - also den Quelltext der Webseite/das Bild/was auch immer.
Um deren Inhalt zu lesen rufen wir response.getEntity().getContent() auf und bekommen dafür einen InputStream. Da man daraus nur Bytes lesen kann erstellen wir einen InputStreamReader und dann einen BufferedReader. Nun können wir ganz einfach Zeilenweise lesen, bis wir null erhalten, also nichts mehr gelesen werden kann (= Ende der Webseite). Hierbei bietet sich wieder ein StringBuilder an.
Eigentlich könnte unser AsyncTask so schon funktionieren, aber bisher sehen wir noch nicht viel von unserem Ergebnis! Nun kommen wir zum oben genannten Problem: In einem anderen Thread (und damit in der doInBackground-Methode des AsyncTasks) dürfen wir Views nicht verändern, also auch nicht den Text der Webseite z.B. in ein TextView schreiben.
Im AsyncTask kann man aber die onPostExecute()-Methode überschreiben: Diese wird nach der doInBackground von Android wieder auf dem UI-Thread ausgeführt, also dürfen Views in der onPostExecute()-Funktion verändert werden.
Am besten, wir rufen einfach in der onPostExecute wieder unsere Activity auf, die dann das Ergebnis z.B. in einer TextView darstellt.
Dazu verwende ich ein Listener/Interface (genauso wie z.B. ein OnClickListener für einen Button): Die Activity muss, wenn sie ein Objekt vom Downloader erstellt, dann ein Objekt vom Listener/Interface übergeben:
2void onDownloadComplete(String result);
3}
4
5private DownloadCompleteListener dc = null;
6public Downloader(DownloadCompleteListener dc) {
7this.dc = dc;
8}
9
10@Override
11protected void onPostExecute(String result) {
12 dc.onDownloadComplete(result);
13}
Die Aktivity
Hier mal ein Beispiel, wie man unseren tollen Downloader benutzen kann:
2@Override
3 public void onCreate(Bundle arg) {
4super.onCreate(arg);
5 setContentView(R.layout.deinLayout);
6 Button btDownloadStart = (Button) findViewById(R.id.einButton);
7 btDownloadStart.setOnClickListener(new View.OnClickListener() {
8 @Override
9 public void onClick(View b) {
10 startDownload();
11 }
12 });
13 }
14
15}
2Downloader.DownloadCompleteListener dcl = new Downloader.DownloadCompleteListener() {
3@Override
4public void onDownloadComplete(String result) {
5TextView tv = (TextView) findViewById(R.id.eineTextView);
6tv.setText(result);
7}
8};
9
10Downloader downloader = new Downloader(dcl);
11downloader.execute("Eine URL, die geladen werden soll");
12}
Dann wird ein neues Objekt unseres AsyncTask erstellt und mittels execute() ausgeführt. Wichtig: Würde wir einfach nur die doInBackground()-Funktion ausführen, würde das wie bei jedem Objekt ganz normal auf dem UI-Thread passieren.
Wichtiger Hinweis: Jedes AsyncTask-Objekt kann nur einmal ausgeführt werden!
Weiteres
Jetzt solltet ihr in der Lage sein, eine Webseite herunterzuladen.
Folgendes ist nicht mehr notwendig zu wissen, um dies zu tun, aber für den ein oder anderen vielleicht ganz interessant:
Was ist, wenn man in der doInBackground etwas auf dem UI-Thread ausfphren möchte, z.B. um den aktuellen Fortschritt des Downloads anzuzeigen? Bisher verändern wir Views nur in der onPostExecute()-Funktion, dann ist der Download aber natürlich schon beendet.
Dafür gibt es die nette Funktion publishProgress(Void... progress). Das Void ist die mittlere der drei Klasse von
Rufen wir also publishProgress auf, wird in dem AsyncTask die Funktion onProgressUpdate() auf dem UI-Thread ausgeführt, dass bedeuted, dort darf man Views wieder verändern.
Man kann also onProgressUpdate überschreiben:
2 protected void onProgressUpdate(Void... progress) {
3 //Hier Views verändern, um z.B. den Download-Fortschritt anzuzeigen
4 }
Der komplette Code
So nun noch einmal den kompletten Code:
Downloader.java
2import java.io.IOException;
3import java.io.InputStream;
4import java.io.InputStreamReader;
5
6import org.apache.http.HttpResponse;
7import org.apache.http.client.HttpClient;
8import org.apache.http.client.methods.HttpGet;
9import org.apache.http.impl.client.DefaultHttpClient;
10
11import android.os.AsyncTask;
12import android.util.Log;
13
14public class Downloader extends AsyncTask<String, Void, String> {
15
16 @Override
17 protected String doInBackground(String... urls) {
18 String response = "";
19 for (String url : urls) {
20 response += downloadWebpage(url);
21 }
22
23 return response.toString();
24 }
25
26 private String downloadWebpage(String url) {
27 try {
28 HttpClient client = new DefaultHttpClient();
29 HttpGet get = new HttpGet(url);
30 HttpResponse response = client.execute(get);
31 InputStream in = response.getEntity().getContent();
32 BufferedReader reader = new BufferedReader(
33 new InputStreamReader(in));
34 String source = "";
35 String tmp;
36 while ((tmp = reader.readLine()) != null) {
37 source += tmp;
38 }
39
40 return source;
41 } catch (IOException io) {
42 Log.e("Downloader", "Couldn't downlaod " + url);
43 io.printStackTrace();
44 return "Error when downloading Webpage" + url;
45 }
46 }
47
48 public interface DownloadCompleteListener {
49 void onDownloadComplete(String result);
50 }
51
52 private DownloadCompleteListener dc = null;
53
54 public Downloader(DownloadCompleteListener dc) {
55 this.dc = dc;
56 }
57
58 @Override
59 protected void onPostExecute(String result) {
60 dc.onDownloadComplete(result);
61 }
62
63}
Und MainActivity.java
2import android.os.Bundle;
3import android.view.View;
4import android.widget.Button;
5import android.widget.TextView;
6
7public class MainActivity extends Activity {
8 @Override
9 public void onCreate(Bundle arg) {
10super.onCreate(arg);
11 setContentView(R.layout.einLayout);
12 Button btDownloadStart = (Button) findViewById(R.id.einButton);
13 btDownloadStart.setOnClickListener(new View.OnClickListener() {
14 @Override
15 public void onClick(View b) {
16 startDownload();
17 }
18 });
19 }
20
21 private void startDownload() {
22 Downloader.DownloadCompleteListener dcl = new Downloader.DownloadCompleteListener() {
23 @Override
24 public void onDownloadComplete(String result) {
25 TextView tv = (TextView) findViewById(R.id.eineTextView);
26 tv.setText(result);
27 }
28 };
29
30 Downloader downloader = new Downloader(dcl);
31 downloader.execute("Eine URL, die geladen werden soll");
32 }
33}
So, ich hoffe ihr kommt damit weiter! ;)
— geändert am 01.03.2014, 00:29:51
Liebe Grüße impjor.
Für ein gutes Miteinander: Unsere Regeln
Apps für jeden Einsatzzweck
Stellt eure App vor!
Empfohlener redaktioneller Inhalt
Mit Deiner Zustimmung wird hier ein externer Inhalt geladen.
Mit Klick auf den oben stehenden Button erklärst Du Dich damit einverstanden, dass Dir externe Inhalte angezeigt werden dürfen. Dabei können personenbezogene Daten an Drittanbieter übermittelt werden. Mehr Infos dazu findest Du in unserer Datenschutzerklärung.