JSON Request in Java verarbeiten

  • Antworten:30
  • OffenNicht stickyBentwortet
  • Forum-Beiträge: 26

21.07.2012, 00:03:25 via Website

Hallo!

Ich versuche mir gerade ein gescheites Konzept für JSON Aufrufe innerhalb der Anwendung zu überlegen. Wie macht ihr da die Architektur?

Mich interessiert vor allem wie ihr mit asynchronen Threads bei Netzwerkaufrufen umgeht. Ich übergebe hier nämlich den Context der Activity und lasse dann die Methode ausführen wenn der Thread in meinem JSON Service beendet ist.

Das hier funktioniert... ich frag mich nur ob es sauber ist. Ein Beispiel für eine Authentifierung mit einem PHP Backend:

1<?php
2 include("configuration/database.php");
3 $json = file_get_contents('php://input');
4 $obj = json_decode($json);
5 $result2=mysql_query("SELECT id, activationcode FROM users WHERE email='".$obj->{'email'}."' AND password='".$obj->{'password'}."'") or die(mysql_error());
6
7 $user="nouser";
8 while ($row = mysql_fetch_array($result2, MYSQL_NUM)) {
9 $user=$row[0];
10 $activationcode=$row[1];
11 }
12
13 $posts = array(1);
14
15 if($user!="nouser")
16 {
17 if($activationcode=="activated")
18 {
19 session_start();
20 session_register("userid");
21 $_SESSION["userid"] =$user;
22 header('Content-type: application/json');
23 echo "success";
24 }
25 else
26 {
27 $registrationcode=substr(md5($obj->{'email'}." ".$obj->{'password'}),6);
28 mysql_query("UPDATE users SET activationcode='".$registrationcode."' WHERE email='".$obj->{'email'}."'") or die(mysql_error());
29 mail($obj->{'email'},"Registrationcode for contactupload.com","Hello!<br><br>\n You or someone else has registered this email address via mobile phone.<br><br>\nThe generated registrationcode is ".$registrationcode.".<br><br>\nPlease type this registrationcode in the register section of the contact sync app.<br><br>\nWith best regards,<br>\ncontactupload.com");
30 echo "not activated";
31 }
32 }
33 else
34 {
35 echo "not registered";
36 }
37 mysql_close($con);
38?>

Das hier ist die MainActivity, die onLoginResult implementiert, welche aufgerufen wird wenn der Thread des JSON Aufrufs (siehe weiter unten) beendet ist:

1package com.caprisoft.contactupload;
2
3import com.caprisoft.contactupload.services.BjoernsServiceCaller;
4
5import android.os.Bundle;
6import android.app.Activity;
7import android.util.Log;
8import android.view.Menu;
9import android.view.View;
10import android.widget.EditText;
11import android.widget.Toast;
12
13public class MainActivity extends Activity {
14
15 private EditText email = null;
16 private EditText passwort = null;
17
18 /** Called when the user selects the Send button */
19 public void onClickLoginButton(View view) {
20 // Do something in response to button
21 Log.d(this.getClass().toString(), "Login Button gedrückt!");
22 BjoernsServiceCaller.getInstance().loginUser(this, email.getText().toString(), passwort.getText().toString());
23 }
24
25 /** Called when the user selects the Send button */
26 public void onClickRegisterButton(View view) {
27 // Do something in response to button
28 Log.d(this.getClass().toString(), "Register Button gedrückt!");
29 }
30
31 public void onLoginResult(String text)
32 {
33 Toast.makeText(this, "HALLO: "+text, Toast.LENGTH_LONG).show();
34 }
35
36 @Override
37 public void onCreate(Bundle savedInstanceState) {
38 super.onCreate(savedInstanceState);
39 setContentView(R.layout.activity_main);
40
41 email = (EditText) findViewById(R.id.textUsername);
42 passwort = (EditText) findViewById(R.id.textPassword);
43 }
44
45 @Override
46 public boolean onCreateOptionsMenu(Menu menu) {
47 getMenuInflater().inflate(R.menu.activity_main, menu);
48 return true;
49 }
50
51
52}

