====== Threads en Android ======
Aclarirem certs conceptes de //threading// aplicats a Android.
{{ android:androidthreads.jpg?450 }}
Referències:
* [[Android]]
* [[Android ListView]]
{{tag> #Dam #DamMp08 #DamMp08Uf01 #DamMp08Uf1 #DamMp08Uf02 #DamMp08Uf2 #DamMp09 #DamMp09Uf02 #DamMp09Uf2 android threads mobile java }}
===== Conceptes previs =====
El mes important treballant amb //threads// en Android és saber que:
* **Totes les operacions relacionades amb la interfície gràfica s'han d'executar des del //UI Thread//**. La majoria de codi que creem al iniciar una aplicació (//onCreate//, //onPause//, etc.) es fan des d'aquest //UI Thread//.
* **No podem bloquejar el //UI Thread//** en operacions com //sleep// o altres que prenguin molt de temps com per exemple les relacionades amb comunicacions, xarxa, descàrregues, etc. Per a això caldrà utilitzar //threads// secundaris.
No seguir alguna d'aquests dues premisses ens portarà a alguna excepció d'execució.
Per executar tasques curtes disposàvem oficialment de l'objecte //ad-hoc// [[https://developer.android.com/reference/android/os/AsyncTask|AsyncTask]], però a partir de la API 30 d'Android ha quedat obsolet i es recomana utilitzar el [[https://www.baeldung.com/java-util-concurrent|generic java.util.concurrent package]].
\\
===== Utilitzant ExecutorService =====
La forma oficial d'utilitzar //threads// actualment a Java és emprar la llibreria ''java.util.concurrent''.
Referències:
* [[https://stackoverflow.com/questions/58767733/the-asynctask-api-is-deprecated-in-android-11-what-are-the-alternatives|Alternatives to AsyncTask: ExcecutorService]]
* [[https://www.baeldung.com/java-util-concurrent|Article sobre java.util.concurrent]]
* [[https://docs.oracle.com/javase/7/docs/api/java/util/concurrent/package-summary.html|java.util.concurrent Summary]]
* Per ampliar: [[https://www.baeldung.com/java-executor-service-tutorial|Java ExcecutorService tutorial]].
Resumint, la forma recomanda actualment per executar un //thread// és aquesta:
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(new Runnable() {
@Override
public void run() {
// Tasques en background (xarxa)
Handler handler = new Handler(Looper.getMainLooper());
handler.post(new Runnable() {
@Override
public void run() {
// Tasques a la interfície gràfica (GUI)
}
});
}
});
\\
===== Executar operacions amb retard al UI thread =====
Si volem executar operacions en el UI Thread després d'un cert temps, [[https://stackoverflow.com/questions/53105652/executing-operations-on-ui-thread-after-delay|aquest post clarifica com utilitzar un Handler]] i un Runnable.
Les tasques enviades al ''Handler'' s'executaran en el mateix //thread// on s'ha creat. Per tant, si estem en un //thread// secundari, podem crear un ''Handler'' que envii missatges al //UI thread// indicat que el seu ''Looper'' és el MainLooper:
final Handler handler1 = new Handler(Looper.getMainLooper());
handler1.postDelayed(r, 1000);
on ''r'' és un Runnable.
\\
===== runOnUIThread =====
Seguint el què hem explicat, des d'un //thread// secundari no podem fer operacions sobre el UI. Si necessitem fer-ne, caldrà utilitzar:
* [[https://developer.android.com/reference/android/app/Activity#runOnUiThread(java.lang.Runnable)|Activity.runOnUiThread]] que s'encarregarà d'executar el codi del ''Runnable'' en el //UI thread//.
* [[https://developer.android.com/reference/android/os/Handler|Handler]]: permet enviar //messages// a un //thread//. Un ''Message'' pot ser un objecte amb text i dades, però també poden ser porcions de codi que es poden executar de forma asíncrona quan el //thread// pugui.
* ''Handler'' disposa de funcions com ''postDelayed'' que permet l'execució diferida d'un codi.
Teniu [[https://medium.com/@yossisegev/understanding-activity-runonuithread-e102d388fe93|aquest article]] que explica força coses de com funcionen els objectes ''Handler'', ''Looper'', ''Message'' i altres.
\\
===== Threads de comunicació =====
Una aplicació típica és la utilització de //threads// per a comunicació. Tal i com hem dit, no podem executar funcions que bloqueginen el mainUIthread d'Android, i les comunicacions ho fan en tant que esperen la resposta remota.
Posem aquest codi d'una crida HTTP en una funció ''getDataFromUrl'' que podem afegir a la nostra ''MainActivity''. Aquí tens més [[https://www.twilio.com/blog/5-ways-to-make-http-requests-in-java|info sobre llibreries per a crides HTTP en Java]].
Si l'executem dins el ''MainThread'' ens saltarà una excepció ''NetworkOnMainThreadException''. Per això necessitem el codi del //thread// de mes amunt per poder-la executar.
String error = ""; // string field
private String getDataFromUrl(String demoIdUrl) {
String result = null;
int resCode;
InputStream in;
try {
URL url = new URL(demoIdUrl);
URLConnection urlConn = url.openConnection();
HttpsURLConnection httpsConn = (HttpsURLConnection) urlConn;
httpsConn.setAllowUserInteraction(false);
httpsConn.setInstanceFollowRedirects(true);
httpsConn.setRequestMethod("GET");
httpsConn.connect();
resCode = httpsConn.getResponseCode();
if (resCode == HttpURLConnection.HTTP_OK) {
in = httpsConn.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(
in, "iso-8859-1"), 8);
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append("\n");
}
in.close();
result = sb.toString();
} else {
error += resCode;
}
} catch (IOException e) {
e.printStackTrace();
}
return result;
}
Per poder-se connectar a internet, cal [[https://developer.android.com/training/basics/network-ops/connecting|activar el permís Android d'accés a Internet]]:
\\
==== APIs i JSON ====
Les crides a APIs externes ens retornaran objectes JSON que cal descodificar. Per exemple, pots provar ''https://api.myip.com'' per veure la teva IP. Des de la //shell// fariem:
$ curl https://api.myip.com
{"ip":"139.47.113.84","country":"Spain","cc":"ES"}
Es recomana aquest [[https://stackoverflow.com/questions/16574482/decode-json-string-in-java-with-json-simple-library|exemple per descodificar els missatges JSON]].
Ull, perquè si el server de **myip.com** rep moltes peticions seguides **acaba per bloquejar-se ja que es pensa que som atacants**.
Si veieu que amb el CURL no funciona, cerqueu una altra API. Cerqueu alguna que us agradi d'aquest [[https://github.com/public-apis/public-apis|llistat de public APIs]]. Per exemple, aquesta ens retorna la URL d'una imatge aleatòria de guineus:
$ curl https://randomfox.ca/floof/
\\
===== Exercicis =====
Llençar comunicacions des d'un //thread// alternatiu:
- Crea un nou projecte [[Android]] amb un botó.
- Afegeix la funció ''getDataFromUrl'' i crida-la amb una URL (per exemple ''https://api.myip.com'') al prémer el botó, directament des del ''onClick''.
- Comprova que ens salta l'excepció ''NetworkOnMainThreadException''.
- Afegeix el codi amb ''Executor'' i crida la funció de xarxa per obtenir les dades d'una URL. Mostra les dades per la consola de //debug// amb ''Log.i''.
- No oblidis activar el permís Android per a accés a Internet o obtindràs una altra excepció.
Actualitzar la GUI:
- Afegeix un ''TextView'' en el projecte.
- Quan carreguem dades d'internet, actualitza-les en el ''TextView''. Comprova que si ho fem en el mateix cos del //thread//, la funció ''run'', funciona.
Anem a posar alguna acció gràfica que ens obligui a utilitzar el ''Handler'', que serà el què ens permetrà executar en el //thread// principal (el de gràfics).
Farem servir una ''ImageView'':
- Afegeix la ''ImageView'' al //layout//.
- Descarrega una imatge d'internet i transforma-la en ''Bitmap''. Ho podem fer així
String urldisplay = "https://randomfox.ca/images/122.jpg";
Bitmap bitmap;
try {
InputStream in = new java.net.URL(urldisplay).openStream();
bitmap = BitmapFactory.decodeStream(in);
} catch (Exception e) {
Log.e("Error", e.getMessage());
e.printStackTrace();
}
- Mostra la imatge al ''ImageView''. Comprova que al fer el ''imageView.setImageBitmap(bitmap)'' ens falla amb una excepció si ho fem al //thread// de comunicacions.
- Utilitza el ''Handler'' com a l'exemple i comprova que ara canvia la imatge i no peta.
Si et queda temps, crida la API randomfox (explicada mes amunt) i obtingues una imatge diferent cada cop, i mostra-la al ''ImageView''.
Per si et resulta avorrit, mes feina (exercici optatiu):
Anem a provar amb una GUI més ambiciosa: una ''ListView''.
- Afegeix una [[Android ListView]] al //layout//.
* Pots provar amb una simple ''ArrayList'' com a model per no haver de fer //layouts// personalitzats.
- Afegeix //items// al model quan premem el botó.
- Comprova que al refrescar el GUI des del //thread// de comunicacions amb
adapter.notifyDataSetChanged();
ens surt una ''ViewRootImpl$CalledFromWrongThreadException''.
- Posa el refresc del GUI dins el ''handler.post'' i comprova que funciona.
\\
====== Android i WebSockets ======
Per poder emprar la [[https://github.com/TooTallNate/Java-WebSocket|biblioteca de codi Java WebSockets]] en Android caldrà afegir algunes línies als arxius:
dependencies {
...
implementation(libs.websocket)
...
}
[versions]
...
websocket = "1.5.7"
[libraries]
...
websocket = { group = 'org.java-websocket', name = "Java-WebSocket", version.ref = "websocket" }
Recordem que per poder-se connectar a internet, cal [[https://developer.android.com/training/basics/network-ops/connecting|activar el permís Android d'accés a Internet]]:
Un cop fet tot això podrem emprar les biblioteques Java WebSockets al nostre codi:
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.drafts.Draft_6455;
import org.java_websocket.handshake.ServerHandshake;
...
WebSocketClient client = null;
URI location = "ws://mywsserver.com";
try {
client = new WebSocketClient(new URI(location), (Draft) new Draft_6455());
client.connect();
} catch (URISyntaxException e) {
e.printStackTrace();
System.out.println("Error: " + location + " no és una direcció URI de WebSocket vàlida");
}
client.send("Hola! M'acabo de connectar");
...
Teniu més explicacions i exemples a la pàgina [[WebSockets Java]] d'aquesta wiki.