Week 08
explicitný a implicitný Intent, intent filter, JSON, HttpURLConnection
Úvod
- http://sli.do#ki-smart - anketa a otázky pre prednášajúceho
- hackaton - (slide) - posledný novembrový víkend 25.-27.nov.2017 na tému Hack your life!
- kultúra - (slide) - v kine úsmev bude budúci týždeň 31.okt.2017 Halloweenska párty spojená s premietaním kultového filmu Shining
Mr. Iľko
- Dnes nás čaká nová aplikácia na predpoveď počasia. Pracovný názov aplikácie bude Mr. Iľko.
- Aby sme sa na začiatku nezdržiavali, mám rovno pripravenú kostru projektu, ktorú je možné stiahnuť z tejto linky. Tento projekt zahrňuje:
- dve aktivity -
SearchActivity
pre vyhľadávanie aDetailActivity
pre zobrazenie výsledkov - niekoľko pripravených zdrojov (najmä obrázkových)
- upravené konfiguračné súbory pre použitie knižnice ButterKnife
- dve aktivity -
Intents
- O zámeroch sme už hovorili pri baterke, kde sme ich používali na spustenie príslušnej služby. Podobným spôsobom ich použijeme aj tentokrát, keď budeme chcieť z jednej aktivity spustiť inú aktivitu. To však tiež nebude všetko, čo sa dnes o zámeroch naučíme.
- Existujú však dva typy zámerov:
- explicitný - špecifikuje komponent (obyčajne triedu), ktorý má byť spustený
- implicitný - nešpecifikuje komponent, ktorý má byť spustený, ale deklaruje akciu, ktorá má byť vykonaná (komponentom inej aplikácie)
- Ukážme si teda použitie najprv explicitného
Intent
-u, kedy po kliknutí na tlačidlo Search dôjde k prechodu z aktivitySearchActivity
do aktivityDetailActivity
.
Explicit Intent
Najprv teda vytvoríme metódu
search()
, po zavolaní ktorej dôjde k vytvoreniu explicitného intentu. Do tohto intentu pridáme aj extra údaj, ktorým bude zadané mesto pre vyhľadávanie. Následne vytvorený intent odovzdáme metódestartActivity()
, ktorá zabezpečí zobrazenie aktivityDetailActivity
a doručenie pripraveného intent-u priamo do nej.Predtým však vytvoríme príslušnú členskú premennú, ktorá bude referenciou na objekt typu
EditText
. Samozrejme priamo použijeme knižnicu ButterKnife, ktorej binding inicializujeme v metódeonCreate()
aktivity: ```java @BindView(R.id.edittext_search) EditText etInput;@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_search); ButterKnife.bind(this); } ```
Následne už vytvoríme samotnú metódu
onSearchClicked()
:java @OnClick(R.id.button_search) public void onSearchClicked(){ Intent intent = new Intent(this, DetailActivity.class); intent.putExtra("city", etInput.getText().toString()); startActivity(intent); }
Receiving Intent
Hneď po vytvorení akvity
DetailActivity
v metódeonCreate()
potrebujeme pristúpiť k odovzdanému intentu. To zabezpečíme volaním metódygetIntent()
priamo nad objektom aktivity. Z tohto intentu následne vytiahneme hodnotu parametracity
, vytvoríme z neho členskú premennú triedy a toast, kde len overíme jeho prijatie:public class DetailActivity extends AppCompatActivity { private String city; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_detail); .bind(this); ButterKnife = this.getIntent(); Intent intent this.city = intent.getStringExtra("city"); .makeText(this, this.city, Toast.LENGTH_SHORT).show(); Toast} }
Implicit Intent
V aktivite
DetailActivity
si ukážeme použitie implicitného intentu. V pravom dolnom rohu aktivity sa nachádza textový popisok, na ktorom je názov poskytovateľa údajov o počasí. Vytvoríme implicitný intent, pomocou ktorého zabezpečíme, že po kliknutí na tento popisok dôjde k otvoreniu prehliadača s webovou stránkou poskytovateľa.Priamo môžeme otvoriť stránku s počasím pre mesto, ktoré sme dostali pomocou intentu z aktivity
SearchActivity
. Jej adresa bude v tvare (slide)http://openweathermap.org/find?q=city
Vytvoríme metódu
onProviderClicked()
, ktorá sa zavolá po kliknutí na uvedený popisok:@OnClick(R.id.textview_detail_provider) public void onProviderClicked(){ = new Intent( Intent intent .ACTION_VIEW, Intent.parse("http://openweathermap.org/find?q=" + this.city)); UristartActivity(intent); }
Launcher Activity
- Aktuálne máme k dispozícii dve aktivity - ktorá sa bude spúšťať ako prvá?
- To je zadefinované v manifeste projektu pomocou tzv. (slide) intent filtru aktivitiy.
Intent Filter
- An intent filter is an expression in an app’s manifest file that specifies the type of intents that the component would like to receive. For instance, by declaring an intent filter for an activity, you make it possible for other apps to directly start your activity with a certain kind of intent. A zasa naopak, ak žiadny intent filter pre aktivitu nezadeklarujete, potom túto aktivitu môžete spustiť len pomocou explicitného intentu.
Downloading Data
Aby aplikácia začala konečne prinášať hodnotu, potrebujeme stiahnuť živé údaje zo serveru poskytovateľa pomocou jeho REST API. Adresa pre požiadavku je v tvare (slide):
http://api.openweathermap.org/data/2.5/weather?units=metric&q=CITY&APPID=KEY
kde:CITY
predstavuje mesto, o ktorom chceme získať informácie o počasí, aKEY
predstavuje používateľský kľúč pre prístup k informáciám z tohto serveru. Kľúč je možé získať po zaregistrovaní sa na serveri služby OpenWeatherMap.
Jednoducho si môžeme otestovať toto spojenie aj pomocou príkazu
curl
z OS Linux (slide):curl "http://api.openweathermap.org/data/2.5/weather?units=metric&q=kosice&APPID=3718d7f90e7b081ca8f46aa4305c05ea" | json_reformat
Odpoveď zo servera dostaneme vo formáte JSON a našou úlohou bude získať tieto údaje v rámci našej Android aplikácie.
Class HttpURLConnection
Pre stiahnutie týchto údajov použijeme objekt triedy (slide)
HttpURLConnection
. Môžete sa však stretnúť aj s použitím Apache Http Client-a. Tento však už nie je od verzie Androidu 6.0 (API Level 23) súčasťou Android SDK a je dostupný vo forme externej knižnice.Na to, aby sme mohli v našej aplikácii pracovať so sieťou, musíme mať príslušné povolenie. Toto nastavíme v súbore
AndroidManifest.xml
nasledovne:uses-permission android:name="android.permission.INTERNET"/> <
Takže po kliknutí na tlačidlo Search dôjde k stiahnutiu údajov priamo zo služby poskytovateľa údajov o počasí:
@OnClick(R.id.button_search) public void onSearchClicked(){ String city = this.etInput.getText().toString(); try { URL url = new URL("http://api.openweathermap.org/data/2.5/weather?units=metric&APPID=3718d7f90e7b081ca8f46aa4305c05ea&q=" + city); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); .connect(); connection .i(TAG, "Connecting to " + url.toString()); Log.i(TAG, "Status: " + connection.getResponseCode()); Log = new Intent(this, DetailActivity.class); Intent intent .putExtra("city", city); intentstartActivity(intent); } catch (IOException e) { .makeText(this, "Connection error", Toast.LENGTH_SHORT).show(); Toast.printStackTrace(); e} }
Miesto výsledku však dostaneme výnimku (slide)
android.os.NetworkOnMainThreadException
.
Exception android.os.NetworkOnMainThreadException
K tejto výnimke dôjde vtedy, ak sa aplikácia snaží vykonávať sieťové operácie v hlavnom vlákne.
Po spustení aplikácie totiž systém vytvorí vlákno, v ktorom je aplikácia vykonávaná. Je označovaná ako “main”, teda hlavné vlákno. Toto vlákno zodpovedá za úlohy súvisiace s používateľským rozhraním. Ak by sme v hlavnom vlákne spustili úlohu, ktorej vykonanie trvá príliš dlho, aplikácia sa bude javiť ako nefunkčná aj napriek tomu, že vlastne len čaká na dokončenie tejto dlhotrvajúcej úlohy.
Dlhotrvajúca úloha nemusí vždy trvať dlho, ale jej dĺžka môže byť neurčitá. Medzi takéto úlohy môžeme zaradiť napríklad sieťové operácie alebo databázové operácie.
Dlhotrvajúcu úlohu si môžeme vyskúšať veľmi jednoducho - stačí, ak po stlačení tlačidla Search spustíme toto:
while(True){ .i(TAG, "Thinking about the Answer to the Ultimate Question of Life, the Universe, and Everything..."); Log}
Po kliknutí na tlačidlo sa aplikácia zahryzne a nereaguje na žiadne ďalšie podnety, resp. vstupy. Z tohto stavu nás vyseká už len Android sám tým, že sa ponúkne nereagujúcu aktivitu vypnúť.
V prípade, že potrebujeme spustiť nejakú dlhotrvajúcu operáciu, ktorá nemusí byť nutne sieťová, musíme to urobiť v samostatnom vlákne. Na možnosti, ktoré ponúka Android pre takéto úlohy sa následne pozrieme.
Threading in Android
- Čo je to vlákno? Definícia z wikipédie. (slide)
- Ilustrácia o spolubývajúcich (viaceré vlákna) z jednej izby (proces) čítajúcich jedny skriptá (zdieľaná pamäť) - v jednom čase môže ku skriptám (zdieľaným údajom) pristupovať len jeden z nich. (Stack Overflow)
- V Android-e máme niekoľko možností, ako môžeme niektorú (dlhotrvajúcu) úlohu vykonávať v samostatnom vlákne, resp. na pozadí.
- Jedno z nich je vytvoriť štandardné Javové vlákno (slide). Nie je to však odporúčaný postup, nakoľko tu vzniká niekoľko problémov, ako napr.: synchronizácia s hlavnou triedou, ak z vlákna budete chcieť vrátiť údaje späť UI.
- Android poskytuje alternatívny prístup ku práci s vláknami cez
android.os.Handler
aleboAsyncTask
. - Trieda
Handler
sa používa ako jednoduchý komunikačný kanál medzi vláknami. Ani ona nám však nepomôže, ak chceme v rámci vlákna spracovávať sieťovú komunikáciu. Tu musíme použiť trieduAsyncTask
.
Class AsyncTask
AsyncTask
enables proper and easy use of the UI thread. This class allows to perform background operations and publish results on the UI thread without having to manipulate threads and/or handlers.- Trieda
AsyncTask
je abstraktná. Ak ju chceme použiť, musíme z nej vytvoriť podtriedu (slide). Používa generiká a premenlivý počet parametrov. - Význam jednotlivých parametrov je nasledovný:
Params
- typ parametrov odoslaných úlohe pri spustení.Progress
- the type of the progress units published during the background computation.Result
- the type of the result of the background computation. Not all types are always used by an asynchronous task. To mark a type as unused, simply use the typeVoid
.
- When an asynchronous task is executed, the task goes through 4 steps:
onPreExecute()
- invoked on the UI thread before the task is executed. This step is normally used to setup the task, for instance by showing a progress bar in the user interface.doInBackground(Params...)
- invoked on the background thread immediately afteronPreExecute()
finishes executing. This step is used to perform background computation that can take a long time. The parameters of the asynchronous task are passed to this step. The result of the computation must be returned by this step and will be passed back to the last step. This step can also usepublishProgress(Progress...)
to publish one or more units of progress. These values are published on the UI thread, in theonProgressUpdate(Progress...)
step.onProgressUpdate(Progress...)
- invoked on the UI thread after a call topublishProgress(Progress...)
. The timing of the execution is undefined. This method is used to display any form of progress in the user interface while the background computation is still executing. For instance, it can be used to animate a progress bar or show logs in a text field.onPostExecute(Result)
- invoked on the UI thread after the background computation finishes. The result of the background computation is passed to this step as a parameter.
- Vzhľadom na uvedené teda vytvoríme vlastnú triedu s názvom
ForecastTask
, ktorá bude potomkom triedyAsyncTask
Class ForecastTask
Začneme identifikáciou generických typov:
- Akého typu budú vstupné údaje úlohy? Čo bude vstupom tejto úlohy? (názov mesta, takže
String
) - Akého typu bude výstup z úlohy? Čo táto úloha vráti? (z webu providera príde JSON objekt serializovaný ako reťazec, ktorý v Jave vieme reprezentovať triedou
JSONObject
. Tento údajový typ však nie je vhodný, nakoľko ho nemôžeme zabaliť do intentu. Preto bude nakoniec lepšie použiť na prenos údajový typString
) - Budeme potrebovať nejakým spôsobom reprezentovať priebeh operácie? (max. tak točiacim sa kolieskom, takže tento typ zostane prázdny (
Void
))
- Akého typu budú vstupné údaje úlohy? Čo bude vstupom tejto úlohy? (názov mesta, takže
Prázdna trieda bude vyzerať teda nasledovne:
public class ForecastTask extends AsyncTask<String, Void, String> { }
Následne vytvoríme konštruktor tridy, ktorému predáme referenciu na aktivitu, v ktorej bola inštancia objektu
AsyncTask
vytvorená:public class ForecastTask extends AsyncTask<String, Void, String> { private final SearchActivity activity; public ForecastTask(SearchActivity activity) { this.activity = activity; } }
Nakoniec vytvoríme metódu
doInBackground()
, ktorá bude spustená v samostatnom vlákne a zabezpečí stiahnutie údajov o počasí zo služby OpenWeatherMap: ```java @Override protected String doInBackground(String… cities) { this.city = cities[0];try { URL url = new URL("http://api.openweathermap.org/data/2.5/weather?units=metric&APPID=3718d7f90e7b081ca8f46aa4305c05ea&q=" + city); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.connect(); Log.i(TAG, String.format("Connecting to %s", url.toString())); Log.i(TAG, String.format("HTTP Status Code: %d", connection.getResponseCode())); if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) { return null; } BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(connection.getInputStream())); StringBuilder stringBuilder = new StringBuilder(); String line; while ((line = bufferedReader.readLine()) != null) { stringBuilder.append(line + '\n'); } Log.i(TAG, String.format("GET: %s", stringBuilder.toString())); return new stringBuilder.toString(); } catch (MalformedURLException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return null;
} ```
Processing Downloaded Data
Keď sa tieto údaje úspešne stiahnu a vytvorí sa z došlého reťazca objekt typu
JSONObject
, bude tento objekt odovzdaný metódeonPostExecute()
. Ak sa tak stalo, vytvoríme explicitný intent, pomocou ktorého necháme spustiť aktivituDetailActivity
a zabalíme do neho aj stiahnuté údaje v reťazcovej podobe s kľúčomjson
.Samozrejme je potrebné overiť aj prípad, či sme príslušný objekt naozaj dostali a nedostali sme náhodou hodnotu
null
. Táto hodnote reprezentuje prípad, kedy sa údaje nepodarilo úspešne stiahnuť.Výsledná podoba metódy
onPostExecute()
bude vyzerať nasledovne:@Override protected void onPostExecute(String json) { super.onPostExecute(json); // if no data are provided, stay with search activity if(json == null){ .makeText(this.activity, "Connection error", Toast.LENGTH_SHORT).show(); Toastreturn; } // prepare intent to start DetailActivity = new Intent(this.activity, DetailActivity.class); Intent intent .putExtra("json", json); intentthis.activity.startActivity(intent); }
Následne už stačí upraviť metódu
onSearchClicked()
, kde vytvoríte inštanciu tejto triedy a zavoláte nad ňou metóduexecute()
s potrebným parametrom (mestom):@OnClick(R.id.button_search) public void onSearchClicked(){ // get city from etInput field String city = etInput.getText().toString(); .i(TAG, "Downloading forecast for " + city); Log // execute async task = new ForecastTask(this); ForecastTask task .execute(city); task}
Progress Bar
Samozrejme môžeme v aplikácii zobraziť aj priebeh operácie pomocou točiaceho sa kolieska reprezentujúceho všeobecný stav - Pracujem. Pridáme teda metódu
onPreExecute()
:@Override protected void onPreExecute() { super.onPreExecute(); this.progress = new ProgressDialog(SearchActivity.this); this.progress.setMessage("Searching..."); this.progress.show(); }
A v metóde
onPostExecute()
zasa tento priebeh zrušíme zavolaním:java this.progress.dismiss();
Complete Source code
Celkový kód bude vyzerať nasledovne:
"src/ForecastTask.java" #include
Presentation of data
- Následne už treba iba rozbaliť prijatý intent v metóde
onCreate()
a pomocou získaných hodnôt naplniť aktivitu. Nižšie sa nachádza celkový výpis triedyDetailViewActivity
:
"src/DetailActivity.java" #include
- Ak ste postupovali správne, načítané údaje sa vám zobrazia:
Implicit Intent
Implicitný zámer môžeme vytvoriť ešte jeden - na základe získaných geo informácií (zemepisná šírka a výška) - po kliknutí na mesto sa zobrazí mapa s jeho lokáciou.
@OnClick(R.id.textview_detail_city) public void onLocationClicked(){ = new Intent( Intent intent .ACTION_VIEW, Intent.parse(String.format("geo:%f,%f?z=10", this.latitude, this.longitude))); UristartActivity(intent); }
Tu však pozor - môže sa stať, že aktivita pre zobrazenie daného zámeru neexistuje, čo by mohlo viesť k zrúteniu aplikácie. Preto tento fragment kódu obaľte do odchytenia výnimky:
@OnClick(R.id.textview_detail_city) public void onLocationClicked(){ = new Intent( Intent intent .ACTION_VIEW, Intent.parse(String.format("geo:%f,%f?z=10", this.latitude, this.longitude))); Uri try { startActivity(intent); }catch (ActivityNotFoundException e){ .makeText(this, "Geolocation viewer is not installed.", Toast.LENGTH_SHORT).show(); Toast} }
Additional Resources
- slides - prezentácia z tohto týždňa
- Intents and Intent Filters
- JSON - Homepage of JSON (JavaScript Object Notation) - the lightweight data-interchange format.
- Temps - A simple but smart weather app. (odkukaný layout pre Mr Iľka)
- OpenWeatherMap REST API - dokumentácia API služby openweathermap.org
- Threads - Android SDK dokumentácia ku vláknam
- AsyncTask -
AsyncTask
enables proper and easy use of the UI thread. This class allows you to perform background operations and publish results on the UI thread without having to manipulate threads and/or handlers.