Und hier wollte ich die Services aufrufenn:

1package com.caprisoft.contactupload.services;
2
3import java.io.BufferedReader;
4import java.io.InputStreamReader;
5
6import org.apache.http.HttpResponse;
7import org.apache.http.client.HttpClient;
8import org.apache.http.client.methods.HttpPost;
9import org.apache.http.entity.StringEntity;
10import org.apache.http.impl.client.DefaultHttpClient;
11import org.apache.http.message.BasicHeader;
12import org.apache.http.params.HttpConnectionParams;
13import org.apache.http.protocol.HTTP;
14import org.json.JSONArray;
15import org.json.JSONObject;
16import org.json.JSONTokener;
17
18import com.caprisoft.contactupload.MainActivity;
19
20import android.content.Context;
21import android.os.Looper;
22import android.util.Log;
23import android.widget.Toast;
24
25
26public class BjoernsServiceCaller
27{
28
29 final String authenticationURL = "http://www.capri-soft.de/android/authenticationservice.php";
30 private static BjoernsServiceCaller instance = null;
31 private BjoernsServiceCaller() {}
32
33
34 /**
35 * Statische Methode, liefert die einzige Instanz dieser
36 * Klasse zurück
37 */
38 public static BjoernsServiceCaller getInstance()
39 {
40 if (instance == null)
41 {
42 instance = new BjoernsServiceCaller();
43 }
44 return instance;
45 }
46
47
48 public void loginUser(final MainActivity ctx, final String email, final String password)
49 {
50 Thread t = new Thread()
51 {
52 public void run()
53 {
54 Looper.prepare(); //For Preparing Message Pool for the child Thread
55
56 HttpClient client = new DefaultHttpClient();
57 HttpConnectionParams.setConnectionTimeout(client.getParams(), 10000); //Timeout Limit
58 HttpResponse response;
59 JSONObject json = new JSONObject();
60 try
61 {
62 HttpPost post = new HttpPost(authenticationURL);
63 json.put("email", email);
64 json.put("password", password);
65 StringEntity se = new StringEntity( json.toString());
66 se.setContentType(new BasicHeader(HTTP.CONTENT_TYPE, "application/json"));
67 post.setEntity(se);
68 response = client.execute(post);
69 /*Checking response */
70 if(response!=null)
71 {
72 BufferedReader reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), "UTF-8"));
73 String derText = reader.readLine();
74 ctx.onLoginResult(derText);
75 }
76
77 }
78 catch(Exception e)
79 {
80 e.printStackTrace();
81 }
82 Looper.loop(); //Loop in the message queue
83 }
84 };
85 t.start();
86 }
87}

Wie würdet ihr das machen?

Antworten
  • Forum-Beiträge: 636

21.07.2012, 10:38:18 via Website

Hi,
folgende Gedanken habe ich dazu:

1. Du verschickt die Logindata im Klartext via http und das ist immer bedenklich. Somit den Login via https laufen lassen oder die Daten eben verschlüsseln.

2. Du übergibst an einen Thread den Context einer Activity. Das ist meiner Meinung nach schlechter Stil, da du dir so Mem-Leaks einfangen kannst.
Der User klickt auf Login und verlässt deine MainActivity und schon kann der GC nicht sauber aufräumen, da der ctx von der MainActivity im Thread hängt. Jetzt kann man argumentieren das der Login eh nicht so lange braucht und das schon alles gut gehen wird aber ich finde es unsauber.
Für diese Sachen nutze ich gerne den AsyncTask und zeigt eine "Login..."-Dialog an.

Gruß,
Markus

Antworten
Gelöschter Account
  • Forum-Beiträge: 694

21.07.2012, 13:50:37 via Website

Der Context wird nicht nur übergeben - er wird auch im run() genutzt. Nur der try/catch Block mit un-spezifizierter Exception drumherum verhindert das dieses Konstrukt in sich zusammenfällt.

Das folgende Skelett nutzt einen AsyncTask als InnerClass in der Activity. Beide "patchen" sich gegenseitig wichtige Variablen. AsyncTask zeigt der Activity ob er läuft oder nicht, Activity tauscht den Context im AsyncTask aus wenn z.B. ein OrientationChange statt fand. Das lässt sich beliebig erweitern:

