====== 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.