VYSOKÁ ŠKOLA POLYTECHNICKÁ JIHLAVA Katedra elektrotechniky a informatiky Obor Aplikovaná informatika
A p l i k a c e p ro O S A n d ro i d bakalářská práce
Autor: Petr Vybíral Vedoucí práce: Ing. Marek Musil Jihlava 2014
Abstrakt Práce se zabývá tvorbou jednoduché aplikace pro zařízení s operačním systémem Android. Demonstruje praktické užití programových nástrojů, využívaných při tvorbě mobilních aplikacích pod OS Android. Teoretická část nás seznamuje s vývojářským prostředím a základními prvky, které jsou nutné pro naprogramování této aplikace. Praktická část řeší vývoj a chování již naprogramované aplikace, včetně krátkých ukázek zdrojových kódů. Dále se zabývá problémy, které mohou nastat a jejich řešením. Výsledkem práce je aplikace umožňující výuku cizích jazyků.
Klíčová slova Aktivita, Android, Eclipse, Fragment, Java, mobilní aplikace
Abstract The work deals with creating a simple application for devices running on Android. It demonstrates the practical use of software tools used in creating mobile applications under Android OS. The theoretical part introduces to us the development environment and essential elements that are necessary for programming this application. The practical part deals with the development and behavior of the already programmed application, including short excerpts of source codes. It also deals with the problems that may occur and their solutions. The result is an application that allows for teaching foreign languages.
Key words Activity, Android, Eclipse, Fragment, Java, mobile application
Prohlašuji, že předložená bakalářská práce je původní a zpracoval/a jsem ji samostatně. Prohlašuji, že citace použitých pramenů je úplná, že jsem v práci neporušil/a autorská práva (ve smyslu zákona č. 121/2000 Sb., o právu autorském, o právech souvisejících s právem autorským a o změně některých zákonů, v platném znění, dále též „AZ“). Souhlasím s umístěním bakalářské práce v knihovně VŠPJ a s jejím užitím k výuce nebo k vlastní vnitřní potřebě VŠPJ. Byl/a jsem seznámen s tím, že na mou bakalářskou práci se plně vztahuje AZ, zejména § 60 (školní dílo). Beru na vědomí, že VŠPJ má právo na uzavření licenční smlouvy o užití mé bakalářské práce a prohlašuji, že s o u h l a s í m s případným užitím mé bakalářské práce (prodej, zapůjčení apod.). Jsem si vědom/a toho, že užít své bakalářské práce či poskytnout licenci k jejímu využití mohu jen se souhlasem VŠPJ, která má právo ode mne požadovat přiměřený příspěvek na úhradu nákladů, vynaložených vysokou školou na vytvoření díla (až do jejich skutečné výše), z výdělku dosaženého v souvislosti s užitím díla či poskytnutí licence. V Jihlavě dne
............................................... Podpis
Poděkování Na tomto místě bych rád poděkoval svému vedoucímu práce Ing. Marku Musilovi za poskytnutí tématu a možnost vytvářet ho pod jeho vedením.
Obsah 1
Úvod a motivace ....................................................................................................... 8
2
Analýza problému ..................................................................................................... 9
3
O systému Android ................................................................................................. 10
4
5
3.1
Historie ............................................................................................................. 10
3.2
Architektura...................................................................................................... 11
3.3
Verze systému .................................................................................................. 12
Vývojové prostředí ................................................................................................. 13 4.1
Nástroje nutné pro vývoj .................................................................................. 13
4.2
Struktura projektu............................................................................................. 14
Základní prvky využité v aplikaci .......................................................................... 16 5.1
Activity (aktivita) ............................................................................................. 16
5.2
Fragment .......................................................................................................... 19
5.3
Intent (úmysl) ................................................................................................... 21
5.4
Obsluha změn orientace displeje ...................................................................... 22
5.4.1
Využití onRetainNonConfigurationInstance ............................................ 22
5.4.2
Využití onSaveInstanceState .................................................................... 22
5.4.3
Využití fragmentů ..................................................................................... 23
6
Postup při vývoji aplikace....................................................................................... 24
7
Rozbor aplikace ...................................................................................................... 25
8
9
7.1
Návrh databáze ................................................................................................. 25
7.2
Popis uživatelského rozhraní............................................................................ 26
Tvorba základních prvků aplikace .......................................................................... 31 8.1
Adaptér a ListView .......................................................................................... 31
8.2
Vlastní dialog ................................................................................................... 33
8.3
Sdílení dat mezi aplikacemi ............................................................................. 35
8.4
Načítání dat ze souboru .................................................................................... 35
8.5
Navigační menu ............................................................................................... 36
8.6
Práce s databází ................................................................................................ 38
8.6.1
Třída DatabaseOperations......................................................................... 38
8.6.2
Třída DatabaseHelper ............................................................................... 39
8.6.3
Třída Database .......................................................................................... 40
8.7
Vyhledávání pomocí Action Baru .................................................................... 40
8.8
Rozpoznávání hlasu ......................................................................................... 41
8.9
Nastavení aplikace ........................................................................................... 43
Závěr ....................................................................................................................... 44
Seznam použité literatury ............................................................................................... 45 Seznam obrázků .............................................................................................................. 46 Seznam použitých zkratek .............................................................................................. 47 Přílohy............................................................................................................................. 48 1
Obsah přiloženého CD ............................................................................................ 48
2
Uživatelský manuál................................................................................................. 49
1 Úvod a motivace Android je open source platforma založená na jádru Linux. V současné době se jedná o nejrozšířenější platformu po celém světě s tržním podílem kolem 80% a počtem denních aktivací přesahující 1,5 milionu zařízení. Android již není pouze mobilní platforma, jak dříve bývala. Nyní se již objevuje v tabletech, noteboocích, televizorech, hodinkách nebo dokonce brýlích. Hlavním důvodem, proč jsem si zvolil jako téma bakalářské práce vývoj aplikace pro tuto platformu, je především vidina velkého potenciálu i v budoucnosti. Cílem práce je ukázka základních nástrojů využívaných při tvorbě aplikací běžících pod OS Android. Během intenzivního hledání se mi nepodařilo nalézt vhodnou publikaci popisující užití základních nástrojů takovým způsobem, jaký by mi vyhovoval. Na internetu je sice nepřeberné množství zdrojových kódů, ale žádný nesplňoval mé požadavky. Myslím si, že výhodnější pro začínající vývojáře je mít přístup k podobné aplikaci a vyzkoušet si v praxi, že určitá funkčnost je proveditelná.
8
2 Analýza problému Při hledání možných způsobů řešení mě napadly pouze 2 odlišné postupy. V prvním případě bych vytvořil aplikaci, která by neměla žádné praktické využití. Aplikace by byla rozdělena na několik částí a v každé části bych se zabýval určitou „problematikou“ vývoje Android aplikace. Jako příklad uvádím vytvoření vlastního výpisu, ukázku práce s databází a podobně. Druhý možný způsob by byl naprogramování plnohodnotné aplikace, kde bych dané „problémy“ řešil v praxi. Výsledkem by byl program zahrnující veškeré využívané komponenty. Po dlouhém rozhodování jsem zvolil druhý způsob řešení. Jelikož s tímto systémem nemám žádné vývojářské zkušenosti, beru to jako velkou výzvu. Rozhodl jsem se vytvořit aplikaci sloužící k prohlížení a zkoušení cizojazyčných slovíček. Uživatel si bude v aplikaci vytvářet lekce, do kterých si následně může nadefinovat slovíčka. Pomocí vysouvacího menu si zvolí mezi režimem prohlížení a zkoušení slovíček. Při zkoušení si bude moci zvolit směr testování. Aplikace bude navrhnuta pro verzi systému 4.0.3 a výše. Popis návrhu aplikace a plánovaný vývoj aplikace je v následujících kapitolách.
9
3 O systému Android 3.1 Historie „Historie operačního systému Android začala již v roce 2003, kdy stejnojmennou společnost založili Andy Rubin, Rich Miner, Nick Sears a Chris White. Jejich cílem bylo dle jejich slov, začít vytvářet chytřejší mobilní přístroje, které budou brát v úvahu nároky uživatelů a jejich polohu. Činnost této společnosti z počátku nevzbuzovala příliš pozornosti, pro svět byli „jen“ jedním z dalších vývojářů softwaru pro mobilní telefony.“ [4] „Zlom nastal až v roce 2005, kdy společnost Android Inc. byla koupena gigantem Google. Klíčoví lidé na svých postech zůstali i po akvizici (Andy Rubin je nyní vice prezidentem mobilní divize Googlu) a pod novým majitelem dostal vývoj rychlejší tempo. Objevily se první spekulace o tom, že Google plánuje představit svůj vlastní mobilní telefon (přezdívaný gPhone), což bylo umocněno tím, že získali mnoho patentů v oblasti mobilní komunikace.“ [4] V roce 2008 bylo oficiálně představeno první mobilní zařízení s tímto operačním systémem. Jednalo se o T-Mobile G1 (označovaný také jako HTC Dream) s verzí operačního systému Android 1.6 Donut. V roce 2010 společnost Google vydala první smartphone řady Nexus. Řada Nexus se vyznačuje hlavně čistým operačním systémem. V současné době se považuje tato řada za referenční zařízení pro tento operační systém. Nexus zařízení nejsou vyráběny přímo spoležností Google, ale vznikají ve spolupráci s jinými výrobci mobilních telefonů.
10
3.2 Architektura Architektura Android je rozdělena do pěti sekcí a čtyř hlavních vrstev.
Obrázek 1: Architektura systému Android [5]
Aplikace – Do vrstvy patří veškeré aplikace z Obchodu Play a některé předinstalované aplikace (SMS aplikace, prohlížeč, kontakty a podobně). Aplikační Framework – Třídy zprostředkovávající vývojáři přístup k velkému počtu služeb. Služby umožňují vývojáři přístup například k poloze zařízení, gyroskopu, výpisu hovorů a zpráv. Knihovny – Umožňují zařízením obsluhovat různé typy dat, jako je SQLite, SSL, WebKit a podobně. Tyto knihovny jsou napsány v programovacím jazyce C, případně C++. Android runtime – Součástí je virtuální stroj Dalvik. Dalvik se stará o běh samotných aplikací. Oproti Javě je Dalvik upravený a optimalizovaný přímo pro běh na mobilních zařízeních. Android runtime dále obsahuje knihovny tříd, které umožňují programování v Javě. V nejnovější verzi systému se ve vývojářských možnostech zařízení začíná objevovat možnost sloužící k přepnutí na virtuální stroj ART, jenž by měl Dalvik v budoucnu nahradit. ART je stále ve vývoji, tudíž není doporučován ke každodennímu používání. 11
Linux Kernel – Nejnižší vrstva tvořící abstraktní vrstvu mezi použítým hardwarem a softwarem ve vyšších vrstvách.
3.3 Verze systému Verze systémů Android jsou především známé podle jejich jmén. Jména jsou řazena v abecedním pořadí a pokaždé mají název podle nějaké laskominy. Výpis všech doposud vydaných verzí, který je umístěn níže také obsahuje u vybraných verzí nejzásadnější novinky. -
Android 1.5 Cupcake (API level 3)
-
Android 1.6 Donut (API level 4)
-
Android 2.0 – 2.1 Eclair (API level 4 – 7) o Instalace aplikací na paměťovou kartu
-
Android 2.2 – 2.2.3 Froyo (API level 8)
-
Android 2.3 – 2.3.7 Gingerbread (API level 9 – 10)
-
Android 3.0 – 3.2 Honeycomb (API level 11 – 13) o První možnost využití fragmentů o Verze určená pouze pro tablety
-
Android 4.0 – 4.0.4 Ice Cream Sandwitch (API level 14 – 15) o Spojení verze pro tablety a pro mobilní telefony o Android Beam pro sdílení pomocí NFC (Near Field Communication)
-
Android 4.1 – 4.3 Jelly Bean (API level 16 – 18) o Rozpoznávání hlasu offline o Podpora více uživatelských účtů na jednom zařízení
-
Android 4.4 – 4.4.2 KitKat (API level 19)
12
4 Vývojové prostředí Než je možné se pustit do programování své první aplikace, je zapotřebí stáhnout a nainstalovat několik programů. Aktuálně se pro vývoj nejčastěji využívá open source vývojová platforma Eclipse společně s ADT (Android Developer Tools) pluginem.
4.1 Nástroje nutné pro vývoj K vývoji je zapotřebí balík nástrojů JDK (Java SE Development Kit). JDK v sobě obsahuje JRE (Java Runtime Enviroment), vývojářské nástroje, monitorování a ladění java aplikací. V JRE jsou umístěny prvky potřebné pro chod Java aplikací. Dalším krokem je třeba stáhnout a rozbalit ADT Bundle. Jedná se o balík obsahující zbytek potřebných nástrojů, který zahrnuje vývojovou platformu Eclipse, ADT plugin a Android SDK (Software Development Kit). ADT plugin obsahuje nástroje pro vývoj a ladění Android aplikací. Android SDK zahrnuje potřebné knihovny, debugger, emulátor, ukázkové zdrojové kódy a podobně. SDK Manager slouží například k aktualizaci a stahování systémových obrazů všech vydaných verzí systému. Pomocí těchto obrazů je možno v emulátoru vytvořit vlastní emulované zařízení, na kterém je následně možné testovat vyvíjené aplikace. SDK Manager také umožňuje stahování ukázkových zdrojových kódů, případně knihovnen zajišťujících zpětnou kompatibilitu. Alternativou k Eclipse bude v budoucnosti prostřední Android Studio, které je momentálně ve stavu předběžného přístupu (early access preview). S tímto prostředním nemám žádné zkušenosti.
13
4.2 Struktura projektu Při generování nového projektu se vytvoří několik adresářů a souborů (viz Obrázek 2).
Obrázek 2: Struktura projektu
\src – Obsahuje zdrojové kódy vytvořené uživatelem. Je vhodné třídit vytvořené třídy do balíčků podle jejich funkčnosti. \gen – Obsahuje automaticky generované třídy pomocí pluginu ADT. Jako příklad uvedu třídu R, která obsahuje veškeré konstanty zajišťující snadný přístup ke zdrojům umístěným v adresáři res (ikony, obrázky, texty, layouty). \bin – Kompiler využívá tento adresář jako odkladiště souborů, které mají být následně zabaleny do apk balíčku. \res – V adresáři jsou umístěny ikony, obrázky, texty a layouty. \drawable – Zde jsou umístěny veškeré obrázky, které aplikace využívá. \layout – Obsahuje veškeré XML (eXtensible Markup Language) soubory, které určují vzhled aplikace. \values – Obsahuje soubory typu XML uchovávající veškerá data ve formátu klíč - hodnota. Klíč je využíván ve zdrojovém kódu. Pomocí klíče je zobrazena hodnota. Mezi nejpoužívanější soubory v tomto adresáři patří strings.xml, který umožňuje lokalizovat aplikaci ve více jazycích. Vytvořím-li adresář \values-cs, do kterého 14
umístím soubor strings.xml, aplikace začne využívat tento soubor v případě, že uživatel má ve svém zařízení nastaven český jazyk. AndroidManifest.xml – Soubor popisuje veškeré vlastnosti dané aplikace. Definuje aktivity, které jsou součástí aplikace. Pokud vývojář aplikace zapomene nově vytvořenou aktivitu zapsat do tohoto souboru, aplikace při pokusu o spuštění této aktivity spadne. Dále je zde umístěn seznam oprávnění, která aplikace vyžaduje (úplný přístup k síti, čtení textových zpráv, přímé volání na telefonní čísla a podobně), intent filtry, případně pro jakou verzi systému je daná aplikace určena.
15
5 Základní prvky využité v aplikaci V kapitole jsou popsány nejčastěji využívané prvky. Součástí popisu je také krátká programová ukázka popisující daný prvek.
5.1 Activity (aktivita) Obsahuje grafické rozhraní sloužící k interakci mezi aplikací a uživatelem. Většina aplikací obsahuje více než jednu aktivitu. Tyto aktivity jsou deklarovány ve vlastní třídě. Aktivita, kterou uživatel vidí po spuštění aplikace, se zpravidla nazývá MainActivity. Každá aktivita může následně spustit další aktivitu (po stisknutí tlačítka a podobně). Spuštění aktivity se provádí metodou startActivity(). Každá aktivita musí obsahovat metodu onCreate(). V této metodě jsou volány ty metody, které se mají provést při spouštění aktivity. Volá se zde i nejdůležitější metoda setContentView() sloužící k načtení vzhledu z res/layout/. Jednoduchá aktivita může vypadat například takto: public class A extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // načtení layoutu z res\layout\ setContentView(R.layout.activity_main); } .... }
Získání výsledku z aktivity Z aktivity lze také získat výsledek. Ke spuštění aktivity, ze které požadujeme výsledek, slouží metoda startActivityForResult(). K nastavení výsledku se využívá metody setResult(), pro následné zpracování onActivityResult(). Způsob získání výsledku znázorňuje Obrázek 3.
16
Obrázek 3: Princip získání výsledku z aktivity
Příklad Uvažujme, že máme aktivitu A sloužící pro výpis prvků a aktivitu B zajišťující vytváření těchto prvků prvků. Abychom docílili aktualizace výpisu prvků v aktivitě A, je zapotřebí zjistit, že byl vytvořen nový záznam v aktivitě B. Ke spuštění aktivity B využijeme metody startActivityForResult(). Tato metoda oproti standardní startActivity() v sobě obsahuje datový typ int, sloužící jako číslo požadavku (request code). Číslo požadavku se využíje po návratu z aktivity B do A ke zjištění, jaká aktivita daný výsledek vrátila. Pomocí onActivityResult() provedeme přijmutí dat. Metoda přijímá číslo požadavku, výslednou hodnotu a balík dat (Bundle). Pro nastavení výsledku využijeme již zmíněný setResult(). Výsledný kód by mohl vypadat takto: // aktivita A public class A extends Activity { private static final int code = 1; // kód, pod kterým budu přijímat data ... // spuštění aktivity Intent intent = new Intent(this, B.class); startActivityForResult(intent, code); ... // přijmutí výsledku z aktivit protected void onActivityResult(int request_code, int result_code,Intent data) { if(code == requestCode) { if(RESULT_OK == result_code) { String jmeno = data.getStringExtra("klic1"); } } } }
17
// aktivita B public class B extends Activity { ... String jmeno = Karel; Intent intent = new Intent(); intent.putExtra("klic1", jmeno); setResult(RESULT_OK, intent); finish(); // ukončení prováděné aktivity ... }
Životní cyklus Každá aktivita má svůj životní cyklus (viz Obrázek 4), který má předem dané pořadí. Až na metodu onCreate(), není nutné (pokud k tomu vývojář nemá důvod) ostatní metody implementovat.
Obrázek 4: Životní cyklus aktivity [1]
Z obrázku se dá vyčíst několik důležitých poznatků, kterých si vývojář musí být vědom. V případě, že je aktivita pozastavena nebo úplně zastavena, systém může tuto aktivitu při nedostatku volné pamětu ukončit. Je tedy vhodné data, o které nechceme přijít uložit.
18
Během mého testování jsem si všiml jedné zajímavosti, o které jsem doposud nevěděl. Při návratu pomocí softwarového tlačítka zpět z aktivity do předchozí aktivity je volána metoda onRestart(). Při kliknutí na ikonu aplikace v levém horním rohu je volána metoda onCreate().
5.2 Fragment Fragment
v
Androidu
doslova
značí
úlomek.
Poprvé
se
objevil
od verze Android 3.0 Honeycomb, kdy spoležnost Google začala reagovat na stále zvyšující se poptávku po tabletech. Hlavním cílem bylo umožnit vývojářům, co nejsnázeji vytvářet aplikace, které se dokáží přizpůsobit malým i velkým displejům. Jako výsledek této snahy vznikl fragment. Hlavní výhoda spočívá v možné změně fragmentů za chodu aplikace. Mezi nejčastější možnost využití fragmentů patří tvorba tabletového rozhraní, které se docílí kombinací několika fragmentů v jedné aktivitě. Životní cyklus „Fragment musí být vždy začleněn do aktivity. Jeho životní cyklus je tím pádem ovlivňován danou aktivitou. Například, pokud je aktivita pozastavena, veškeré fragmenty náležící dané aktivitě jsou také pozastaveny. Pokud je aktivita zničena, fragmenty jsou také zničeny. Dokud aktivita běží je možno manipulovat s každým fragmentem nezávisle. Fragmenty nemusí být pokaždé součastí layoutu. Dají se využít bez jejich vlastního UI (jako neviditelný pracovník pro aktivitu).“ [3] Životní cyklus fragmentu (viz Obrázek 5) má některé metody shodné jako životní cyklus aktivity. Hlavním rozdílem je fakt, že vzhled fragmentu se načítá v metodě onCreateView().
19
Obrázek 5: Životní cyklus fragmentu (pokud je aktivita spuštěna) [3]
20
5.3 Intent (úmysl) Intenty značí doslova úmysly. Sloužící k předávání zpráv mezi komponentami (aktivity, služby) stejné aplikace, případně z cizích aplikací. „Zpravidla bývají popsány několika atributy, kterými jsou: akce (popisuje akci, která se má vykonat), data (popisuje cestu, k datům nad kterými se má vykonat daná akce), kategorie (dodatečné informace o intentu), typ dat (MIME typ obsahu), název komponenty (konkrétní komponenta, která specifikuje danou aktivitu v explicitních indentech) a další data (data, která se přidávají intentu navíc ve formě Bundle).“ [2] Druhy intentů Explicitní intent nejčastěji slouží ke spouštění aktivit a komponent, které jsou součástí naší aplikace. Explicitní intenty jsou pokaždé doručeny cíli. Implicitní intent je využíván ke komunikaci mezi naší a cizí aplikací. Implicitní intent přímo nespecifikuje, jakou aplikací se má akce provést. Místo toho musí obsahovat dostatečné množství informací, podle kterých systém zobrazí seznam aplikací, které jsou schopné daný intent zpracovat. Na rozdíl od explicitního intentu, implicitní nemusí být doručen cíli v případě, že žádná aplikace nesplňuje požadavky daného intentu. Intent filtr představuje jednoduchý způsob, jakým aplikace dává najevo systému svojí schopnost implicitní intent splnit. Intent filtr se musí definovat v AndroidManifestu, kde se musí přiřadit aktivitě nebo službě. Důležité je správně nadefinovat typ dat, která bude schopen zpracovat. Nakonec je třeba ve zdrojovém kódu aktivity obsahující filtr vytvořit funkci pro následné zpracování. Špatně naprogramovaný intent filtr se může považovat za bezpečnostní riziko. Útočník může aplikaci poslat data, která mají vliv na stabilitu aplikace nebo jí mohou dokonce ublížit.
21
5.4 Obsluha změn orientace displeje Operační systém Android standartně zvládá rotaci displeje. Během rotace se celá aktivita vytváří znovu (opětovně se volá onCreate(), viz Obrázek 4). Z tohoto důvodu aktivita může přijít o některá data (zobrazené prvky v komponentě ListView, označené komponenty Checkbox, zvolenou pozici v navigačním menu a podobně).
5.4.1 Využití onRetainNonConfigurationInstance Reference [8] uvádí několik různých způsobů obsluhy rotace displeje. Během testování těchto způsobu jsem byl párkrát nemile překvapen. Vývojové prostředí Eclipse začalo zobrazovat u některých metod upozornění, že daný způsob je zastaralý a měl bych začít využít novější způsob. Jako
příklad
zastaralého
způsobu
bych
uvedl
využití
metody
onRetainNonConfigurationInstance() sloužící k uchování objektu během rotace displeje. Pro následné přijmutí objektu stačí v onCreate() zavolat metodu getLastNonConfigurationInstance().
5.4.2 Využití onSaveInstanceState Z
uváděných
způsobů
v
referenci
[8]
doporučuji
využití
metody
onSaveInstanceState(), která slouží k uložení stavu. Metoda se využívá nejen při rotaci displeje, ale také v případě ukončení aktivity samotným systémem (například při nedostatku volné paměti). Data se v ní ukládají do balíku (Bundle). Následně se dají znovu načíst v onCreate(). Také doporučuji vytvoření rozdílných layoutů pro orientaci na výšku a na šířku. Layout určený pro orientaci na výšku stačí umístit do složky res/layout/. Layout určený pro orientaci na šířku umístíme pod stejným názvem do složky res/layout-land/. Předání proměnné pomocí onSaveInstanceState() demonstruje následující kód.
22
// ukázka předání proměnné „hodnota“ pomocí metody onSaveInstanceState() public class MainActivity extends Activity { private int hodnota = 2; @Override protected void onCreate(Bundle savedInstanceState) { … // načtení hodnoty z savedInstanceState pomocí klíče "pokus" if(savedInstanceState != null) { hodnota = savedInstanceState.getInt("pokus"); } } @Override public void onSaveInstanceState(Bundle savedInstanceState) { // uložení hodnoty pod klíčem "pokus" savedInstanceState.putInt("pokus", hodnota); } }
5.4.3 Využití fragmentů Během rotace displeje dochází k vytvoření nového fragmentu, jenž je následně přidán do správce fragmentů (FragmentManager). Správce fragmentů společně s možností přidání tagu nově vytvářenému fragmentu je možno využít při jeho následném znovupoužití. Po rotaci displeje stačí provést kontrolu, zdali je fragment umístěn ve správci fragmentů nebo doposud není. Pokud není, je třeba ho nově vytvořit, jinak se nemusí provádět nic. Následující ukázka demonstruje jednoduchou kontrolu, zdali je již fragment umístěn ve správci fragmentů či doposud není. public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { … // najití fragmentu podle tagu "tag1", zdali již neexistuje Fragment fragment = (TestingTextFragment) getFragmentManager().findFragmentByTag("tag1"); if (fragment == null) { fragment = new TestingTextFragment(); getFragmentManager().beginTransaction().add(R.id.a_testin g_text_container, fragment, "tag1").commit(); } } }
23
6 Postup při vývoji aplikace Prvním krokem bylo navrhnutí struktury databáze. Po vytvoření struktury databáze bylo zapotřebí ještě před samotným programováním navrhnout grafický vzhled aplikace. V první fázi programování jsem si vytvořil několik aplikací. V každé jsem testoval jinou funkcionalitu (práci s databází, zobrazování prvků, předávání dat fragmentům a podobně). Po otestování jednotlivých funkcionalit jsem je začal začleňovat do jedné aplikace. Vzhledem k tomu, že aplikace má sloužit k prohlížení a zkoušení slovíček bylo zapotřebí tyto volby od sebe způsobem odlišit. K tomuto účelu se nabízelo vytvoření navigačního menu. Pro usnadnění testování databáze jsem vytvořil aktivitu zprostředkovávající zobrazení slovíček. Této aktivitě jsem po otestování správné funkčnosti databáze upravil vzhled a začal ji využívat přímo pro režim prohlížení slovíček. Doteď bylo možné vytvářet záznamy pouze programově. Pro vytváření záznamů jsem vytvořil novou aktivitu, která současně vypisovala veškerá slovíčka zvolené lekce. V pozdější fázi vývoje jsem aktivitu zprostředkovávající vytváření záznamů nahradil dialogy. Na domovské obrazovce je možno vytvářet, upravovat a odstraňovat lekce, zatímco se slovíčky se pracuje v prohlížení slovíček. Nyní přišla řada na zkoušení slovíček. Vytvořil jsem dva rozdílné způsoby zkoušení. Během prvního způsobu musí uživatel pokaždé napsat svou odpověď. Pro snadnější zadávání jsem implementoval funkci pro rozpoznávání hlasu a do nastavení aplikace přidal možnosti sloužící k úpravě způsobu kontroly odpovědí. U druhého způsobu zkoušení uživatel pouze zadává, zdali zná nebo nezná správný překlad. Po vytvoření základní funkčnosti jsem se začal zabývat různými vylepšeními aplikace, mezi které patří vyhledávání slovíček, sdílení a následné načítání lekcí. Během prvního spuštění aplikace je do databáze vloženo několik testovacích záznamů pro snadnější testování aplikace.
24
7 Rozbor aplikace V aplikaci je jako primární datový zdroj využívána databáze. Načtená data se předávají pomocí balíků aktivitám. Aktivity přijaté data předají svým fragmentům.
7.1 Návrh databáze Po vyzkoušení několika návrhů tabulek jsem se uchýlil k databázi obsahující 3 tabulky. Zvolil jsem možnost, kdy lekce může obsahovat několik primárních slov. Beru také v potaz možnost výskytu více lekcí se stejným názvem. Uživatel si je může odlišit podle různého popisu. Primární slovo může mít současně několik překladů. Z důvodu usnadnění přidávání slovíček jsem nevzal v potaz stav, kdy by překlad měl stejný význam, jako několik primárních slov. ERA model navržené databáze zobrazuje Obrázek 6.
Obrázek 6: ERA model databáze
25
7.2 Popis uživatelského rozhraní Domovská obrazovka Uživatel má možnost si volit pomocí navigačního menu mezi režimem pro prohlížení slovíček a dvěma způsoby zkoušení. Při zvolení režimu prohlížení je zobrazen seznam vytvořených lekcí. Po kliknutí na lekci z tohoto seznamu jsou zobrazena slovíčka zvolené lekce. V případě volby jednoho z režimů zkoušení je také zobrazen výpis lekcí, jen s tím rozdílem, že si uživatel může navolit několik lekcí, na které chce být zkoušen. Při dlouhém stisku na lekci je zobrazeno kontextové menu umožňující upravit, odstranit nebo sdílet zvolenou lekci. Pro snadnější orientaci mezi lekcemi, které již mají vytvořené záznamy, a které doposud žádné nemají je za názvem lekce zobrazen počet slovíček dané lekce. Domovská obrazovka také obsahuje Action Bar zobrazující tlačítka pro tvorbu nové lekce a nastavení aplikace. Prohlížení slovíček V režimu prohlížení je zobrazen seznam všech slovíček, která obsahuje uživatelem zvolená lekce. Uživatel má možnost slovíčka vyhledávat, vytvářet, upravovat, odstraňovat a řadit.
Obrázek 7: Dialog sloužící k vytváření nových slovíček
26
Vyhledávání probíhá dynamicky podle uživatelem zadaného textu. Hledaný text je porovnáván s primárním slovem a překlady. Pro vyhledání slovíček není třeba zadávat celý jeho text, ale stačí pouze jeho část. Při dlouhém stisknutí na libovolné slovíčko se zobrazí kontextový Action Bar sloužící pro případnou editaci a odstranění záznamů. Tento Action Bar je možné aktivovat i během hledání.
Obrázek 8: Prohlížení slovíček
Zkoušení slovíček formou vyplňování Uživatel je zkoušen na slovíčka ze zvolených lekcí. Následné vyhodnocení probíhá formou správně/špatně a vyhodnocené slovíčko je zobrazeno ve výpisu. Průběh zkoušení zobrazuje Obrázek 9.
27
Obrázek 9: Diagram zobrazující průběh zkoušení
Uživateli je zobrazeno slovíčko, na které musí napsat překlad do komponenty EditText. Pro zadávání správné odpovědi je možno využít funkci rozpoznávání řeči v případě podpory ze strany zařízení. Pokud zařízení tuto funkci nepodporuje, tlačítko není vůbec zobrazeno. Pro potvrzení odpovědi je třeba kliknout na odpovídající tlačíko. Případně je umožněno potvrzení pomocí tlačítka na klávesnici. Zkoušená slovíčka jsou vybírána v náhodném pořadí, přičemž uživatel má možnost navolit směr zkoušení (primární slovo -> překlad nebo obráceně). V případě volby zkoušení ve směru „primární slovo -> překlad“ je třeba zadat pouze jeden překlad. Po celou dobu zkoušení je zobrazován počet správných a chybných odpovědí, jak za současné zkoušení, tak celkový počet za všechna zkoušení v daném směru.
28
Obrázek 10: Zkoušení slovíček formou vyplňování
Algoritmus náhodného výběru prvku Pro náhodný výběr prvku ze seznamu jsem si vytvořil statickou metodu přijímající seznam slovíček, ze kterých je třeba vybrat slovíčko. Po výběru je slovíčko odstraněno ze seznamu a vráceno pomocí return. V případě přijetí seznamu neobsahujícího žádné slovíčko, metoda vrací null. Pro náhodný výběr je využita třída Random, která umožňuje vybrat náhodný prvek v rozsahu 0 až X, kde jako X volím celkový počet přijatých slovíček. Výsledný kód vypadá takto: public static PrimaryWord getRandomPrimaryWord(ArrayList
primaryWords) { Random myRandomizer = new Random(); PrimaryWord pw = null; int size = primaryWords.size(); int randomIndex = -1; // v listu je alespoň jeden záznam if (size >= 1) { // vybrání náhodného indexu, pokud je v listu více, než jeden záznam if (size > 1) { randomIndex = myRandomizer.nextInt(size - 1); } else randomIndex = 0; pw = primaryWords.get(randomIndex); primaryWords.remove(randomIndex); return pw; } return null; }
29
Zkoušení slovíček formou volby vím/nevím Zkoušení probíhá stejným způsobem, jako testování formou vyplňování. Liší se pouze v tom, že uživatel nemusí zadávat správný překlad, ale pouze zvolí, zdali ho zná nebo nezná. Pokud si není jistý, zdali zná správnou odpověď, má možnost si zobrazit správnou odpověď odpovídajícím tlačítkem.
Obrázek 11: Zkoušení slovíček formou volby vím/nevím
30
8 Tvorba základních prvků aplikace 8.1 Adaptér a ListView Velké množství aplikací obsahuje různý výpis prvků, napříč kterým je možné scrollovat. Při stisknutí na položku z tohoto seznamu bývá zobrazen podrobnější popis zvoleného prvku nebo se provádí jiná akce. Pro výpis se často využívají komponenty ListView, GridView nebo Spinner. Tyto komponenty mají nadefinovanou základní sadu adaptérů. Pokud si chce vývojář vytvořit vlastní vzhled řádků, musí si také nadefinovat vlastní adaptér. Adaptér slouží ke správě datového modelu (ArrayList, Cursor, …) a přizpůsobuje jej do jednotlivých řádků dané komponenty. Vzhled řádku si adaptér načítá z layoutu, který si vývojář vytvořil ve složce \res\layout\ pro tento účel. Daný layout reprezentuje vzhled jednoho řádku. Vlastní adaptér může dědit vlastnosti od jednoho ze základních adaptérů (Base, Array, Spinner, Cursor, …). Každý z těchto adaptérů má svá specifika. CursorAdapter se často využivá při načítání dat z databáze (databáze vrací Cursor objekt). Vrácený Cursor musí obsahovat sloupec s názvem _id. ArrayAdapter pro změnu vypisuje pouze textovou reprezentaci objektu toString(). Adaptér sloužící k zobrazení většího množství prvků má využívat recyklaci (znovupoužití) již načtených řádků (takzvaný view holder design pattern). Recyklace má za následek snížení potřeby volání metody findViewById(). Tato metoda je poměrně náročná a může mít za následek snížení rychlosti načítání. Zpomalení by se dalo pozorovat například během rychlého procházení většího počtu záznamů. Pokud se provádí změny v datovém modelu (například úprava záznamu), je zapotřebí informovat adaptér pomocí metody notifyDataSetChanged(). Níže je ukázka tvorby jednoduchého adaptéru využívajícího view holder design pattern.
31
public class TestAdapter extends BaseAdapter { private Context context; // kontext private List items; // záznamy // konstruktor načítající kontext a záznamy, které budou zobrazeny public TestAdapter(Context context, List items) { this.context = context; this.items = items; } // třída uchovávající reference na prvky daného řádku private static class ViewHolder { TextView txtLessonName; } // vrácení pohledu public View getView(int position, View convertView, ViewGroup parent){ ViewHolder holder = null; LayoutInflater mInflater = (LayoutInflater) context .getSystemService(Activity.LAYOUT_INFLATER_SERVICE); if (convertView == null) { // vytvoření řádku z xml souboru (row_lesson) a najití ID daných // prvků v něm convertView = mInflater.inflate(R.layout.row_lesson, null); holder = new ViewHolder(); holder.txtLessonName = (TextView) convertView .findViewById(R.id.lesson_name); convertView.setTag(holder); } else { // řádek již byl vytvořený dříve, takže ho stačí pouze načíst holder = (ViewHolder) convertView.getTag(); } // nastavení/změna textu v řádku Lesson lesson = (Lesson) getItem(position); holder.txtLessonName.setText(lesson.getName()); return convertView; } // vrácení prvku podle pozice @Override public Object getItem(int position) { return items.get(position); } … }
32
8.2 Vlastní dialog V aplikaci jsem využil několik vlastních dialogů. Hlavním cílem bylo vytvořit dialog, do kterého by bylo možné následně vložit layout reprezentující vzhled daného dialogu. První verzi dialogů jsem měl naprogramovanou poměrně rychle. Bohužel z důvodu nepřečtení oficiální dokumentace jsem zjistil, že se tento způsob již nepoužívá. Nyní se používají místo tohoto řešení dialog fragmenty. Mezi další problémy mé první verze dialogu také patřila neschopnost přetrvání dialogu po rotaci displeje. Po přechodu na dialog fragmenty se nabízely dva druhy dialogů. Prvním byl Alert dialog, u kterého není třeba definovat vzhled tlačítek. Tento dialog v sobě zahrnuje tři základní tlačítka (pozitivní, negativní a storno), která se dají případně vypnout. Další možnosí bylo vytvořit standardní dialog s layoutem obsahujícím i tlačítka. Po otestování a porovnávání obou dialogů jsem nakonec dal přednost Alert dialogu. Jako hlavní nevýhodu využití Alert dialogu pro mé potřeby považuji automatické zavření dialogu při kliknutí na tlačítka. Problém jsem následně vyřešil implementací onShowListeneru, ve kterém jsem nastavil listenery na tyto tlačítka a ručně si řídím, kdy se má daný dialog ukončit. Také je vhodné dát layout reprezentující vzhled dialogu do ScrollViewu. Tím se zabrání nezobrazení veškerého obsahu dialogu na menších displejích. Pro komunikaci dialogu s aktivitou/fragmentem jsem využil rozhraní, které jsem implementoval v aktivitě/fragmentu, kde daný dialog používám. V ukázkovém příkladě by bylo zapotřebí implementovat VlastniDialogListener do aktivity/fragmentu.
33
public class VlastniDialog extends DialogFragment implements DialogInterface.OnClickListener { private EditText etName = null; // EditText v dialogu // rozhraní, pomocí kterého se komunikuje s aktivitou/fragmentem public interface VlastniDialogListener { void onVlastniDialogPositiveClick(String name); } // vytváření dialogu public static VlastniDialog newInstance() { VlastniDialog frag = new VlastniDialog(); return frag; } @Override public Dialog onCreateDialog(Bundle savedInstanceState) { AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); LayoutInflater inflater = LayoutInflater.from(getActivity()); // načtení layoutu z \res\layout\ a následné nastavení View alertView = inflater.inflate(R.layout.dialog_lesson, null); builder.setView(alertView); // nastavení nadpisu, tlačítek a najití EditTextu builder.setTitle("Nadpis dialogu"); builder.setPositiveButton("Potvrdit", this); builder.setCancelable(false); builder.setNegativeButton("Zrusit", this); etName = (EditText) alertView .findViewById(R.id.custom_lesson_new_editText_name); AlertDialog dialog = builder.create(); // změna velikosti dialogu např. při zobrazení klávesnice dialog.getWindow().setSoftInputMode( WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE); // zabránění zavření dialogu při kliknutí mimo něj dialog.setCanceledOnTouchOutside(false); return dialog; } // reakce na stisk tlačítek @Override public void onClick(DialogInterface dialog, int which) { switch(which) { case AlertDialog.BUTTON_POSITIVE: VlastniDialogListener activity = null; activity = (getTargetFragment() != null) ? (VlastniDialogListener) getTargetFragment() : (VlastniDialogListener) getActivity(); String name = etName.getText().toString().trim(); // poslání dat pomocí rozhraní if (!name.equals("")) { activity.onVlastniDialogPositiveClick(name); } break; } getDialog().dismiss(); } }
34
8.3 Sdílení dat mezi aplikacemi Pro sdílení dat mezi aplikacemi jsem využil share intent sloužící k předávání dat ostatním službám. Vytvoření jednoduchého intentu sloužícího pouze pro sdílení textu bylo jednoduché, ale chtěl jsem také umožnit uživateli lekci přímo načíst ze souboru do aplikace. Soubor jsem si původně vytvořil v interním úložišti, do kterého má přístup pouze moje aplikace. Po změně oprávnění vytvořeného souboru na MODE_WORLD_READABLE získaly přístup k souboru i ostatní aplikace. Toto fungovalo pro většinu aplikací, bohužel jsem narazil na problém s aplikací Gmail, která je jedna z nejčastěji používaných e-mailových aplikací. Tato aplikace je bohužel naprogramována tak, že neumožňuje načítat soubory přímo z interního úložiště. Problém se mi podařilo obejít přidáním nového oprávnění pro mou aplikaci, které umožňuje zapisovat na externí paměťové úložiště. Pak už stačilo pouze soubor vytvořit na tomto úložišti. Další
možností
by bylo
vytvoření
vlastního
Content
Provideru
sloužícího
k zprostředkování dat ostatním aplikacím. Tento způsob řešení se dá také využít pro sdílení databáze, případně pokud bych chtěl využívat našeptávání při vyhledávání. Na rozdíl od mého zvoleného řešení má výhodu také v tom, že nepotřebuje žádné dodatečné oprávnění. Tento způsob řešení mi bohužel ne vždy fungoval, proto jsem zůstal u již dříve zmiňovaného řešení.
8.4 Načítání dat ze souboru V předchozí části jsem popsal způsob sdílení dat. Nyní by bylo zapotřebí tyto data dokázat přijmout. K přijímání dat jsem si vytvořit vlastní intent filtr v Android manifestu. Na příkladu níže je jednoduchá ukázka, jak by takový intent filtr mohl vypadat.
35
Pomocí action.SEND dávám systému vědět, že aplikace dokáže přijmout data v případě, že implicitní intent zavolá metodu Intent(Intent.ACTION_SEND). O jaký typ dat se jedná, specifikuje mimeType. V mém případě aplikace dokáže přijmout data ve formátu text/plain, což jsou textové soubory. Dalším krokem je třeba v aktivitě obsahující daný intent filtr napsat kód zajišťující kontrolu přijatých dat včetně následného zpracování těchto dat. // kontrola, jestli uživatel nenačítá data např. ze souboru Intent intent = getIntent(); if (intent != null) { // zjištění, jakým způsobem byla aplikace spuštěna String receivedAction = intent.getAction(); // aplikace byla spuštěna pomocí ACTION_SEND if (receivedAction != null && receivedAction.equals(Intent.ACTION_SEND)) { // načtení mimeType (kdyby jich v AndroidManifestu bylo více) String mimeType = intent.getType(); // byl přijat mimeType "text/plain" if (mimeType.startsWith("text/plain")) { Uri receivedUri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM); // načtení cesty k souboru a následné zpracování String filePath = receivedUri.getPath(); Shares.readFromFile(filePath, this); } } }
8.5 Navigační menu Již na začátku vývoje jsem byl plně rozhodnutý, že tuto „vychytávku“ musím do aplikace za jakoukoliv cenu zakomponovat. Velmi mě zajímalo, jak se takové navigační menu vůbec programuje. Po prostudování dokumentace jsem zjistil, že se prakticky jedná o ListView (občas obsahující pár dalších prvků). Podle zvoleného řádku v komponentě ListView se následně změní fragment zobrazující obsah plochy. Vzhled komponenty ListView lze samozřejmě změnit pomocí vlastního adaptéru. Zde jsem adaptér využil na zvýraznění zvoleného řádku. K zprovoznění této funkčnosti stačí uchovávat v adaptéru datový typ int uchovávající pozici zvoleného řádku v komponentě ListView. V metodě getView() se podle zvolené pozice rozhoduje, zdali má být písmo tučné či nikoliv.
36
public class NavigationDrawerListAdapter extends BaseAdapter { private int selectedItem = 0; // pozice prvku, která má být zvýrazněna @Override public View getView(int position, View convertView, ViewGroup parent) { ... txtTitle.setTypeface(null, position == selectedItem ? Typeface.BOLD : Typeface.NORMAL); ... } // změna zvoleného prvku a následná aktualizace adaptéru public void selectItem(int selectedItem) { this.selectedItem = selectedItem; notifyDataSetChanged(); } ... }
Během testování na několika dostupných zařízeních jsem si všiml krátkého zastavení během zavírání navigačního menu. Po chvíli jsem našel zdroj tohoto zaseknutí. Mohla za to výměna fragmentů, která patří k náročnějším operacím. Na funkčnost to sice nemělo vliv, ale i tak jsem tomu věnoval pozornost. Napadl mě způsob řešení pomocí listeneru dávajícího mi vědět, kdy se navigační menu zavřelo. Po chvíli testování jsem tento způsob řešení zavrhl. Problém spočíval v tom, že zde bylo zpoždění mezi listenerem a tím, kdy se navigační menu zavřelo vzhledově. V referenci [7] jsem nalezl poměrně jednoduché řešení, které sice nefunguje vždy, ale ve velké většině případů postačuje. Řešení spočívá v nadefinování Handleru sloužícího ke zpožděnému zavření navigačního menu. Jako ideální hodnota během testování mi přišla 180 ms. // zpoždění zavření navigačního menu o 180 ms new Handler().postDelayed(new Runnable() { @Override public void run() { mDrawerLayout.closeDrawer(mDrawerList); } }, 180);
Tabletovému rozhraní jsem se v rámci aplikace nevěnoval, jelikož jsem nebyl schopen navrhnout rozhraní, které by vzájemně ladilo a přitom plně využívalo schopností fragmentů. Alespoň jsem tedy do aplikace zakomponoval možnost stále otevřeného navigačního menu na tabletech. Jako vždy jsem zkoušel několik způsobů řešení. Prvním způsobem bylo využít přímo navigačního menu, které by při režimu na šířku zůstalo stále otevřené a nešlo zavřít. 37
Zde mi bohužel nastávaly problémy, které jsem nebyl schopen vyřešit. V případě, že jsem aplikaci spustil při orientaci tabletu na šířku, navigační menu nešlo vůbec zavírat. Naopak, pokud jsem aplikaci spustil při orientaci na výšku, tak po následné rotaci na šířku bylo navigační menu sice otevřené, ale šlo stále zavírat, ikdyž jsem to programově zakázal a prokrokoval celý program debuggerem, abych se ujistil, že to tak opravdu proběhlo. V druhém způsobu řešení jsem vyzkoušel trošku jiný způsob. U tabletového layoutu, který je umístněn v /res/layout-sw600dp-land/ jsem předělal navigační menu na klasický ListView. V programu následně porovnávám, zdali je DrawerLayout (který nepoužívám v tabletovém layoutu) různý od null. Podle toho určuji, zdali zařízení je tablet a současně má orientaci na šířku, či nikoliv. Pokud se jedná o tablet a je na šířku, použije se tabletový layout, kde už „nemám“ navigační menu. Důležité je také dát kontrolu na null u všech prvků, kde je zapotřebí, jinak aplikace samozřejmě nebude stabilní a bude docházet k pádům.
8.6 Práce s databází Pro uchovávání dat jsem využil open source SQLite databázi, jež je dostupná v každém zařízení s Android OS. Obsluhu celé databáze jsem rozdělil do tří tříd.
8.6.1 Třída DatabaseOperations V této třídě se provádí veškeré operace s databází. Pro vkládání záznamů do více tabulek využívám transakce. Tím docílím jistoty, že se vloží veškeré záznamy. Transakce mají obrovskou výhodu v tom, že se provedou všechny nadefinované dotazy (v mém případě vložení záznamů do tabulky) nebo se v případě chyby neprovede nic. Transakcí využívám při vytváření nových slovíček, kdy současně vkládám primární slovo a jeho překlady, jež jsou umístěny v jiné tabulce. Při odstraňování záznamů využívám mazání v kaskádě. Jedním dotazem tak docílím smazání veškerých na sebe navazujících záznamů. Tím pádem není třeba postupně mazat lekci, všechna slovíčka a překlady této lekce. Než se mi podařilo zprovoznit mazání v kaskádě, dalo mi to chvíli zabrat. S pomocí reference [6] jsem zjistil, že SQLite má standardně vypnutou podporu pro cizí klíče. Podpora se musí po každém
38
připojení k databázi zapnout. K tomu stačí po otevření databázového spojení provést jednoduchý dotaz. // zapnutí cizích klíčů if (!database.isReadOnly()) { database.execSQL("PRAGMA foreign_keys = ON;"); }
V případě vývoje aplikace pro Android 4.1 a výše, stačí využít metody setForeignKeyConstraintsEnabled(true), která je schopna zapnout podporu cizích klíčů. Pro úpravu údajů nic zvláštního nevyužívám. Při úpravě slovíček mi přišlo nejjednodušší dané slovíčko odstranit a následně celé znovu vložit do databáze. V případě využití úpravy záznamu je podle mého názoru poměrně zbytečné a komplikované kontrolovat u primárního slova a jeho překladů, co se změnilo, případně zdali nějaký překlad nepřibyl.
8.6.2 Třída DatabaseHelper Tato třída je potomkem třídy SQLiteOpenHelper. Má na starost vytváření databáze, přidávání tabulek, případně aktualizaci databáze. Uchovávám zde názvy sloupců ve formě veřejných statických proměnných, ke kterým mohu přistupovat v rámci celé aplikace. Konstruktor třídy slouží k vytvoření databáze. Metoda super() volá svého rodiče, předá mu kontext aplikace, jméno a verzi databáze. Verze databáze se musí změnit pokaždé, upravuji-li nějakou tabulku. Pokud vývojář verzi nezmění, dojde k problému u veškerých zařízení, která již mají uloženou starší verzi databáze. Aplikace by využívala starší verzi databáze a přitom metody, které s ní pracují, by byly navrženy pro novou strukturu databáze. S tím také souvisí (v lepším případě) částečná nefunkčnost aplikace a zvýšené riziko pádů aplikace. Po změně verze databáze dojde k zavolání metody onUpgrade(), kterou popisuji dále. private static final String DATABASE_NAME = "Database.db"; private static final int DATABASE_VERSION = 2; // konstruktor předávající kontext, jméno a verzi databáze public DatabaseHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); }
39
Metoda onCreate() je zavolána pouze tehdy, pokud databáze ještě nebyla vytvořena. Tím je toto místo ideální pro tvorbu tabulek, které chci mít umístěné v databázi. Veškeré tabulky by měly mít identifikátor pojmenován jako _id. Tohoto následně mohou využívat některé specifické komponenty (například CursorAdapter). V aplikaci jsem toho nevyužil, jelikož data načítám formou spojení více tabulek. Tyto data následně parsuji podle názvů sloupců a převádím do datových tříd uchovávajících načtené záznamy. Během parsování by v mém případě nastal problém, jelikož by více sloupců mělo stejný název. Pokud bych nenačítal všechna id, nastaly by problémy pří úpravě a mazání záznamů, jelikož bych tyto záznamy nemohl přesně určit. Metoda onUpgrade() je automaticky zavolána při změně verze databáze. Zde je umožněno porovnávání čísla databáze nové a staré verze. Podle toho je možno si data ze staré verze načíst a následně je vložit do nové verze, případně pouze odstranit veškerá data, pokud nejsou důležitá. Záleží pouze na vývojáři, čemu dá přednost. Po provedení operací je třeba zavolat onCreate() kvůli vytvoření tabulek s novou strukturou.
8.6.3 Třída Database Třídu využívám pro spojení obou dříve jmenovaných tříd. Také slouží k otevírání a zavírání databázového spojení. V neposlední řadě v metodě open(), jež otevírá databázové spojení, také zapínám podporu pro cizí klíče.
8.7 Vyhledávání pomocí Action Baru Jelikož jsem jako ukázkovou aplikaci zvolil aplikaci podporující prohlížení slovíček, bylo by více než vhodné umožnit uživateli slovíčka vyhledávat. K tomu jsem využil SearchView widget. Často se po kliknutí na ikonu hledání v Action baru, zadání hledaného textu a následného potvrzení spustí nová aktivita zobrazující výsledky hledání. V mém případě jsem na to šel trošku jinak. SearchView widget jsem využil jako dynamické vyhledávání v původní aktivitě. Nejdříve bylo potřeba implementovat vhodný listener zprostředkovávající opětovné hledání při jakékoliv změně textu. Dále jsem musel zajistit zobrazení původních dat při ukončení hledání. K tomu jsem chtěl využít onCloseListener(), který by se zavolal při ukončení hledání. K mému překvapení jsem
40
zjistil, že minimálně na Androidu 4+ nefunguje a nezavolá se. Naštěstí se tato nefunkčnost dala obejít využitím onActionExpandListener(). private SearchView mSearchView; // načtení Action Baru @Override public boolean onCreateOptionsMenu(Menu menu) { // načtení menu z \res\menu\wods_menu , který obsahuje prvek pro vyhledávání getMenuInflater().inflate(R.menu.words_menu, menu); // najití vyhledávacího prvku v Action Baru MenuItem searchItem = menu.findItem(R.id.action_words_search); mSearchView = (SearchView) MenuItemCompat.getActionView(searchItem); // listener pro kontrolu změny textu mSearchView.setOnQueryTextListener(new OnQueryTextListener() { ... }); // nastavení onActionExpandListeneru místo onCloseListeneru searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { ... }); return true; }
8.8 Rozpoznávání hlasu Android aplikace velmi často umožňují pro snažší zadávání textu využívat rozpoznávání hlasu, jenž je následně převeden na text. Rozpoznávání hlasu jsem využil při testování slovíček formou vyplňování. Implementace je poměrně jednoduchá. Stačí pouze spustit vhodný intent obstarávající zpracování hlasu. Výsledný text se přijme v metodě onActivityResult(). Jako správný programátor nesmím zapomenout na možnost, kdy by uživatelovo zařízení nepodporovalo rozpoznávání hlasu. V tomto případě je třeba tlačítko spouštějící rozpoznávání hlasu vypnout a skrýt. Skrytím tlačítka, zde myslím úplné odstranění tlačítka z layoutu a ne pouze zneviditelnění. Pro otestování, zda aplikace je schopná zpracovávat rozpoznávání hlasu jsem využil metody queryIntentActivities() vracející veškeré aktivity, které jsou schopné zpracovat daný intent. Pokud bude počet vrácených aktivit roven nule, zneviditelní se tlačítko pro zpracování hlasu, jinak se nastaví listener na stisk tlačítka. Toto otestování je vhodné provést v metodě onCreate(), případně nějaké obdobné.
41
Ukázkový příklad je zde: // Tlačítko pro rozpoznávání hlasu a následné vyzkoušení, zdali rozpoznávání // hlasu zařízení umožňuje. ImageButton speakButton = (ImageButton) getView().findViewById(R.id.a_testing_text_speechToText); List activities = getActivity().getPackageManager().queryIntentActivities(new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH), 0); // pokud se vrátí 0, funkce pro rozpoznávání hlasu není dostupná if (activities.size() == 0) { speakButton.setEnabled(false); speakButton.setVisibility(View.GONE); } else { speakButton.setOnClickListener(this); }
Po otestování přítomnosti rozpoznávání hlasu je třeba naprogramovat spuštění samotného intentu zprostředkovávajícího rozpoznání hlasu. Zde
je
velmi
důležitá
metoda
putExtra()
obsahující
jako
jméno
RecognizerIntent.EXTRA_LANGUAGE_MODEL, jehož hodnotou řekneme, jaký jazyk chceme zpracovat, případně zdali se hlas má zpracovat přesně tak, jak jde slyšet. // Spuštění intentu pro rozpoznávání hlasu. Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,RecognizerIntent.LANGUA GE_MODEL_FREE_FORM); startActivityForResult(intent, 1);
Nakonec stačí výsledný text přijmout v již zmiňované metodě onActivityResult() a libovolně zpracovat. @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case 1: if (resultCode == Activity.RESULT_OK) { ArrayList<String> found = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS); String receivedText = found.get(0); } break; default: break; } super.onActivityResult(requestCode, resultCode, data); }
42
8.9 Nastavení aplikace Nastavení aplikace považuji za takový speciální případ aktivity. Pokud vývojář chce nabídnout uživateli nějaké možnosti nastavení aplikace, využije pomocnou třídu PreferenceFragment. Na rozdíl od ostatních aktivit a fragmentů, kde jejich vzhled bývá umístěn v \res\layout\, je vzhled nastavení umístěn v \res\xml\. Vzhled nastavení aplikace je zpravidla konzistentní napříč různými aplikacemi. Je sice možnost si nastylovat vlastní vzhled, ale dělá se to poměrně obtížně. Přídávání prvků do nastavení je naprosto jednoduché. Stačí do souboru reprezentujícího vzhled přidat prvky, které bude moci uživatel měnit (CheckBoxPreference, ListPreference, EditTextPreference, …) a případně je rozkategorizovat podle typu použití. Těmto prvkům se také musí přiřadit klíč, podle kterého bude moci vývojář nastavené hodnoty načíst a využívat v aplikaci. Klíče slouží jako spojení mezi položkamy v SharedPreferences a zdrojovým kódem aplikace. Pro jednoduchý přístup a změnu hodnot nastavení mimo PreferenceActivity jsem si vytvořil třídu Preferences, kde mám vytvořeny veřejné statické metody get() a set() sloužící k nastavení a vrácení prvků z nastavení aplikace. Díky tomuto způsobu řešení mohu jednoduše přistupovat k uloženým hodnotám odkudkoliv a současně si nemusím neustále pamatovat klíč ke každé hodnotě. // vrácení hodnoty (pokud uživatel nic nenastavil, vrátí se 0) public static int getCountFromPreference(Context context) { return getPreferences(context).getInt("key1", 0); } // změna hodnoty public static void setCountFromPreference(Context context, int number) { getPreferences(context).edit().putInt("key1", number).apply(); } private static SharedPreferences getPreferences(Context context) { return context.getSharedPreferences("MojePref", Context.MODE_PRIVATE); }
43
9 Závěr Práce na této aplikaci mi dala obrovské množství zkušeností v oboru, kterému jsem se dosud nikdy nevěnoval. Při vytváření aplikace mi nejednou pomohlo každodenní zálohování. Při větších změnách napříč celou aplikací, které ne vždy dopadly nejlépe, jsem se mohl vrátit k starší a v té době funkčnější verzi. Výsledná aplikace slouží k prohlížení a zkoušení cizojazyčných slovíček. Uživatel si může v aplikaci vytvářet lekce, do kterých si následně může nadefinovat slovíčka. Lekce je možno sdílet a následně opětovně načíst do aplikace. Velkou pomocí pro mě byl emulátor usnadňující testování aplikace bez nutnosti vlastnit naemulované zařízení. V pozdější fázi vývoje jsem začal zjišťovat, že ne vše jde vyzkoušet na emulátoru. Tím, že si výrobci zařízení přidávají různé grafické nadstavby a podobně, dochází k problémům, jenž by jinak nemusely nastávat. Nejhorší na tomto je, že vývojář tyto problémy není schopen nijak naemulovat bez přístupu k fyzickému zařízení. Jak jsem již na začátku zmiňoval, jakožto začátečníka pro mě byla tvorba Android aplikace opravdu výzva. Několikrát se mi během vývoje stalo, že se mi podařila zprovoznit nějaká funkce, kterou jsem chtěl mít obsaženou v aplikaci a postupem času jsem zjistil, že se tento způsob řešení přestal využívat a nahradil ho nějaký jiný. Na této práci se mi podařilo demonstrovat využití aktivit, fragmentů, dialogů, adaptérů, nastavení aplikace, komponenty Action Bar se zakomponovaným vyhledáváním a intentů. Nejvíce mě mrzí, že jsem z časových důvodů nestihl zakomponovat práci s více vlákny. Chvíli jsem měl v projektu zpracování ve více vláknech obsažené, ale nebylo to ve stavu, který bych mohl nazvat plně funkční. Mezi další možnosti rozšíření bych viděl vytvoření tabletového rozhraní a ukládání hodnocení ze zkoušení do databáze. Tyto výsledky by se následně daly využít pro zobrazení různých grafů, procentuální úspěšnosti a podobně. V neposlední řadě také již zmíněné zpracování ve více vláknech.
44
Seznam použité literatury [1]
Activity. Android
Developers [online].
25
Apr
2014
[cit.
2014-04-30].
Dostupné
z:http://developer.android.com/reference/android/app/Activity.html [2] Aktivity a intenty. AndroidWiKi [online]. 9. 11. 2012 [cit. 2014-01-20]. Dostupné z:http://www.androidwiki.cz/Aktivity_a_intenty
[3] Fragments. Android Developers [online]. 20. 12. 2013 [cit. 2014-01-11]. Dostupné z:http://developer.android.com/guide/components/fragments.html
[4] Mobilní operační systém: jak to všechno začalo. Diit.cz [online]. 27. 07. 2011 [cit. 2013-1220]. Dostupné z:http://diit.cz/clanek/mobilni-operacni-system-android
[5] Android Architecture. Tutorialspoint [online]. © 2014 [cit. 2014-03-06]. Dostupné z:http://www.tutorialspoint.com/android/android_architecture.htm [6] SQLite Delete Cascade not working. Stack Overflow [online]. 2012-11-30 [cit. 2014-05-10]. Dostupné z:http://stackoverflow.com/questions/13641250/sqlite-delete-cascade-not-working [7] Optimizing drawer and activity launching speed. Stack Overflow [online]. 2013-8-27 [cit. 2014-05-10]. Dostupné z:http://stackoverflow.com/questions/18343018/optimizing-drawer-andactivity-launching-speed [8] GRANT, Allen a Jakub MUŽÍK. Android 4: Průvodce programováním mobilních aplikací. 1. vyd. Brno: Computer Press, 2013. ISBN 978-80-251-3782-6.
45
Seznam obrázků Obrázek 1: Architektura systému Android ..................................................................... 11 Obrázek 2: Struktura projektu......................................................................................... 14 Obrázek 3: Princip získání výsledku z aktivity .............................................................. 17 Obrázek 4: Životní cyklus aktivity ................................................................................. 18 Obrázek 5: Životní cyklus fragmentu (pokud je aktivita spuštěna) ................................ 20 Obrázek 6: ERA model databáze .................................................................................... 25 Obrázek 7: Dialog sloužící k vytváření nových slovíček ............................................... 26 Obrázek 8: Prohlížení slovíček ....................................................................................... 27 Obrázek 9: Diagram zobrazující průběh zkoušení .......................................................... 28 Obrázek 10: Zkoušení slovíček formou vyplňování ....................................................... 29 Obrázek 11: Zkoušení slovíček formou volby vím/nevím ............................................. 30
46
Seznam použitých zkratek ADT Android Developer Tools JDK
Java SE Development Kit
JRE
Java Runtime Enviroment
NFC
Near Field Communication
OS
operační systém
SDK Software Development Kit SQL
Structured Query Language
XML eXtensible Markup Language
47
Přílohy 1 Obsah přiloženého CD Na přiloženém CD se v kořenovém adresáři nachází tato bakalářská práce ve formátu bakalarska_prace.pdf s jednoduchým návodem navod.txt pro spuštění a obsluhu programu. V adresáři Projekt se nachází kompletní projekt se všemi zdrojovými kódy. V adresáři Apk je umístěn instalační soubor. Adresář Dokumentace obsahuje vygenerovanou dokumentaci Javadoc.
48
2 Uživatelský manuál Manuál slouží ke stručnému popisu uživatelského rozhraní společně s jeho ovládáním. Aplikace je rozdělena do několika sekcí. Domovská obrazovka Na obrazovce jsou zobrazeny veškeré uživatelem vytvořené lekce. Uživatel může pomocí vysouvacího menu volit mezi režimem
prohlížení
slovíček,
zkoušení
formou
doplňování odpovědi (zkoušení textem) a zkoušení formou odpovědi vím/nevím. Symbol + umístěný v pravém horním rohu slouží k vytváření nové lekce. Pro vytvoření nové lekce je třeba zadat minimálně jméno lekce. Při dlouhém stisku na jakoukoliv lekci se zobrazí Obrázek 1: Domovská obrazovka
kontextové menu umožňující úpravu, odstranění a sdílení zvolené lekce.
Dále je zde umístěna volba pro otevření nastavení aplikace.
49
Prohlížení slovíček Jsou zde zobrazena veškerá slovíčka dané lekce. Slovíčka jsou abecedně seřazena a dají se řadit sestupně nebo vzestupně. Symbol + v tomto případě slouží k vytvoření nových slovíček. Pro vytvoření nového slovíčka je třeba zadat minimálně primární slovo a alespoň jeden překlad. Slovíčka se dají také vyhledávat. Při vyhledávání se porovnává
hledaný
text
s primárním
slovem
a veškerými překlady. Pokud se nějaká část slovíčka shoduje s hledaným textem, je slovíčko včetně všech překladů zobrazeno. Obrázek 1: Prohlížení slovíček
Při dlouhém stisku na libovolné slovíčko je možno
upravit a odstranit označené slovíčko. Zkoušení formou doplňování odpovědi (zkoušení textem) Zkoušení probíhá formou napsání odpovědi a probíhá, dokud nedojdou veškerá slovíčka v uživatelem zvolených lekcích. Tlačítko umístěné napravo od místa pro psaní odpovědi slouží k zadávání textu hlasem. Na displeji jsou zobrazeny výsledky za současné zkoušení společně s výsledky za celou dobu. Tlačítko umístěné v pravém horním rohu slouží ke změně směru zkoušení. Při stisku tohoto tlačítka může být uživatel také zkoušen na slovíčka, na která Obrázek 3: Zkoušení formou
již byl zkoušen v předchozím směru zkoušení.
doplňování odpovědi
50
Zkoušení formou odpovědi vím/nevím Zkoušení
probíhá
a probíhá,
dokud
formou nedojdou
odpovědi
vím/nevím
veškerá
slovíčka
v uživatelem zvolených lekcích. Tlačítka správně a špatně slouží jako odpovědná tlačítka. Pokud uživatel klikne na tlačítko správně, znamená to, že zadaný překlad zná. Pokud klikne na tlačítko špatně, zadaný překlad nezná. Pokud si uživatel není jistý správnou odpovědí, může si nechat zobrazit správnou odpověď. Tlačítko umístěné v pravém horním rohu slouží ke změně směru zkoušení. Při stisku tohoto tlačítka Obrázek 4: Zkoušení formou odpovědi vím/nevím
může být uživatel také zkoušen na slovíčka, na která již byl zkoušen v předchozím směru zkoušení.
Nastavení aplikace V sekci nastavení lze upravovat způsob, jakým bude probíhat kontrola zadané odpovědi zkoušení. Uživatel zde může vynulovat výsledky doposud provedených zkoušení.
Obrázek 5: Nastavení aplikace
51