1public class MyActivity extends Activity {
2
3 private class MyAsyncTask extends AsyncTask<..., ..., ...> {
4
5 /* package */ MyActivity context;
6
7 public MyAsyncTask(MyActivity context) {
8 super();
9
10 this.context = context;
11 }
12
13 @Override
14 protected ... doInBackground(...) {
15 // Context nicht nutzen
16 }
17
18 @Override
19 protected ... onPostExecute(...) {
20 if (context != null) {
21
22 // Context nutzen
23
24 // Task endet
25 context.task = null;
26 }
27 }
28
29 @Override
30 protected ... onPreExecute () {
31 }
32 }
33
34 /* package */ MyAsyncTask task;
35
36 @Override
37 public void onCreate(Bundle bundle) {
38 // ...
39 task = (MyAsyncTask) getLastNonConfigurationInstance();
40 if (task != null) {
41 // Context austauschen
42 task.context = this;
43 }
44 // ...
45 }
46
47 @Override
48 public Object onRetainNonConfigurationInstance() {
49 if (task != null) {
50 task.context = null;
51 }
52
53 return task;
54 }
55 }

— geändert am 21.07.2012, 13:52:46

Antworten
  • Forum-Beiträge: 26

21.07.2012, 14:22:04 via Website

Hallo!

Danke für eure Antworten. Muss dieser AsyncTask immer eine Inner Class einer Activity sein oder Kapselt ihr die ganzen Aufrufe in eigene Dateien manchmal weg ? Wenn man es irgendwie wegkapselt, kann man dann irgendeinen Listener nutzen der die DaTen im View-Context setzt?

Viele Grüße

Antworten
  • Forum-Beiträge: 2.243

21.07.2012, 16:25:48 via Website

Also meist ist es übersichtlicher die AsyncTasks in eigene Klassen auszulagern.
Speziell natürlich, wenn man die gleichen in mehreren Activities nutzt.

Um die Oberfläche aus dem Task zu aktualisieren, kann Du z.B. die aufrufende Activity im Konstruktor übergeben und dort entsprechende Methoden bereitstellen. Wenn es ganz sauber sein soll das ganze dann auch über eine Art Listener-Interface kapseln und die Activity das Interface implementieren lassen.

Antworten
  • Forum-Beiträge: 2

21.07.2012, 17:11:54 via Website

kann mich meinen vorrednern nur anschließen.
solche aufgaben immer in einer async task laufen lassen und diese am besten in einer extra klasse halten. macht den code einfach übersichtlich und aufgeräumt.
der asyn task im konstruktor die aufrufende activity mitgeben und dann ist der onPostExecute() eine methode in der activity wieder rufen.

Antworten
Gelöschter Account
  • Forum-Beiträge: 694

21.07.2012, 20:30:44 via Website

Ich bin ja nicht unbedingt der Java Spezialist deshalb würde ich mich freuen wenn mir das jemand mal erklärt:

* Activity im Konstruktor des ausgelagerten AsyncTask: Während der Laufzeit im doInBackground fliegt die Activity weg (OrientationChange, oder was auch immer). Der AsyncTask kommt anschließend in den onPostExecute und nutzt die übergebene Activity-Instanz. Was passiert dann? Diese Activity-Instanz ist nach dem OrientationChange weg. Wie handelt Ihr das wenn der AsyncTask ausgelagert ist? Haltet Ihr zum Swap der Activity-Instanz (des Context) Methoden bereit?

Antworten
  • Forum-Beiträge: 26

21.07.2012, 23:59:49 via Website

Danke nochmals!

Würde das übergeben des Kontextes der Main-Activity an den Async-Thread hier in diesem Fall z.B. mit der Aussage von Markus B. (Punkt 2.) kollidieren oder ist das in diesem Fall nichts verwerfliches?

Hat jdm. ein gutes Beispiel für einen weggekapselten AsyncTask mit Listener, der evtl. das rückgelieferte Datenobjekt an die MainActivity übergibt?

