Week 04
Service, Intent, Activity Manager, ButterKnife, SDK Version
Úvod
(slide) http://sli.do#ki-smart - anketa a otázky pre prednášajúceho
https://join.slack.com/t/ki-smart/signup - slack pre rýchlu komunikáciu
(slide) hackaton - máme konečne dátum - Hackathon sa uskutoční v posledný novembrový víkend, čo bude 25.-27.nov.2017
Torch Application - The Final Part
- Baterka síce funguje, ale aj tak má zatiaľ svoje nedostatky. Jedným z nich je, že ak sa napr. vypne obrazovka alebo aplikáciu minimalizujete, dôjde k uvoľneniu zdrojov, čo nemusí byť vždy žiadúce. A rovnako tak to nepríjemné blikanie, keď aplikáciu otočíte. Zatiaľ to vyzerá tak, že s tým, čo poznáme, sa už ďalej nepohneme. A preto si predstavíme ďalší stavebný komponent Android aplikácií. Dnes sa pozrieme na služby (slide).
Activity Lifecycle
S našou aplikáciou však máme stále jeden veľký problém - po zmene orientácie dôjde k resetnutiu stavu aktivity (spustí sa nanovo). To nie je veľmi dobré, nakoľko môžeme stratiť veľmi dôležité údaje (napr. výsledok športovej činnosti po 30min, stav rozohratej hry, …).
Problém by bolo možné vyriešiť zakázaním zmeny orientácie aplikácie. Toto je možné zabezpečiť pridaním nasledujúceho atribútu pre potrebnú aktivitu v súbore s manifestom aplikácie:
activity android:name=".MainActivity" < android:screenOrientation="portrait">
Aby sme lepšie porozumeli tomu, prečo je to vlastne tak, pozrime sa na životný cyklus aktivity (slide). Aktivita sa teda môže nachádzať v jednom z týchto stavov:
- starting - V tomto momente dochádza k spusteniu aktivity a teda jej vytvoreniu ako objektu. Volajú sa postupne metódy
onCreate()
,onStart()
aonResume()
- running - Stav, v ktorom aplikácia vykonáva svoju činnosť v tzv. UI vlákne - je viditeľná a aktívna.
- paused - Aplikácia stratila fokus, napr. je prekrytá inou nie plneobrazovkovou priesvitnou aktivitou. Aplikácia je teda čiastočne viditeľná a drží svoj stav. Pri prechode do tohto stavu volá metódu
onPause()
. - stopped - Ak je aktivita kompletne prekrytá inou aktivitou, je v tomto stave. Stále síce obsahuje svoje členské premenné, ale nie je používateľovi viditeľná.
- destroyed - Stav, kedy sa aktivita ukončuje (buď s povolením používateľa alebo automaticky). Ak sa znova zobrazí používateľovi, aktivita je vytvorená nanovo.
- starting - V tomto momente dochádza k spusteniu aktivity a teda jej vytvoreniu ako objektu. Volajú sa postupne metódy
Basically, whenever Android destroys and recreates your Activity for orientation change, it calls
onSaveInstanceState()
before destroying and callsonCreate()
after recreating. Whatever you save in the bundle inonSaveInstanceState
, you can get back from theonCreate()
parameter.Vytvoríme teda metódu
onSaveInstanceState()
a uložíme v nej stav baterky:@Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean("state", this.isOn); }
A následne upravíme aj metódu
onCreate()
, v ktorej v prípade, že bol stav aktivity uložený, tak si ho odpamätáme:@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if(savedInstanceState != null){ this.isOn = savedInstanceState.getBoolean("state"); if(this.isOn == true){ ImageView image = (ImageView) findViewById(R.id.image); Button button = (Button) findViewById(R.id.button); image.setImageResource(R.drawable.bulb_on); button.setText(R.string.turn_off); } }else{ this.isOn = false; } }
Tento fragment kódu sa nám čiastočne prekrýva s metódou
toggle()
, takže môžeme urobiť menší refaktoring:- vytvoríme súkromnú metódu
render()
, ktorá len vyrenderuje aktivitu na základe aktuálneho stavu (na základe hodnoty členskej premennejisOn
, a - aktualizujeme metódu
toggle()
, z ktorej presunieme všetok fragment kódu týkajúci sa renderovania do metódyrender()
.
- vytvoríme súkromnú metódu
Metóda
toggle()
teda bude vyzerať nasledovne:public void toggle(View view){ player.start(); this.isOn = !this.isOn; render(); }
Metóda
render()
bude po refaktoringu vyzerať nasledovne:private void render(){ Button button = (Button) findViewById(R.id.button); ImageView image = (ImageView) findViewById(R.id.image); Camera.Parameters params = this.camera.getParameters(); if(this.isOn == true) { image.setImageResource(R.drawable.bulb_on); button.setText(R.string.turn_off); params.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH); this.camera.setParameters(params); this.camera.startPreview(); }else{ image.setImageResource(R.drawable.bulb_off); button.setText(R.string.turn_on); params.setFlashMode(Camera.Parameters.FLASH_MODE_OFF); this.camera.setParameters(params); this.camera.stopPreview(); } }
Následne metóda
onCreate()
bude vyzerať takto:
Services Introduction
Služba beží na pozadí bez priameho zásahu používateľa. Služba nemá žiadne používateľské rozhranie, nie je nijako zviazaná s UI. (alebo nemusí byť?)
Služby sú však spúšťané s vyššou prioritou ako neaktívne aktivity, takže Android ich v prípade nedostatočných systémových prostriedkov neukončí.
Tu však treba ešte povedať aj to, čo služba nie je. Takže:
Android poskytuje aj niektoré systémové služby, ktoré je možné používať v rámci aplikácie, pokiaľ tá má potrebné prístupové práva. Prístup k týmto službám je možný prostredníctvom metódy
getSystemService()
. TriedaContext
poskytuje niekoľko konštánt pre prístup k týmto službám, ako napríklad:ALARM_SERVICE
- umožňuje získať prístup ku službeAlarmService
.BATTERY_SERVICE
- umožňuje získať prístup kuBatteryManager
pre správu stavu batérie.
Service Types
- V Androide máme k dispozícii dva typy služieb (slide):
- started - A service is “started” when an application component (such as an activity) starts it by calling
startService()
. Once started, a service can run in the background indefinitely, even if the component that started it is destroyed. Usually, a started service performs a single operation and does not return a result to the caller. For example, it might download or upload a file over the network. When the operation is done, the service should stop itself. - bound - A service is “bound” when an application component binds to it by calling
bindService()
. A bound service offers a client-server interface that allows components to interact with the service, send requests, get results, and even do so across processes with interprocess communication (IPC). A bound service runs only as long as another application component is bound to it. Multiple components can bind to the service at once, but when all of them unbind, the service is destroyed.
- started - A service is “started” when an application component (such as an activity) starts it by calling
Service Lifecycle
Samotná služba má však podobne ako aktivita svoj vlastný životný cyklus. Pozrime sa naň (slide).
Životný cyklus služby je výrazne jednoduchší, ako v prípade aktivity. Počas neho služba prechádza len troma metódami:
onCreate()
- význam tejto metódy je rovnaký ako v prípade aktivity - metóda sa spustí pri vytvorení služby,onStart()
- metóda sa spustí po spustení služby (zavolaní metódystartService()
), aonDestroy()
- metóda sa spustí pri ukončovaní služby (po zavolaní metódystopService()
).
Okrem uvedených metód je možné so službou komunikovať prostredníctvom metódy
onStartCommand()
. Túto metódu zavolá systém vždy, keď klient spustí službu explicitne volaním metódystartService()
.
Service Example
Vytvoríme si jednoduchú službu s názvom
ServiceExample
, na ktorej si ukážeme základy fungovania služby. Na vytvorenie použijeme Android Studio, ktoré nám s tvorbou pomôže.Pri vytvorení služby je potrebné službu opísať, resp. zaregistrovať aj v manifeste projektu, aby o nej vedel aj systém sám. To za nás vyrieši Android Studio pridaním nasledovného XML elementu:
service < android:name=".ExampleService" android:enabled="true" android:exported="true"></service>
Význam jednotlivých atribútov je nasledovný:
android:name
- názov služby, povinný atribútandroid:enabled
- ak má hodnotutrue
, služba môže byť spúšťaná, v opačnom prípade ju nebude možné vôbec spustiťandoird:exported
- ak má hodnotutrue
, službu môžu volať aj iné komponenty iných aplikácií
Na nasledujúcom fragmente kódu si pozrieme, ako služba pracuje. Jedná sa o prázdnu službu len s jej základnými metódami, ktoré budeme volať pomocou nástroja
am
:public class ExampleService extends Service { private static final String TAG = "ExampleService"; @Override public void onCreate() { super.onCreate(); .i(TAG, "Service has been created"); Log} @Override public void onDestroy() { super.onCreate(); .i(TAG, "Service has been destroyed"); Log} @Override public int onStartCommand(Intent intent, int flags, int startId) { .i(TAG, "Handling intent with message " + intent.getStringExtra("message")); Logreturn super.onStartCommand(intent, flags, startId); } @Nullable @Override public IBinder onBind(Intent intent) { .i(TAG, "onBind()"); Logreturn null; } }
Službu spustíme volaním
am startservice -n sk.tuke.smart.torch/.ExampleService
a vypneme ju volaním
am stopservice -n sk.tuke.smart.torch/.ExampleService
Pokiaľ je služba zapnutá, môžeme s ňou komunikovať posielaním intentov volaním nástroja
am
s parametromstartservice
. Tentokrát však nedôjde k opätovnému spusteniu služby, ale len k zavolaniu metódyonStartCommand()
, ktorému je odovzdaný intent:am startservice -n sk.tuke.smart.torch/.ExampleService --es message "hello world"
TorchService
Pre naše potreby budeme teda používať prvý typ služby - Started Service. Vytvoríme teda novú triedu s názvom
TorchService
, ktorá bude potomkom triedyService
reprezentujúcej typ služby Started Service.Po vytvorení bude kostra služby vyzerať nasledovne:
public class TorchService extends Service { public TorchService() { } @Override public IBinder onBind(Intent intent) { // TODO: Return the communication channel to the service. throw new UnsupportedOperationException("Not yet implemented"); } }
My teda budeme postupovať nasledovne:
- po vytvorení služby získame referenciu na kameru a otvoríme ju
- po spustení služby zapneme na kamere blesk
- pri ukončení služby blesk zasa vypneme a kameru uvoľníme
Kód riešenia bude vyzerať nasledovne:
public class TorchService extends Service { private static final String TAG = "TorchService"; private Camera camera; private Camera.Parameters params; public TorchService() { } @Override public IBinder onBind(Intent intent) { // TODO: Return the communication channel to the service. throw new UnsupportedOperationException("Not yet implemented"); } @Override public void onCreate() { super.onCreate(); this.camera = Camera.open(); this.params = this.camera.getParameters(); } @Override public void onStart(Intent intent, int startId) { super.onStart(intent, startId); this.params.setFlashMode(Camera.Parameters.FLASH_MODE_TORCH); this.camera.setParameters(this.params); this.camera.startPreview(); } @Override public void onDestroy() { super.onDestroy(); this.params.setFlashMode(Camera.Parameters.FLASH_MODE_OFF); this.camera.setParameters(this.params); this.camera.stopPreview(); this.camera.release(); } }
Spustenie služby pomocou Activity Manager-a
- Vytvorenú službu môžeme otestovať zatiaľ pomocou nástroja
am
priamo na Android zariadení:shell adb shell am startservice -n sk.tuke.smart.torch/.TorchService
- Následne ju môžeme vypnúť pomocou volania
am
s parametrom stopservice:shell adb shell am stopservice -n sk.tuke.smart.torch/.TorchService
Intent
Teraz sa však vráťme späť do samotnej aktivity, z ktorej budeme službu pri zapnutí tlačidla spúšťať a pri vypnutí zasa vypínať. Za tým účelom budeme volať metódy
startService()
astopService()
. Parametrom tejto metódy však bude objekt typuIntent
(slide).Intent v Android-e predstavuje objekt reprezentujúci správu, pomocou ktorej je možné požiadať o vykonanie príslušnej akcie od inej časti aplikácie (komponentu). Rovnako je možné pomocou intentu medzi rozličnými komponentmi komunikovať a odovzdávať si informácie.
V našom prípade použijeme intent len na vytvorenie správy, pomocou ktorej zabezpečíme spustenie služby z našej aktivity. To vykonáme aktualizovaním metódy
toggle()
:public void toggle(View view){ = MediaPlayer.create(this, R.raw.flashlight_on); MediaPlayer player .start(); player this.state = !this.state; = new Intent(this, TorchService.class); Intent intent if(this.state == true){ startService(intent); }else{ stopService(intent); } render(); }
Conclusion
Zhrňme však ešte postup, ktorý sme použili pri práci s hw súčasťami zariadenia. (slide)
Až teraz môžeme povedať, že je aplikácia hotová. Bolo by ešte možné vytvoriť niekoľko úprav (napr. zrušiť orientáciu na šírku, aby aplikácia zbytočne neblikala), ale tu sa už dá hovoriť o profite. (slide)
Refactoring with ButterKnife
Aj pre Android existuje množstvo knižníc, ktoré nejakým spôsobom rozširujú alebo uľahčujú vývoj aplikácií. Pokiaľ budete hľadať na internete, nájdete rozličné rebríčky typu (slide) “Must have libraries” alebo (slide) “X best Android libraries”] alebo (slide) “Top 5 libraries in 2015” a podobne. V rámci tohto kurzu sa s niektorými z nich tiež zoznámime. A dnes to bude rovno knižnica s názvom ButterKnife.
Butterknife is a popular View “injection” library for Android. This means that the library writes common boilerplate view code for you based on annotations to save you time and significantly reduce the lines of boilerplate code written.
ButterKnife Setup
Upozornenie
Preventvívne je dobré skontrolovať postup a verziu priamo zo stránky projektu
Je potrebné upraviť konfiguráciu Gradle pre aplikáciu (
app/build.gradle
) pridaním nasledovných riadkov do častidependencies
:'com.jakewharton:butterknife:8.8.1' compile 'com.jakewharton:butterknife-compiler:8.8.1' annotationProcessor
ButterKnife Usage
- There are three major features of ButterKnife:
- Improved View Lookups
- Improved Listener Attachments
- Improved Resource Lookups
Improved View Lookups
Pokiaľ chcete v štandardnom Android SDK získať referenciu na niektorý pohľad, použijete za týmto účelom metódu
findViewById()
. Ak teda napr. v našej baterke osamostatníme jednotlivé pohľady a vytvoríme z nich členské premenné, bude tento fragment kódu vyzerať nasledovne:public class MainActivity extends AppCompatActivity { private Button button; private ImageView imageView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); this.button = (Button)findViewById(R.id.button); this.imageView = (ImageView)findViewById(R.id.image); } ... }
Knižnica ButterKnife umožňuje anotovať členské premenné triedy pomocou
@BindView()
a identifikátora príslušného pohľadu, čím automaticky vyhľadá príslušný pohľad v rozložení aktivity. Uvedený kód je teda možné prepísať s využitím tejto knižnice nasledovne:public class MainActivity extends AppCompatActivity { @BindView(R.id.button) Button button; @BindView(R.id.image) ImageView imageView; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); .bind(this); ButterKnife} ... }
Improved Listener Attachments
Ak chceme ošetriť kliknutie na tlačidlo alebo vo všeobecnosti na pohľad, máme dve možnosti:
- buď v XML súbore definujúcom rozloženie aktivity pridáme príslušnému elementu atribút
android:onClick
, ktorého hodnotou bude názov metódy ošetrujúceho kliknutie, alebo - vytvoríme anonymnú vnútornú triedu (inner-class) v metóde
setOnClickListener()
objektu príslušného pohľadu, napríklad takto:
this.button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { toggle(null); } });
- buď v XML súbore definujúcom rozloženie aktivity pridáme príslušnému elementu atribút
Knižnica ButterKnife však ponúka riešenie pomocou anotácie metódy ošetrujúcej kliknutie
@OnClick()
, kde parametrom je identifikátor príslušného pohľadu. Je však rovnako možné príslušnej metóde pridať v rámci anotácie naraz niekoľko pohľadov, čo je aj náš prípad - metódatoggle()
bude ošetrovať kliknutie na tlačidlo aj na obrázok. Treba však samozrejme odstrániť predchádzajúcu používanú metódu.Anotovaná metóda
toggle()
bude vyzerať nasledovne:@OnClick({R.id.button, R.id.imageView}) public void toggle(View view){ ... }
Min/Target/Compile SDK Version
- Význam jednotlivých položiek je nasledovný (slide):
min sdk version - Is the earliest release of the Android SDK that your application can run on. Usually this is because of a problem with the earlier APIs, lacking functionality, or some other behavioral issue.
target sdk version - The version your application was targeted to run on. Ideally this is because of some sort of optimal run conditions. If you were to “make your app for version 19” this is where that would be specified. It may run on earlier or later releases, but this is what you were aiming for. This is mostly to indicate how current your application is for use in the marketplace, etc.
compile sdk version - The version of android your IDE (or other means of compiling I suppose) uses to make your app when you publish a .apk file. This is useful for testing your application as it is a common need to compile your app as you develop it. As this will be the version to compile to an APK, it will naturally be the version of your release. Likewise it is advisable to have this match you target sdk version.
- V našom prípade teda potrebujeme nastaviť hodnotu min sdk a target sdk na 19. V ideálnom prípade aj hodnotu verzie compile sdk. Tá však závisí od nainštalovanej verzie Android SDK build tools.