Šta je Thread i multithreading?
Thread (srp. nit) je niz uputstava u okviru programa koji se može izvršiti nezavisno od drugog koda. Ako kompjuter ima više procesora i operativni sistem koji podržava tzv. multithreading, to omogućava programerima da dizajniraju programe čiji se thread-ovi mogu izvršavati istovremeno.
Thread mehanizmi
“Looper” i “MessageQueue” su mehanizmi koje koristi Thred pri izvršavanju zadataka. “Message” je zadatak (Runnable objekt) koji treba da se izvrši na Thread-u. MessageQueue se sa jedne strane puni zadacima koji trebaju da se izvrše (message), dok sa druge strane “looper” uzima jedan po jedan zadatak sve dok ne isprazni MessageQueue.
Kod androida MainThread (glavna nit) je zauzeta bavljenjem stvarima kao što je crtanje korisničkog interfejsa (UI), odgovaranje na interakcije korisnika i generalno, po defaultu, izvršavanje (većine) koda koje pišemo. Stoga pošto andriod podržava multithreading, pametno je iskoristiti ovu mogućnost i pri programiranju radnje koje mogu privremeno da blokiraju rad UI-a izmestiti na drugi Thread. Kada izmestimo zahtevne radnje koje se sporo izvršavaju sa glavne niti, potrebno je da se vratimo sa tim podacima na nju jer samo glavna nit može da ažurira UI.
Primer pogrešnog koda koji blokira UI
Klikom na dugme počinje da se download-uje slika sa mreže (akcija može da potraje), interfejs je sve vreme zamrznut dok traje download i dok se slika ne prikaže u svom ImageView:
1 2 3 4 5 6 7 |
((Button)findViewById(R.id.Button1)).setOnClickListener( new OnClickListener() { @Override public void onClick(View v) { final Bitmap b = loadImageFromNetwork(); mImageView.setImageBitmap(b); } }); |
OBJAŠNJENJE:
“Caller Thread”: je thread koja poziva “worker Thread” za obavljanje nekog zadatka. Često je “Caller Thread” ustvari nit koji crta UI tzv. “Main Thread”, pa se tada “Worker Thread” još i zove “background Thread”.
Kreiranje novog Thread-a
Kreiranje novog Thread-a se može izvršiti na dva načina.
a) Ekstendovanjem Thread klase
Prvi način je da naša klasa ekstenduje klasu Thread i tako nasledi sve metode ove klase. Metoda koja je bitna za izvršavanje zadataka na Thread-u je run().
1 2 3 4 5 6 |
class ThreadKlasa extends Thread{ public void run(){ System.out.println("thread is running..."); // neki kod koji treba da se izvrši na sporednom thread-u } } |
Nakon kreiranja klase je dovoljno u okviru aktivnosti kreirati njenu instancu i pokrenuti metodu start():
1 2 |
ThreadKlasa noviThread = new ThreadKlasa() noviThread.start() |
b) Implementiranjem Runnable interfejsa
Za ovaj pristup je potrebno kreirati klasu koja implementira Runnale interfejs. Runnable interfejs sadrži samo jednu metodu run(). U ovu metodu se ubacuje kod koji treba da se izvrši. Objekat koji nastaje instanciranjem ovakve klase se koristi kao parametar pri kreiranju Thread-a. Jedan objekat se može koristiti za više Thread-ova.
1 2 3 4 5 6 |
class RunnableKlasa implements Runnable { public void run(){ System.out.println("thread is running..."); // neki kod koji treba da se izvrši na sporednom thread-u } } |
Da bi kreirali Thread na ovaj način potrebno je u okviru aktivnosti kreirati instancu klase Thread koja prihvata Runnable objekat kao parametar i zatim pokrenuti metodu start():
1 2 |
Thread noviThread = new Thread(new RunnableKlasa()) noviThread.start() |
NAPOMENA:
U praksi se instanca klase koja implementira “Runnable” interfejs nejčešće kreira “on the fly” bez definisanja klase:
1 2 3 4 5 |
new Thread(new Runnable() { public void run() { // neki kod } }).start(); |
Koja je razlika izmedju ova dva pristupa?
Ako proširimo klasu sa Thread, naša klasa ne može više proširiti neku drugu klasu jer Java ne podržava višestruko nasledjivanje. Ali, ako implementiramo Runnable interfejs, naša klasa se može i dalje proširivati sa drugim klasama. Medjutim treba naglasiti da proširivanjem klase interfejsom Runnable ipak ne dobijamo sve metode koje se dobijaju sa proširivanjem klase sa Thread (npr. yield(), interrupt()…). Stoga ukoliko Vam trebaju te metode interfejs nije opcija, ali ako vam je dovoljna metoda run() Runnable interfejs je preporučen način stim što vam preostaje opcija da možete proširite vašu klasu sa nekom drugom klasom.
Ažuriranje UI sa sporednog Thread-a
Kod Androida radnje koje se odvijaju na drugoj niti (background thread) ne mogu da ažuriraju UI, jer UI može da ažurira samo Main Thread. Stoga potrebno je da sporedna nit na neki način komunicira sa glavnom niti i pošalje joj zadatke u MessageQueue, nakon čijeg izvršenja bi glavna nit ažurirala UI.
a) View.post() / View.postDelayed()
Ovaj pristup koristi metodu post() i na taj način obezbedjuje da Runnable bude dodat u message queue gde čeka na red da se pokrene na glavnom threadu.
Primer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public void onClick(View v) { Thread noviThread = new Thread(new Runnable() { public void run() { final Bitmap b = loadImageFromNetwork(); mImageView.post(new Runnable() { public void run() { mImageView.setImageBitmap(b); } }); } }) noviThread.start(); } |
Ovo je najlakši način ali nije prigodan za složenije slučajeve, jer je teško održavanje i debugovanje koda. Tako da je preporuka da se za kompleksnije interakcije koristimo ili AsyncTask i Handler.
b) Handler
Handler se koristi da olakša komunikaciju izmedju “caller Thread-a” i “worker Thread-a”. Ovaj pristup je odličan upravo kada se poveća kompleksnost kao ili kada izvodimo više ponovljajućih zadataka (npr. preuzimanje više slika koje će biti prikazane u ImageView-u…).
Primer
U ovome primeru se u novom worker Thread-u u okviru run() metode izvršava izvršava dugačka radnja
final Bitmap b = loadImageFromNetwork();
a zatim po završetku Handlerova metoda post() koja ažurira UI:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
final Handler myHandler = new Handler(); Thread noviThread = new Thread(new Runnable() { @Override public void run() { final Bitmap b = loadImageFromNetwork(); myHandler.post(new Runnable() { @Override public void run() { mImageView.setImageBitmap(b); } }); } } noviThread.start(); |
Activity.runOnUiThread()
Ovo je ustvari nadogradjena verzija Handler-a. Metoda runOnUiThread() proverava da li je trenutni Thread ustvari UiThread, ako jeste odmah izvršava odredjeni kod na UI Thread-u. Tek ako trenutni Thread nije Ui Thread, onda se ponaša kao Handler koji prosledjuje zadatak u MessageQueue. Radi lakšeg shvatanja u sledećem delu je prikazan kako bi izgledao sadržaj ove metode:
1 2 3 4 5 6 7 8 9 10 11 |
final Handler mHandler = new Handler(); private Thread mUiThread; // ... public final void runOnUiThread(Runnable action) { if (Thread.currentThread() != mUiThread) { mHandler.post(action); } else { action.run(); } // ... } |
A koristi se tako što joj se prosledi Runnable objekat sa zadatkom za izvršenje.
Primer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Thread noviThread = new Thread(new Runnable() { @Override public void run() { final Bitmap b = loadImageFromNetwork(); runOnUiThread(new Runnable() { @Override public void run() { mTextView.setText(result); } }); } }); noviThread.start(); |
b) AsyncTask
AsyncTask klasa takodje omogućava da se zadaci izvršavaju na sporednom Threadu a da se njihovi rezultati publikuju na main Thread-u. Za izvršavanje asinhronih zadataka je dovoljno ekstendovati našu klasu sa AsyncTask klasom i primeniti njene metode:
- onPreExecute()
- doInBackground()
- onProgressUpdate()
- onProgressUpdate()
- onPostExecute()
Ovo je jednostavan način za koji nije potrebno znanje Thread modela, jer klasa enkapsulira kreiranje Thread-ova i korišćenje Handler-a. Problem sa AsyncTask-om je što klasa nije “svesna” životnog ciklusa (lifecycle) aktivnosti ili fragmenta, pa je na programeru da reši šta da radi sa zadacima definisam kroz AsyncTasks kada se aktivnost uništi. Stoga ovo nije baš najbolja opcija za dugačke operacije, jer ako aplikaciju uništi Android radi oslobadjanja memorije, uništiće i naš pozadinski proces.
AsyncTasks je idealan kada je CallerThread ustvari UI Thread, i akcije ne traju dugo, kao što je:
- Prikupljanje podataka sa web servisa i njihov prikaz
- Upit bazi podataka (database query)
- Čitanje i pisanje na hard disk I/O
Primer
1 2 3 4 5 6 7 8 9 10 11 12 |
private class DownloadImageTask extends AsyncTask { protected Bitmap doInBackground(String... urls) { return loadImageFromNetwork(urls[0]); } protected void onPostExecute(Bitmap result) { mImageView.setImageBitmap(result); } } public void onClick(View v) { new DownloadImageTask().execute("http://example.com/image.png"); } |
Anonimna klasa je klasa koja nema ime, stoga kada bi hteli da instanciramo objekat od ovakve klase to bi izgledalo ovako:
1 |
new { } |
Nama je potrebna specifična anonimna klasa, ona koja implementira odredjeni interfejs, stoga iako ne moramo da definišemo ime klase moramo da definišemo koji interfejs da implementiramo. Ako je interfejs definisan u svome fajlu to se postiže ubacivanjem naziva interfejsa ispred zagrada:
1 |
new NazivInterfejsa () { }; |
Pored ovoga je potrebno da implementiramo sve abstraktne metode ovog interfejsa:
1 2 3 4 5 6 |
new NazivInterfejsa () { @Override public void interfejsMetoda() { // neki kod u callback metodi } }; |
Kreiranje menija kod android aplikacija
Kreiranje sadržaja menija
Ovaj specifični meni se prikazuje u sklopu
Gde se nalaze meniji?
Za svaki meni je potrebno definisati stavke koje se nalaze u njemu. To se definiše u XML formatu u fajlu koje se nalazi u specifičnom folderu pod nazivom “menu” (nalazi se u sklopu “res” foldera). Ovaj fajl generišemo desnim klikom na folder “menu” u project sekciji i izborom New / “Menu resurse file” opcije.
1 2 3 4 |
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> // ovde ide sadržaj menija </menu> |
Atributi item elementa menija
Stavke menija se definišu kroz element <item/> koji može imati definisane mnoge atribute, najčešće korišćeni atributi su sledeći:
- id (android:id=”@+id/delete”)
- title (android:title=”@string/delete”)
- icon (android:icon=”@drawable/ic_delete”)
- enabled (android:enabled=”false”)
- checkable (android:checkable=”true”)
- showAsAction (Ovi atributi su vezani za prikazivanje elemenata “options menija”u okviru action bar-a )
- ifRoom (app:showAsAction=”ifRoom”)
- withText (app:showAsAction=”withText”)
- never (app:showAsAction=”never”)
- always (app:showAsAction=”always”)
Primer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/edit_menu_item" android:title="@string/edit" android:checkable="true" android:checked="true" android:icon="@drawable/ic_mode_edit" /> <item android:id="@+id/make_default" android:title="@string/make_default" android:checkable="true" android:checked="true"/> <item android:id="@+id/delete_menu_item" android:title="@string/delete" android:icon="@drawable/ic_delete" /> </menu> |
Grupe
Moguće je okupiti slične članove menija u grupe:
1 2 3 4 5 6 |
<group android:id="@+id/group_delete"> <item android:id="@+id/menu_archive" android:title="@string/menu_archive" /> <item android:id="@+id/menu_delete" android:title="@string/menu_delete" /> </group> |
Podmeni
Pravljenje podmenija je jednostavno, u sklopu “item” elementa treba ugraditi novi “menu” element:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
<menu xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/edit_menu_item2" android:icon="@drawable/ic_mode_edit" android:title="@string/edit" app:showAsAction="ifRoom" /> <item android:id="@+id/options_menu" android:icon="@drawable/ic_device_hub_black" android:title="Options"> <menu> <item android:id="@+id/subMenu1" android:title="Submenu1"/> <item android:id="@+id/subMenu2" android:title="Submenu2"/> </menu> </item> </menu> |
NAPOMENA:
Kod elemenata kod kojih se koristi atribut android:checkable=”true” je nakon klika na ovakav član menija potrebno da podesite stanje u polju za potvrdu, jer checkBox (ili radio dugme) ne menja automatski svoje stanje. Nakon klika moramo da upitamo kakvo je stanje stavke bile pre ovog klika (da li je bilo čekirano) koristeći isChecked().
Ako je prethodno bilo čekirano onda sada nakon klika više nije i moramo da stavimo stanje setChecked(false) i obrnuto ako nije bilo čekirano
item.isChecked()=false sada posle klika jeste i moramo da stavimo
item.setChecked(true);
1 2 3 4 5 6 7 8 9 10 11 |
switch (item.getItemId()) { case R.id.make_default: if (item.isChecked()){ item.setChecked(false); Toast.makeText(this, "Sada više nije čekirano", Toast.LENGTH_SHORT).show(); }else { item.setChecked(true); Toast.makeText(this, "Sada je čekirano", Toast.LENGTH_SHORT).show(); } return true; } |
Plivajući menu (floating context menu)
Kreiranje sadržaja menija
Prvo je potrebno definisati sadržaj menija u XML formatu.
Primer (neki_menu.xml)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content"> <item android:checkable="true" android:id="@+id/make_default" android:title="@string/make_default" /> <item android:id="@+id/save" android:title="@string/save" /> <item android:id="@+id/remove" android:title="@string/remove" /> </menu> |
U ovome primeru prvi element menija je tzv. checkbox.
Integrisanje menija u aktivnost
Nakon definisanja sadržaja je potrebno da se omogući da taj meni bude dostupan u aktivnosti.
Primer
1 2 3 4 5 6 |
@Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.neki_menu, menu); } |
Registrovanje dugmeta koje otvara meni
U sklopu aktivnosti je potrebno registrovati dugme (u onCreate() metodi) koje će biti povezano za taj meni:
1 2 3 4 |
ImageView ivOptionsMenuBtn; ivOptionsMenuBtn = findViewById(R.id.ivOptionsDots); registerForContextMenu(ivOptionsMenuBtn); |
A zatim zakačiti clickListener za njega koji će na klik da pozove metodu openContextMenu(view):
1 2 3 4 5 6 |
ivOptionsMenuBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { openContextMenu(v); } }); |
Definisanje šta će da se dogodi klikom na člana menija
U metodi onContextItemSelected() definišemo šta će da se desi klikom na neki od članova menija. Za selektovnanje člana menija se koristi “id” itema definisanog u XML-u (neki_menu.xml):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@Override public boolean onContextItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.make_default: Toast.makeText(this, "Make default", Toast.LENGTH_SHORT).show(); return true; case R.id.save: Toast.makeText(this, "Save", Toast.LENGTH_SHORT).show(); return true; case R.id.remove: Toast.makeText(this, "Remove", Toast.LENGTH_SHORT).show(); return true; default: return super.onContextItemSelected(item); } } |
Options meni
Za options meni nije potrebno da se definiše dugme koje će da reaguje na klik i otvori ovaj meni, zato što ovo dugme android sam ubacuje u action bar i dodeljuje mu ikonicu sa tri vertikalne tačke.
Treba naglasiti da ovde imamo priliku definišemo koji će član menija u biti odmah prikazan u actionBar-u pored options ikone (tri vertikalne tačke). Ovo se obezbedjuje sa atributom app:showAsAction="ifRoom". Options meni se prikazuje baš na mestu kliknutog dugmeta. Ukoliko se neki član prikazuje odmah u action baru on se onda ne nalazi u samom meniju.
Kreiranje sadržaja menija
Pored ovih specifičnosti kao i kod svih drugih menija i ovde je neophodno da se definiše sadržaj menija u XML formatu.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <item android:id="@+id/share" android:icon="@drawable/ic_device_hub_black" android:title="device" app:showAsAction="ifRoom" /> <item android:id="@+id/edit_menu_item" android:icon="@drawable/ic_mode_edit" android:title="@string/edit" /> <item android:id="@+id/make_default" android:checkable="true" android:title="Make default" /> <item android:id="@+id/delete_menu_item" android:title="@string/delete" android:icon="@drawable/ic_delete" /> <item android:id="@+id/exit" android:title="Exit" /> </menu> |
Integrisanje menija u aktivnost
Na sličan način kao kod Context Float menija se i ovde integriše meni:
1 2 3 4 5 6 |
@Override public boolean onCreateOptionsMenu(Menu menu) { MenuInflater inflater =getMenuInflater(); inflater.inflate(R.menu.options_menu, menu); return true; } |
Definisanje šta će da se dogodi klikom na člana menija
U metodi onOptionsItemSelected definišemo šta će da se desiti nakon klika na neki od članova menija:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
@Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.share: Toast.makeText(this, "Share", Toast.LENGTH_SHORT).show(); return true; case R.id.edit_menu_item: Toast.makeText(this, "Edit", Toast.LENGTH_SHORT).show(); return true; case R.id.make_default: if (item.isChecked()){ item.setChecked(false); Toast.makeText(this, "Is not default", Toast.LENGTH_SHORT).show(); }else { item.setChecked(true); Toast.makeText(this, "Is default", Toast.LENGTH_SHORT).show(); } return true; case R.id.delete_menu_item: Toast.makeText(this, "Delete", Toast.LENGTH_SHORT).show(); return true; case R.id.exit: Toast.makeText(this, "Exit", Toast.LENGTH_SHORT).show(); return true; default: return super.onOptionsItemSelected(item); } } |
PopUp meni
Ovaj meni je specifičan jer je vezan za mesto gde je kliknuto. Meni se prikazuje tamo gde ima prostora ili ispod kliknutog mesta, ili iznad kliknutog mesta. PopUp meni se sakriva sa ekrana kada korisnik klikne na neki član menija ili negde sa strane van menija.
Definisanje šta će da se dogodi klikom na člana menija
Da bi klasa (npr. aktivnost) mogla da izvrši akcije nakon klika potrebno je da klasa implementira interfejs PopupMenu.OnMenuItemClickListener.
Nakon imeplementiranja interfejsa je potrebno da se implementira (Override) i njegova abstraktna metoda onMenuItemClick(), kroz koju nam je omogućeno da definišemo za svaki član menija koja će akcija biti preduzeta nakon klika na član.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@Override public boolean onMenuItemClick(MenuItem menuItem) { switch (menuItem.getItemId()) { case R.id.edit_menu_item: Toast.makeText(this, "Edit", Toast.LENGTH_SHORT).show(); return true; case R.id.delete_menu_item: Toast.makeText(this, "Delete", Toast.LENGTH_SHORT).show(); return true; case R.id.make_default: Toast.makeText(this, "MakeDefault", Toast.LENGTH_SHORT).show(); return true; default: return false; } } |
Integrisanje menija u aktivnost
Da bi prikazali PopUp meni potrebno je da na okidaču (npr. neko dugme) definišemo onClick metodu (npr. shoPopup()) koja izgleda ovako:
1 2 3 4 5 6 7 8 9 10 |
public void showPopup (View v){ PopupMenu popup = new PopupMenu(this, v); // Dodavanje listenera na svaki član menija popup.setOnMenuItemClickListener(this); //Povezivanje sa sadrzajem menija MenuInflater inflater = popup.getMenuInflater(); inflater.inflate(R.menu.popup_menu, popup.getMenu()); // Prikazivanje menija popup.show(); } |
Contextual Action Mode meni
Ovaj specifični meni se prikazuje u sklopu actionBar-a, tačnije preko actionBar-a se. Pored članova menija prikazanih ikonama u desnom kraju, nalazi se i naslov menija u sredini, i strelica za zatvaranje menija na levoj strani.
Kreiranje sadržaja menija
Kao i za svaki meni prvo je potrebno napraviti sadržaj menija u xml formatu (New “Menu resurse file”) u folderu “menu”
Primer (context_action_menu.xml)
1 2 3 4 5 6 7 8 9 10 11 |
<?xml version="1.0" encoding="utf-8"?> <menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@+id/edit_menu_item" android:title="@string/edit" android:icon="@drawable/ic_mode_edit" /> <item android:id="@+id/delete_menu_item" android:title="@string/delete" android:icon="@drawable/ic_delete" /> </menu> |
Definisanje ActionModeCallback metode
Pre svega u sklopu aktivnosti je potrebno definisati promenjivu tipa ActionMode (napomena izabrari verziju v.7) jer se ona koristi u okviru callback metode:
1 |
private ActionMode mActionMode; |
A zatim i ActionModeCallback instancu u kojoj se vrši sav posao u vezi Contextual Action Mode menijem. Ima 4 metode, u prvoj onCreateActionMode() se povezuje predhodno definisan sadržaj menija sa Action Mode menijem. U metodi onActionItemClicked() se definišu akcije nakon klika na neki od članova. A ono šta se dešava kada se meni zatvori se definiše u metodi onDestroyActionMode().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
private ActionMode.Callback mActionModeCallback = new ActionMode.Callback() { @Override public boolean onCreateActionMode(ActionMode actionMode, Menu menu) { // Povezivanje prethodno definisanog sadržaja sa ovim tipom menija actionMode.getMenuInflater().inflate(R.menu.context_action_menu, menu); // Definisanje Title menija actionMode.setTitle("Izaberite opciju"); return true; } @Override public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) { return false; } @Override public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) { //Definisanje akcije na klik svakog člana menija switch (menuItem.getItemId()) { case R.id.edit_menu_item: // Akcija koja se odvija nakon klika na neki član menija: Toast.makeText(MainActivity.this, "Editujemo nešto", Toast.LENGTH_SHORT).show(); // Možemo da zatvorimo meni nakon klika na izbor actionMode.finish(); return true; case R.id.delete_menu_item: Toast.makeText(MainActivity.this, "Brišemo nešto", Toast.LENGTH_SHORT).show(); // Možemo da zatvorimo meni nakon klika na izbor actionMode.finish(); return true; default: return false; } } @Override public void onDestroyActionMode(ActionMode actionMode) { // Definišemo vrednost ActionMode po zatvaranju menija mActionMode = null; } }; |
Registrovanje dugmeta koje otvara meni
U sklopu aktivnosti je potrebno na odredjenom dugmetu definisati clickListener (u onCreate() metodi) sa kojim ćemo omogućiti da se na klik pozove prethodno definisana callback metoda mActionModeCallback():
1 |
mActionMode = startSupportActionMode(mActionModeCallback); |
Pa bi ceo kod za listener ovako izgledao:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
Button btnContextActionMenu = findViewById(R.id.btnContextActionMenu); btnContextActionMenu.setOnLongClickListener(new View.OnLongClickListener() { @Override public boolean onLongClick(View view) { // Ako je aktivan menu vratiti false if(mActionMode != null){ return false; } // Ako je nekativan onda ga aktiviramo mActionMode = startSupportActionMode(mActionModeCallback); return true; } }); |
Aktivnost u okviru android operativnog sistema
Šta je android aktivnost?
Android aplikacije su kolekcija zadataka, gde svaki zadatak ima svoju funkcionalnost. Te funkcionalnosti mogu se raspodeliti na četiri Androidove komponente:
Funkcionalnost | Android klasa | Primer |
---|---|---|
Interakcija sa korisnikom | Activity | Unošenje beleške, igranje računarske igre |
Pozadinski proces koji dugo traje čak i kada korisnik nije direktno u interakciji sa aplikacijom. Servis se izvršva sve dok se sam ne isključi ili ga neko drugi ekspicitno ne stopira. | Service | Reprodukovanje muzike, ažuriranje izgleda ikonice za vremensku prognozu |
Primanje poruka | BroadcastReceiver | Aktiviranje alarma pri određenom događaju |
Unošenje i učitavanje podataka | ContentProvider | Otvaranje novog telefonskog kontakta |
Aktivnost kao jedan od načina sa kojom android omogućava funkcionalnost aplikacije, ima za cilj da omogući interakciju sa korisnikom. Iz tog razloga svaka aktivnost ima korisnički interfejs. Za interaktivnost sa korisnikom aktivnost obezbedjuje prozor u kome se smešta korisnički interfejs. Ovaj prozor obično popunjava ceo ekran uredjaja, ali može biti manji ili da pliva preko drugih prozora. Najčešće jedna aktivnost implementira jedan ekran.
Da bi naša klasa predstavljala aktivnost u aplikaciji ona mora da ekstenduje klasu “Activity”. Najčešće se koristi klasa “AppCompatActivity” koja je naslednik klase Activity i pokriva nove funkcionalnosti kao što je actionBar… Ova klasa dolazi sa appcompat-v7 biblotekom.
1 2 3 |
public class NekaAktivnost extends AppCompatActivity { // sadržaj klase } |
Sama klasa “Activity” (AppCompatActivity) je podklasa klase Context, što znači da sve aktivnosti imaju pristup globalnim informacijama o okruženju aplikacije.
Ako aplikacija ima više aktivnosti, uvek je jedna aktivnost definisana kao glavna aktivnost (eng. MainActivity). Ova glavna aktivnost predstavlja prvi ekran koji se pojavljuje kada korisnik pokrene aplikaciju, nakon toga iz glavne aktivnost se može pokrenuti i druga aktivnost. Na ovaj način, aktivnost služi kao ulazna tačka za interakciju aplikacije sa korisnikom. Ta aktivnost može da pokrene drugu aktivnost, tada glavna aktivnost prelazi u stanje pauze dok je aktivna sekundarna aktivnost. Kada se završi sekundarna aktivnost, glavna aktivnost se vraća u prvi plan i nastavlja.
Osnovne metode životnog ciklusa
Od trenutka kada se aktivnost prikaže na ekranu do trenutka kada se sakrije, aktivnost prolazi veći broj faza u njenom tzv. “Životnom ciklusu” (eng.lifecycyle).
onCreate()
Ovo je prva metoda koja se pokreće po pokretanju aplikacije. Aplikacija pre izvršenja ove metode nije još nije vidljiva a tek postaje vidljiva kada se metoda onCreate() izvrši.
1 2 3 4 5 |
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Ovde ide kod } |
NAPOMENA:
U telu ove metode NIKADA ne pravite kod koji se predugo izvršava (npr. beskonačnu petlju) jer se Ui aplikacije prikazuje tek po zavšetku ove metode.
Metod onCreate() prihvata parametar savedInstanceState tipa Bundle. Bundle objekat omogućava da u jedan objekat prikupimo različite tipove podataka u vidu key/value. Ako se aktivnost pravi “od nule” onda je ovaj parametar null, medjutim ukoliko se ponovno stvara (nakon onDestroy()), onda taj parametar ima vrednost Bundle objekta upotrebljenog u metodi onSaveInstanceState().
U ovoj metodi se izvršava sve ono što je bitno pre prikazivanja aplikacije, kao što je npr. definisanje eventListener-a ili povezivanje aktivnosti sa odgovarajućim layout-om koji je definisan u XML-u:
1 |
setContentView(R.layout.mojView) |
Primer
1 2 3 4 5 6 7 |
public class NekaAktivnost extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_neka_aktivnost); } } |
onStart()
Po završetku metode onCreate(), UI aplikacije postaje vidljiv i moguća je interakcija aplikacije sa korisnikom pa se pokreće ova metoda. Ukoliko je aplikacija već bila pokrenuta pa je bila zaustavljena, ova metoda se pokreće i po završetku onRestart() metode.
onPause()
Ova metoda se poziva kada se aplikacija pauzira ali i pre nego što se aplikacija u potpunosti zaustavi. Ako se aplikacija samo pauzira nakon ove metode se poziva metoda onResume(), dok u slučaju zaustavljanja rada aplikacije po završetku ove metode se poziva metoda onStop().
onResume()
Pri prvom pokretanju aplikacije ova metoda se pokreće posle metode onStart(), a kasnije se poziva po završetku metode onPause() svaki put kada se aktivnost nastavlja.
onStop()
Ova metoda se pokreće nakon što aktivnost postaje nevidljiva. Razlog može da bude akcija koju izvrši korisnik/android tako što iz nekog razoga potisnu našu iz prvog plana, najčešće zbog neke druge aplikacija (npr. primamo poziv dok radi naša aplikacija). Aplikacija se u tom trenutku ne vidi ali i se i dalje izvršava u pozdini.
onRestart()
Ova metoda se poziva pri restartovanju aplikacije pre nego što aktivnost postane vidljiva. Ova metoda se poziva posle metode onStop(), i može se zaključiti da se ova metoda poziva svaki put kada i metoda onStart(), osim pri prvom pokretanju aplikacije.
Treba naglasiti da se ova metoda NE IZVRŠAVA kada aktivnost prvi put pokreće, kao i kada se rotira ekran (aktivnost uništava i ponovo pokreće).
onDestroy()
Ova metoda se poziva kada sistem odluči da uništi aplikaciju. Prethodi joj metoda onStop().
NAPOMENA:
U saradnji sa nabrojanim lifecycle metodama se veoma često koriste još dve metode:
- onSaveInstanceState(Bundle outState)
- onRestoreInstanceState(Bundle savedInstanceState)
Ove dve metode se koriste za čuvanje i ponovno iskorišćavanje podataka nakon prekida rada aplikacije. Kada se aplikacija zaustavlja android pokreće metodu onSaveInstanceState() pre metode onStop(). Na ovaj način andriod omogućava da se kroz nju sačuva state aplikacije.
Treba naglasiti da android ne poziva metodu onSaveInstanceState () kada korisnik eksplicitno zatvori aktivnost ili kada aplikacija pozove metodu finish().
1 2 3 4 5 6 7 8 9 |
@Override public void onSaveInstanceState(Bundle savedInstanceState) { // Ovde se čuvaju promenjive savedInstanceState.putInt(NEKA_KONSTANTA1, promenjivaČijaVrednostSeČuva1); savedInstanceState.putInt(NEKA_KONSTANTA2, promenjivaČijaVrednostSeČuva2); // Uvek mora da se pozove superclass super.onSaveInstanceState(savedInstanceState); } |
Metoda onRestoreInstanceState() se koristi da povrati sačuvane podatke, pa se izvršava posle metode onStart() a pre metode onResume().
1 2 3 4 5 6 7 8 |
public void onRestoreInstanceState(Bundle savedInstanceState) { // Uvek mora da pozove superclass super.onRestoreInstanceState(savedInstanceState); // Restore state members from saved instance promenjivaČijaVrednostSeČuva1 = savedInstanceState.getInt(NEKA_KONSTANTA1); promenjivaČijaVrednostSeČuva2 = savedInstanceState.getInt(NEKA_KONSTANTA2); } |
Metodi onCreate() se prosedjuje isti Bundle objekat pa se i ona može koristiti za povratak podataka (koristi se u ovu namenu kada je aplikacija uništena i ponovo se kreira).
Primena metoda životnog ciklusa aktivnosti
Kada aplikacija izgubi fokus ili postane nevidljiva, korisnik ne može da vrši interakciju sa aplikacijom, ali se aplikacija i dalje izvršava u pozadini, stoga je potrebno je napisati kod sa kojim bi trebali da zaustavimo sve operacije koje troše CPU, zaustavimo sve audio ili video reprodukcije kao i da sačuvamo trenutne vrednosti nekih podataka (npr. gde je stala reprodukcija videa, vrednost nekog proračuna…). Ako je igra u pitanju savet je da odmah po gubitku fokusa automatski zaustavimo igru, jer je korisnik izgubio mogućnost da kontroliše igru. Takođe, po povratku fokusa nikada ne bismo trebali da automatski da pokrenemo igru, jer bi korisnik mogao biti nepripremljen (npr. vožnja trke u trkačkoj igri), preporuka je da se pokaže ekran za pauzu i na taj način dozvolimo korisniku da sam izbere kada će da nastavi igru. Većina ove potrebne funkcionalnosti se piše u pomenutim metodama.
Slučajevi koji se mogu desiti u životu aplikacije i za koje moramo biti pripremljeni, su:
a) Gubitak i povratak fokusa
Gubitak fokusa (aplikacija je vidljiva!) se najčešće dešava kada se aktivira neki sistemski dijalog. Pre gubitka fokusa se poziva metoda onPause(). Ukoliko aplikacija povrati fokus izvršiće se metoda onResume().
b) Korisnik pauzira aplikaciju a zatim se vraća na nju
Aktivnost može biti pauzirana na više načina:
- kada korisnik pritisne Home dugme (aplikacija će se restartovati ako korisnik ponovo izabere pauziranu aplikaciju u meniju (klikom na dugme pravougaonik) gde su sve privremeno zaustavljene aplikacije)
- kada se isključi ekran (aplikacija će se restartovati kada korisnik uključi ekran)
U oba slučaja se aktivnost zaustavlja, ali se ne uništava. Metode koje su se do tada izvršile su: onPause(), onSaveInstanceState(), i onStop(). Nakon restarta se izvršava metoda onRestart() a zatim odmah i metoda onStart(), onRestoreInstanceState() i na kraju metoda onResume()
c) Operativni sistem uništava i ponovno pokreće aplikaciju
Najbolji primer za ovaj slučaj je rotiranje uredjaja. Android rotaciju posmatra kao promenu konfiguracije i sprema se da uništi aktivnost, a zatim da je ponovo startuje. Pre uništenja se poziva onStateInstanceState() koji je odličan za privremeno čuvanje podataka, da bi se kasnije iskoristili pri ponovnom pokretanju aktivnosti uz metodu onCreate() koja takodje ima Bundle objekat.
1 2 3 4 5 6 7 8 9 |
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if (savedInstanceState != null){ promenjivaČijaVrednostSeČuva = savedInstanceState.getString(KEY_KONSTANTE); // zatim nešto uradi sa tom promenjivom } } |
d) Operativni sistem uništava aplikaciju da oslobodi resurse
Ovo se dešava ako se aplikacija ne koristi dugo vremena (pauzirana je), pa je sistem nateran da je uništi kako bi oslobodio resurse zatvaranjem keširanih procesa. Po izvršenjeu metode onStop() aplikacija postaje nevidljiva, a pre samog uništenja android prvo poziva metodu onSavaInstanceState(). Uništenje aplikacije se dešava nakon metode onDestroy().
OBJAŠNJENJE:
Android “ubija” procese kada mu treba memorija prema odredjenom redosledu koji zavisi od važnosti procesa (“importance hierarchy”):
- keširan process
- servis
- vidljiv proces ali bez fokusa (visible process)
- aktivan proces (foreground process)
Treba naglasiti da u slučaju ponovnog pokretanje nove instance aplikacije, ovoj aplikaciji su dostupni svi sačuvani podaci kroz metodu onSaveInstanceState() pa kao i u prethodnom primeru možemo ih pokupiti iz Bundle objekta u metodi onCreate().
e) Korisnik uništi aplikaciju ili se aplikacija sama završi
Ovaj slučaj se može desiti:
- kada korisnik stisne sistemsko dugme back
- kada aktivnost pokreće sopstveno uništenje pozivom metode finish()
Postupak započinje sa metodama: onPause(), zatim onStop() kada aplikacija postaje nevidljiva, a zatim i metoda onDestroy(). Treba naglasiti da se u oba slučaja podaci ne čuvaju (ne poziva se metoda onSaveInstanceState()), te se aplikacija pri ponovnom pokretanju aplikacija pokreće od “nule” sa defaultnim podacima.
NAPOMENA:
Kada korisnik pređe sa jedne na drugu aktivnost pa se vrati na početnu, može doći do pokretanja više instanci iste aktivnosti na uređaju. Da bi se to izbeglo, programer treba da definiše ponašanje aktivnosti. Ovo se izvšava u okviru fajla AndroidManifest.xml. Da bi se na uređaju pokretala uvek samo jedna instanca aktivnosti, potrebno je u activity elementu activity koji sadrži (MAIN i LAUNCHER):
1 2 3 4 5 6 |
<activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> |
dodati i:
1 |
android:launchMode="singleInstance" |
da krajnji izgleda ovako:
1 2 3 4 5 6 |
<activity android:name=".MainActivity" android:launchMode="singleInstance"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> |
Razvoj mobilnih aplikacija
Uvod
Mobilna aplikacija je program koji je dizajniran da radi na mobilnom uređaju kao što je mobilni telefon, tablet ili wearable (pametni sat…). Te aplikacije imaju pristup senzorima i specifičnim funkcionalnostima uredjaja (kamera, žiroskop, vibracija i dr.). Razvijanje aplikacija za mobilne uređaje zahteva razmatranje karakteristika ovih uređaja i njihovih ograničenja (rade na bateriji i imaju slabije procesore i manje RAM memorije nego desktop računari). Programeri takođe moraju imati u vidu širok spektar: dimenzija ekrana, hardverske specifikacije i konfiguracije različitih uredjaja. Na većini mobilnih uredjaja se nalazi jedan od sledeća dva operativna sistema: Android i IOS.
Različiti pristupi izradi mobilnih aplikacija
Mobilne aplikacije se ponekad kategorizuju u zavisnosti od toga koji je pristup korišćen pri izradi aplikacije, pa stoga možemo razvoj mobilnih aplikacija podeliti na sledeće grupe:
- Izvorne (native) mobilne aplikacije:
- iOS app development
- Android app development
- Kvazi-izvorne mobilne aplikacije:
- React Native
- NativeScript
- Xamarin
- Hybridne mobilne aplikacije
- Ionic + Android.js
- Quasar + Vue.js
- Framework7 + React.js
- Responsivne web aplikacije
Izvorne (native) mobilne aplikacije
Izvorna aplikacija je softverski program koji je razvijen za upotrebu na određenoj platformi ili uređaju. Budući da je izvorna aplikacija izgrađena samo za upotrebu na određenom uređaju i njegovom operativnom sistemu, ona ima mogućnost da maksimalno iskoristi hardver i softver specifičan za uređaje tako da mogu pružiti najbolje performanse.
Native aplikacije moraju biti napisane u odgovarajućem programskom jeziku za svaki operativni sistem, stoga se za iOS aplikacije koristi Objective-C ili Swift, dok se nativne Android aplikacije pišu u Javi ili Kotlin-u.
Da bi ste razvili “nativne mobilne aplikacije” na više različitih platformi, potrebno je da u timu imate softverske inženjere za svaki operativni sistem. Pored velike (čitaj “skupe”) zavisnosti od specifičnog kadra, treba naglasiti da je otežavajuća okolnost što ne možemo da iskoristimo i podelimo programski kod između različitih verzija iste aplikacije. Zaključak je da nativne aplikacije imaju superiorne perfomanse ali i veliku cenu.
Kvazi izvorne mobilne aplikacije
Za izradu kvazi izvornih mobilnih aplikacija se koriste frejmvorci (ReactNative i NativeScript) koji se baziraju na web konceptima pri razvoju mobilnih aplikacija. ReactNative i NativeScript koriste JavaScript, dok Xamarin koristi C# pri razvoju ovih aplikacija. Tako pisan kod se naknadno kompajlira u izvorni programski jezik (u zavisnosti od platforme), a rezultat je nešto što funkcioniše kao prava izvorna aplikacija.
Ovaj način izrade mobilnih aplikacija nam omogućava da istovremeno (sa istim kodom) kreiramo aplikacije za iOS i Android. Koristeći ovaj pristup pravimo veliku uštedom u vremenu i troškovima proizvodnje uz minimalan gubitak perfomansi. Trenutno ReactNative, NativeScript i Xamarin, kažu da kroz svoje API-je imaju 100% podršku izvorne funkcionalnosti. Xamarin je u mogućnosti da pravi iOS, Android, Windows, MacOS i Linux, dok ReactNative i NativeScript su ograničeni samo na iOS i Android.
Poznato je da aplikacije pravljenje za istu namenu ipak na iOS i Android platformama izgledaju malo drugačije, jer svaka platforma prati sopstvene smernice. Ova činjenica je najveći problem kvazi izvornih aplikacija pošto se kod piše jednom a koristi za obe platforme. Postoji slučaj kada se elementi grafički podudaraju sa smernicama specifične platforme, ali se njihov položaj na ekranu ne podudara tj. položaj na ekranu je različit na različitim platformama.
Hibridne aplikacije
Radi se o web aplikacijama koje koriste klasične web tehnologije (HTML, JavaScript, i CSS) i “Cordova” kao wrapper koji im daje mogućnost za komunikaciju sa sistemskim komponentama i kompajlira ih u aplikacije dostupne kroz tzv. webView (ogoljeni browser).
Slično kao i kod kvazi izvornih aplikacija i ovde programski kod pišemo samo jednom, a ostale verzije (za svaku od željenih platformi) se generišu iz tog koda. Ovakve aplikacije se zovu “hibridne”, jer ipak nisu čisto nativne mobilne aplikacije, (sve prikazivanje vrši preko tzv. “WebView” umesto platforme korisničkog interfejsa), ali takodje nisu ni čisto Web-based (jer imaju pristup izvornim API-ima uređaja i pripremljene su za distribuciju na odgovarajućoj platformi).
Problem hibridnih aplikacija je što nemaju pristup svim sistemskim funkcijama a imaju slabije performanse od nativnih aplikacija. Ovaj problem je izražen kod starijih telefona (pogotovo na Android platformi pre verzije 4.0) kod kojih se sporo renderovuje korisnički interfejs a pri izvršavanju animaceije dolazi do seckanje. Iako je trenutno situacija bolja, uglavnom zbog značajne procesorske snage aktuelnih aparata, a delom zbog alata koje se koriste za razvoj i napretka u optimizaciji mobilnih operativnih sistema. Ne treba zanemariti da gradeći hibridne aplikacije mi automatski nasledjujemo sve probleme koje dolaze sa programiranjem na web-u (različita podržanost funkcionalnosti za različite browsere…). Više o ovome pročitajte u članku “Uvod u hibridne mobilne aplikacije”
Responsive web aplikacije
Web aplikacije se takodje mogu tretirti kao mobilne jer se izvršavaju na mobilnom uredjaju ali imaju najlošije perfomase i nemaju mnoge funkcionalnosti koje imaju aplikacije kreirane na jednom od prethodno pomenutih načina. Web aplikacije pružaju tzv. “application-like” iskustvo pri čemu se ne instaliraju lokalno već su dostupne izvršavajući aplikaciju u okviru browser-a na mobilnom uredjaju.
Progressive Web App
“Progressive Web App” se mogu smatrati naprednijom verzijom web aplikacija, jer koriste service workers koji rade u pozadini aplikacije i deluju kao posrednici između internet mreže i aplikacije, pa su u mogućnosti da presretnu mrežne zahteve u pozadini. Progresivne web aplikacije (PWA) se mogu izvršavati kada je uredjaj van mreže (koristeći service workers), i imaju izvesni hardverski pristup uređaju (npr. obaveštenja na uradjaju) koji su tradicionalno dostupni samo izvornim mobilnim aplikacijama. PWA su uvek “up-to-date” zahvaljujući “service worker update process”. PWA omogućavaju korisnicima da “sačuvaju” aplikacije koje smatraju najkorisnijim na svom početnom ekranu kreirajući prečicu bez korišćenja neke od prodavnica aplikacija (GooglePlay, App Store).