Viele Grüße

Antworten
Gelöschter Account
  • Forum-Beiträge: 694

22.07.2012, 12:10:59 via Website

Danke für den Link. Ich habe mir das nochmal genau angeschaut. Das komplexe Beispiel (das letzte) entspricht meiner oben gezeigten Lösung - nur das der Autor mit einem ausgelagerten AsyncTask arbetet - statt wie ich mit einer Inner Class.

Warum nehme ich wenn möglich Inner Classes? Wenn man dem komplexen Beispiel eine Cursor-Beschaffung im Thread sowie einen ProgressDialog spendiert steigt die Komplexität der gekapselten Version immer mehr an. Für jedes geteiltes Objekt benötigst Du Getter/Setter (statt direkten Zugriff) sowie zusätzliche Listener. Schaut Euch mal die Google Samples an. Die arbeiten sehr oft mit Inner Classes (sogar Fragmente in Activities). Die erklären das sogar mit der Übersichtlichkeit ...

Methoden die nicht benötigt werden wenn AsyncTask in Inner Class:

* Task.isRunning(): Activity will wissen ob der Task noch im doInBackground aktiv ist
* Task.setContext(): Activity-Instanz muss entweder ausgetauscht (nach Neustart) oder ge-null-t werden.
* Task.createDialog(): Dialog muss nach Neustart der Activity-Instanz neu erzeugt werden

* Activity.setCursor(): Cursor liegt vor, nutz ihn
* Activity.setTask(): Zum Beispiel Task-Instanz auf NULL setzen. sobald Task geendet ist

Ich hatte mir damals aus dem folgenden Dokument zwei Topics etwas genauer angeschaut ("Avoid Internal Getters/Setters" sowie "Consider Package Instead of Private Access with Inner Classes"). Sobald öffentlicher Zugriff notwendig ist baue ich Getter/Setter - keine Frage. Sobald privater Zugriff möglich ist versuche ich das zu vermeiden. Ich weiß, als 100% Java Entwickler wird Alles und Jedes ausgelagert - deshalb ruckelt Android ja auch <SCNR>.

Designing for Performance

Die Unterschiede sind 100% pur Java-Design versus ein wenig praktischer Laxheit ;-)

Antworten
  • Forum-Beiträge: 636

22.07.2012, 12:58:45 via Website

Hi,
so was können wir als Resümee für dieses konkrete Problem hier merken:

Wie setzten also auf eine inner class damit wir definitiv keine Performance einbüßen. Des weiteren soll die Activity für den Anmeldeprozess per Dialog „gelockt“ werden. Damit wir uns das deklarieren einer inner class sparen können erstellen wir einen AsyncTask, welcher von Activity erbt.
Die Kommunikation mit der Server ist ja schon in einer Klasse ausgelagert. Ich würde hier auf das Instance-Patter verzichten, da es keine Variablen gibt, welche dieses rechtfertigen. Somit würde ich einen private Konstruktor anlegen und die Methoden statisch machen. Danach natürlich das Thread Zeugs entfernen und die passenden Methoden im doInBackground() im AsyncTask aufrufen.
So können deine Anforderungen umgesetzt werden und die Code bleibt übersichtlich.

Das wäre jetzt mein Plan.

Gruß,
Markus

Antworten
  • Forum-Beiträge: 2.243

22.07.2012, 19:38:42 via Website

OK das mit dem neu Erstellen der Activity bei Orientation Change hatte ich völlig vergessen, weil ich dieses Verhalten eigentlich immer per Manifest abstelle. Dann ergibt sich diese Frage garnicht.
IMHO ist das auch besser für die Performance weil der "harte" Orientation Change extrem viel Overhead erzeugt.

Antworten
Gelöschter Account
  • Forum-Beiträge: 694

22.07.2012, 22:24:52 via Website

Ich habe in der Anfangszeit auch den Orientation Change im Manifest modifiziert. Mit der Zeit habe ich aber festgestellt das die Lösung wirklich unsauber ist. Romain Guys (der Google GUI Entwickler) Lösung finde ich ebenfalls völlig daneben (er killt die Threads sobald die Activity abgeschossen wird).

Also habe ich in mehreren Stufen (hat ein paar Monate gedauert) eine gute Lösung gefunden (siehe oben). Das wird in allen meinen Apps verwendet. Ob ich Daten von Google lade (Tankbuch) oder von Garmin (GaCoMo), oder Google Drive/Wuala, jeder Datenbankzugriff der in einen Adapter geht oder sogar die vielen Maps und Charts mit tausenden Punkten - restlos alles wickele ich über das obige Skelett ab. Da kannst Du das Gerät rumdrehen bis Dir schwindlig wird die Threads laufen weiter, die ProgressDialoge werden abgebrochen und wieder neu aufgebaut und die Activities nutzen die Threads nach dem Neustart der Activity einfach weiter.

Android garantiert (!) das der Ablauf von onRetainNonConfigurationInstance, onDestroy, onCreate mehr oder weniger atomisch (nicht im CPU Sinne sondern im Android Message Queue Sinne) ist. Da kommt kein anderer Call zwischen. Der Sinn ist das der AsyncTask weiter läuft, parallel dazu wird die Activity beendet und wieder neu aufgebaut und der neue Context (die Instanz der Activity) wird dem Thread untergejubelt. Man muss dann im onPostExeceute des AsnycTask jede Variable/Methode der surrounding Activity mit "context." präfixen.und vorab auf <> NULL prüfen.

Ich habe heute, nach dem Thread hier, mal einen AsyncTask ausgelagert um mal zu gucken was ich alles anpassen muss. Nach einer Stunde habe ich das aufgegeben. Mann, jede Menge neue Methoden in den AsyncTask Klassen, jede Menge neue Methoden in den Activity Klassen - was für ein Overhead. Ich bleibe - wo es geht - bei meinen AsyncTasks als InnerClass der Actvities.

Habt Ihr Euch mal die neuen Loader (CursorLoader, etc.) angeschaut..Google ist wohl klar geworden was sie mit der alten Lösung verbockt haben. Die neue ist erheblich eleganter als die alten AsyncTasks. Dafür unterstützen die nur noch ContentProvider. Macht Sinn, ist aber für 98% der Entwickler völlig unpraktisch. Das bestätigt dann wieder mein Google/Android/Java-Bild: Alles streng nach Regelwerk/Technik - ohne Blick für die Realität ;-)

— geändert am 22.07.2012, 22:45:20

Antworten
  • Forum-Beiträge: 2.243

23.07.2012, 08:28:02 via Website

Harald Wilhelm
Ich habe in der Anfangszeit auch den Orientation Change im Manifest modifiziert. Mit der Zeit habe ich aber festgestellt das die Lösung wirklich unsauber ist.
Inwiefern unsauber?
Der einzige echte Vorteil, den ich beim killen/neubauen der Activity sehe ist die automatische Umstellung des Layouts, wenn man unterschiedliche für Hoch- und Querformat anbietet. Sonst hat das IMHO nur Nachteile. Sowohl in Sachen Performance als auch Programmieraufwand.

Antworten
Gelöschter Account
  • Forum-Beiträge: 694

23.07.2012, 10:04:23 via Website

Hast Du keine unterschiedlichen Layouts? Ich habe alleine für die Listen (z.B. im GaCoMo) fünf unterschiedliche Layout-Größen für die ListViews.

Es ist halt nur ein Workaround - nicht die empfohlene Lösung (O-Ton Doku): "This technique should be considered a last resort when you must avoid restarts due to a configuration change and is not recommended for most applications."

Außerdem, Du weißt nie was den Google Entwicklern noch so alles einfällt was Du später mal nachträglich in den String bei configChanges eintragen musst. Google ändert ja ohne Rücksicht auf den Migrationspfad permanent. Denk nur mal an den StrictMode.

Mit der Lösung oben bist Du vollständig raus aus der Nummer ...

— geändert am 23.07.2012, 10:05:52

Antworten
  • Forum-Beiträge: 2.243

23.07.2012, 10:14:21 via Website

Harald Wilhelm
Hast Du keine unterschiedlichen Layouts? Ich habe alleine für die Listen (z.B. im GaCoMo) fünf unterschiedliche Layout-Größen für die ListViews.
Die Größen fallen ja nicht unter diesen Spezialfall, weil die Auflösung des Geräts sich ja zur Laufzeit nicht ändert.
Die Umstellung zwischen Hoch- und Querformat behandle ich dann aber lieber im Code. Ist deutlich performanter.

Jeder hat da halt so seine Vorlieben und Prioritäten :)

— geändert am 23.07.2012, 10:15:36

Antworten
  • Forum-Beiträge: 26

28.07.2012, 19:17:44 via Website

Vielen Dank für eure Antworten!

Ich habe mir nun ein kleines Snipplet in Eclipse hinterlegt und das Instance-Pattern von BjoernsServiceCaller entfernt.

Ich wüsste jetzt nur nicht wie ich ein onProgressUpdate in einer ausgelagerten Kalkulation oder ähnlich machen sollte, da dies wieder den MainContext erfordern würde und solche Kalkulationen o.ä. ja in einer Schleife ablaufen ;)

Viele Grüße Björn

1/***********************************************************************************
2 * Hier beginnen AsyncTasks. AsyncTasks sollen für Operationen genutzt werden, die
3 * nur ein paar Sekunden andauern.
4 *
5 * Für eine effiziente Verarbeitung werden AsyncTasks zumeist nicht in extra Klassen
6 * ausgelagert, obwohl die möglich ist.
7 *
8 * Man legt mit 3 generischen Typen fest, welche Typen fest, was übergeben wird
9 * (1. Param), Mit welchem Typ die Progressbar geupdated wird (2. Param) und was
10 * der Rückgabewert nach der Operation ist (3. Parameter - Rückgabe doInBackground
11 * und Übergabeparameter von onPostExecute)
12 ***********************************************************************************/
13 // <Username/Passwort, ProgressZustand für Progressbar (Methode publishProgress in doInBackground aufrufen und onProgressUpdate implementieren), RückgabeStatus>
14 private class LoginAsyncTask extends AsyncTask<String, Void, String>
15 {
16 private ProgressDialog progressDialog;
17
18 protected void onPreExecute()
19 {
20 // Show small progress dialog
21 progressDialog = ProgressDialog.show(MainActivity.this, "Try to log in...", "Trying to authenticate your account please wait until we get response from the server");
22 }
23
24 @Override
25 protected String doInBackground(String... params) {
26 return BjoernsServiceCaller.loginUser(params[0], params[1]);
27 }
28
29 protected void onPostExecute(String result) {
30 // TODO Progress Fenster wieder schließen
31 progressDialog.dismiss();
32 Toast.makeText(MainActivity.this,result, Toast.LENGTH_LONG).show();
33 }
34
35 }

Antworten
  • Forum-Beiträge: 636

28.07.2012, 20:10:44 via Website

Hi,
dafür nutzt du einfach die Methode publishProgress. Diese bekommt dann ein Typ vom zweite Parameter deines AsyncTask, welchen du über die generics definiert hat.
Somit würde du in deine doInBackground-Methode immer dann publishProgress aufrufen, wenn du den User über den aktuellen Stand informieren willst.
Ein Bsp. kannst du in der API finden. http://developer.android.com/reference/android/os/AsyncTask.html (Gleich das Erste von Oben.)

Gruß,
Markus

Antworten
  • Forum-Beiträge: 26

28.07.2012, 21:36:53 via Website

Alles klar danke

Mal ne andere Frage....

wenn ich in der Inner Class z.B. ein Toast machen will, kann man ja mit MainActivity.this auf den Context des Parents zugreifen, solange die Klasse MainActivity heisst.

Da ich das jetzt in einem Snipplet definiert habe, würde mich interessieren ob sowas wie parent.this oder so gehen würde, damit ich nicht den Namen der Parent-Klasse (also die Outer Klasse der Activity obendrüber) angeben muss.

nur leider bekomme ich mit this.parent keine Referenz. Gibts da ne andere Möglichkeit?

Antworten

Empfohlene Artikel