7. stretnutie: Markdown Editor

Cieľové elementy

Vytvoríme textový editor s podporou Markdownu so zobrazením výsledného HTML. Na tablete dokonca v dvojpanelovom režime!

Koncepty, ktoré zvládneme

  • fragmenty, fragmenty, fragmenty
  • statická a dynamická deklarácia fragmentov
  • transakcie fragmentov
  • komunikácia od fragmentu k aktivite a od aktivite k fragmentu
  • webový prehliadač WebView
  • preferencie

Výsledná aplikácia

Screenshot finálnej aplikácie

Git Repo

Finálny kód aplikácie je na GitHube.

Príprava projektu

Vízia

Vytvoríme textový editor s podporou Markdownu so zobrazením výsledného HTML. V textovom políčku si môžeme vytvárať text, ktorý následne môžeme zobraziť v náhľade, teda v naformátovanom tvare s využitím webového prehliadača.

Budeme podporovať dva režimy zobrazovania: na malých zariadeniach (telefónoch) sa budeme prepínať medzi zobrazením textu a náhľadu, zatiaľčo na veľkých zariadeniach zobrazíme text a náhľad vedľa seba.

Nový projekt

Založme si nový projekt s názvom Markdownr a balíčkom sk.upjs.ics.android.markdownr na platforme Android 4.0.3. a vytvorme prázdnu aktivitu MainActivity.

Prečo fragmenty?

S príchodom tabletov zrazu máme na displeji omnoho viac miesta pre omnoho viac informácií. Zrazu už nemusíme zobrazovať informácie len pod sebou, či preklikávať sa na ne cez viacero aktivít, ale pokojne môžeme zobrazovať údaje vedľa seba, či prezentovať viacero panelov z rozličnými informáciami.

Pozrime sa na taký GMail na tabletoch: zrazu máme miesto na zoznam priečinkov, vedľa neho umiestniť zoznam mailov a ešte ostane miesto i na konverzáciu.

Aplikácia GMail na tabletoch

Toto všetko je možné vďaka fragmentom.

Fragment reprezentuje znovupoužiteľný panel či modul s používateľským rozhraním. Má svoj vlastný životný cyklus, ale vždy musí byť obsiahnutý v niektorej aktivite.

Vďaka fragmentom môžeme budovať modulárne používateľské rozhranie. Aktivita totiž môže zobrazovať jeden či viacero fragmentov, ktoré dokáže podľa potreby skrývať, či preusporiadavať. Vďaka flexibilnému API v Androide môže vyzerať aktivita inak na veľkom displeji a inak na štandardnom telefóne: stačí zobrazovať iné fragmenty.

Fragmenty v našej aplikácii

V Markdownri použijeme dva fragmenty: jeden bude obsahovať políčko EditText pre editovaný text a druhý poslúži pre náhľad výsledku. Na malom displeji sa budeme prepínať medzi zobrazovaním a skrývaním oboch fragmentov, ale na veľkých zariadeniach ukážeme oba fragmenty bok po poku.

Fragment pre zdrojový kód

Vytvorme fragment pre zdrojový kód! Budeme potrebovať dve veci: jednak Java triedu dediacu od triedy Fragment a jednak layout, ktorý definuje rozloženie komponentov.

V Studiu zvoľme New | Fragment | Fragment (Blank), čím spustíme sprievodcu vytváraním fragmentu.

Dialóg nového fragmentu

V dialógu vyplňme názov triedy fragmentu a názov layoutového súboru. Keďže vytvárame fragment pre zdrojový text v HTML/Markdowne, nazvime ho SourceFragment.

V sprievodcovi ďalej zrušme začiarknutie vytvárania továrenských metód (Include fragment factory methods) a rovnako vypnime generovanie callbackov (Include interface callbacks), pretože v opačnom prípade dostaneme k dispozícii kopec ukážkového kódu, ktorý nás bude v tejto fáze viac miasť než pomáhať.

Dialóg nového fragmentu

Po skončení sprievodcu sa v projekte objaví Java súbor SourceFragment a layout fragment_source.xml.

Úprava layoutu

Vyhoďme z layoutového súboru fragment_source.xml ukážkový TextView a nahraďme ho textovým políčkom pre zdrojový text HTML/Markdownu:

<EditText
    android:id="@+id/sourceEditText"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:hint="Type *some* text..."
    android:gravity="top|left"
    />

V tomto prípade nemáme nič špeciálne, azda len zarovnanie textu: pomocou atribútu gravity zarovnáme text nahor (top) a zároveň doľava (left), pričom obe možnosti skominujeme cez operátor |.

Zdrojový kód

Fragment je trieda, ktorá dedí od android.app.Fragment a podobne ako aktivita prekrýva niektoré metódy životného cyklu. V tomto je fragment chápaný ako akási „podaktivita“. Platí pre ňu však dôležité pravidlo:

Trieda fragmentu musí mať verejný konštruktor bez parametrov!

Studio nageneruje nasledovný kód:

package sk.upjs.ics.android.markdownr;

import android.os.Bundle;
import android.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

public class SourceFragment extends Fragment {
    public SourceFragment() {
        // Required empty public constructor
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_source, container, false);
    }

}

Do tohto kódu nebudeme dlho nič dopĺnať: koniec koncov, cieľom je len zobrazit textové políčko.

Zobrazenie fragmentu v aktivite: po staticky

Fragment zavedieme do aktivity jednoducho: uvedieme ho do jej layoutového súboru, čo znamená, že upravíme súbor activity_main.xml. Ešte pred akýmikoľvek zmenami odstránime ukážový TextView, ktorý nebude potrebný.

Fragmentu vloženému do layoutu zodpovedá element <fragment> (s malým „f“), ktorý uvedieme nasledovne:

<fragment
    android:id="@+id/sourceFragment"
    android:name="sk.upjs.ics.android.markdownr.SourceFragment"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    />

Najdôležitejšou vlastnosťou konfigurácie je názov triedy fragmentu (name) a šírka s výškou. Identifikátor je voliteľným atribútom, ale hodí sa v neskorších fázach konfigurácie.

Ak je fragment deklarovaný v layoutovom súbore cez element <fragment> ide o statický fragment (nemá to však nič spoločné ani so statickými premennými, ani so statickými triedami v Jave!). Takéto fragmenty nemožno počas behu aplikácie dynamicky nahrádzať, pridávať, ani odstraňovať. Jedným slovom, statické fragmenty sú vytesané do kameňa (teda, pardón, do displeja).

Ak spustíme aplikáciu, uvidíme textové políčko správne vložené do fragmentu v aktivite.

Fragment pre náhľad

Dodajme teraz ešte jeden fragment pre náhľad. Zopakujeme proces vytvárania fragmentu, kde uvedieme názov triedy PreviewFragment a do layoutu fragmentu vložíme element pre zobrazenie HTML: WebView.

<WebView
    android:id="@+id/previewWebView"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

Komponent WebView reprezentuje HTML prehliadač s plnou podporou HTML štandardov.

Zdrojový kód aktivity bude pomerne jednoduchý:

public class PreviewFragment extends Fragment {

    public static final String URL_ENCODING = null;

    private WebView previewWebView;

    public PreviewFragment() {
        // Required empty public constructor
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View fragmentLayout = inflater.inflate(R.layout.fragment_preview, container, false);

        this.previewWebView = (WebView) fragmentLayout.findViewById(R.id.previewWebView);
        this.previewWebView.loadData("<i>No Markdown source</i>", "text/html; charset=UTF-8", URL_ENCODING);

        return fragmentLayout;
    }
}

Metóda onCreateView() vyzerá v tomto prípade trochu inak ako vo fragmente s HTML zdrojákom. Z layoutového súboru fragmentu nafúkneme view, ktorý obsahuje koreňový widget z layoutu (v tomto prípade je ním FrameLayout zodpovedajúci koreňovému elementu <FrameLayout> z layoutového súboru).

Ak chceme vo fragmente vyhľadať widget podľa identifikátora, využijeme metódu findViewById(). Tú však nemáme zdedenú od fragmentu, ale musíme ju zavolať práve na koreňovom viewe fragmentu, čo je vidieť v ukážke.

Následne vyhľadáme inštanciu WebView a poznačíme si ju do inštančnej premennej. Pomocou metódy loadData() načítame ľubovoľné stringové dáta ako HTML. Potrebujeme uviesť:

  • HTML kód, ktorý sa má zobraziť
  • MIME typ obsahu (content type) vrátane znakovej sady. Keďže zobrazujeme HTML súbor s UTF-8 kódovaním, využijeme typ text/html; charset=UTF-8
  • tretí parameter reprezentuje kódovanie, kde použijeme nullovú konštantu.

Dve aktivity bok po boku: statický prístup.

Dve aktivity umiestnime vedľa seba veľmi jednoducho: obe ich deklarujeme vedľa seba v layoutovom súbore v dvoch elementoch <fragment>. Na to, aby to fungovalo, musíme vykonať v layoute aktivity viacero zmien:

  • implicitný layout zmeníme na LinearLayout s horizontálnou orientáciou android:orientation="horizontal". Tým dosiahneme ukladanie komponentov vedľa seba.
  • obom fragmentom nastavíme váhu layout_weight na jedna.
  • obom fragmentom nastavíme šírku na 0dp.

Váha komponentov (weight) určuje dynamické rozloženie voľného miesta pri rozložení widgetov v layoute. Ak má prvý widget váhu X a druhý widget váhu Y, znamená to, že prvý dostane X / (X + X) a druhý zasa Y / (X + Y) miesta na displeji. V našom prípade každý fragment dostané 1 / (1 + 1), teda polovicu miesta na displeji.

Šírka 0dp v šírke widgetu je androiďácka konvencia, ktorá hovorí, že šírka widgetu sa určí automaticky, na základe váh. Zároveň to ušetrí niekoľko cyklov CPU, pretože layout nemusí prepočítavať úvodné rozmery widgetov pred ich prepočítaním na váhy.

Výsledný zdroják teda vyzerá nasledovne:

<LinearLayout android:orientation="horizontal" xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:tools="https://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity">

    <fragment
        android:id="@+id/sourceFragment"
        android:name="sk.upjs.ics.android.markdownr.SourceFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        />

    <fragment
        android:id="@+id/previewFragment"
        android:name="sk.upjs.ics.android.markdownr.PreviewFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        android:layout_weight="1"
        />

</LinearLayout>

Aplikácia po spustení vyzerá veselo:

To nie je ktoviečo, však? Skúsme otočiť displej:

To vyzerá lepšie, ale... nemôžeme predsa nútiť používateľa, aby appka vyzerala v nejakom zobrazení nepoužiteľne!

Odlišné rozloženie pre odlišné zariadenia

Čo keby sme na veľkých zariadeniach zobrazovali fragmenty vždy vedľa seba a na telefónoch zobrazovali buď zdrojový kód alebo náhľad? Ako na to? Platforma Androidu na to myslela a umožňuje definovať viacero rôznych layoutov pre rozličné okolnosti zobrazenia aktivity.

Doposiaľ mala každá aktivita len jeden layoutový XML súbor, ale je čas to zmeniť. Situácie budú dve:

  • pre veľké zariadenia v horizontálnom režime zobrazíme dva fragmenty bok po boku
  • pre ostatné zariadenia a ostatné prípady zobrazíme buď zobrazíme zdrojový fragment alebo uvidíme fragment s náhľadom.

Vytvorenie layoutu s kvalifikátorom

Ako môžeme definovať viacero layoutov? Cez tzv. kvalifikátory. Našim dvom situáciám definujeme dva layouty s dvoma kvalifikátormi:

  • veľké zariadenia v horizontálnom režime budú mať kvalifikátor sw600dp-land
  • ostatné zariadenia budú bez kvalifikátora, čiže sa použijú vo všetkých ostatných prípadoch.

V kvalifikátore znamená:

  • sw600dp je minimálna šírka displeja 600 do
  • land zodpovedá zariadeniu otočenému na šírku (landscape)

Vytvorme teda nový layout hlavnej aktivity, ktorý bude mať tento kvalifikátor (File | New | Android Resource File). Uveďme rovnaký názov súboru (layout_main.xml) ako mal štandardný layout. Zo zoznamu vyberme položky Minimum screen width, pre ktorý vyplňme 600 (dp)

Ďalej nastavíme i orientáciu displeja, a to kvalifikátorom Orientation, kde zvolíme landscape.

Všimnime si ako sa v Studiu priečinok layout pre activity_main.xml zrazu stal kontajnerom pre dve rozličné položky zodpovedajúce dvom kvalifikátorom.

Konfigurácia layoutov: akčný plán

Akčný plán bude nasledovný:

  1. Pre veľké zariadenia deklarujeme fragmenty staticky: uvedieme ich vedľa seba.
  2. Pre ostatné zariadenia budeme fragmenty zobrazovať dynamicky.
  3. Magickým spôsobom(tm) sa rozhodneme, ktorý spôsob použiť.

Dva fragmenty vedľa seba

Dva fragmenty na veľkých zariadeniach dosiahneme presne takým layoutom, ktorý sme používali doteraz. Jednoducho do layoutového súboru s kvalifikátorom sw600-dp skopírujeme celý obsah štandardného layoutového súboru. Na druhej strane, v štandardnom layoute nebudeme používať žiadne widgety, pretože obsah aktivity naplníme dynamicky. Presúvanie obsahu chce trochu kopírovania, ale v konečnom dôsledku by sme mali vzájomne vymeniť obsahy oboch súborov.

Okrem toho ešte jedna zmena: bezfragmentový layout musí dostať svoj vlastný identifikátor, ktorý budeme používať na identifikovanie, či sme v dvojfragmentovom alebo jednofragmentovom režime. Pribudne teda android:id="@+id/singleFragmentLayout", ale len v jednom zo súborov!

Štandardný layout bez kvalifikátora teda bude vyzerať:

<LinearLayout android:orientation="horizontal" xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:tools="https://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity"
    android:id="@+id/singleFragmentLayout"
    >

    <!-- single fragment will be added dynamically -->
</LinearLayout>

Jeden či dva fragmenty? Rozhodnutie v kóde

Rozhodnutie, či sme v jedno- alebo dvojfragmentovom móde, urobíme v kóde. Postup rozhodnutia v sebe nesie jeden a pol triku!

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    if (isSinglePane()) {
        showSourcePane();
    }
}

public boolean isSinglePane() {
    return findViewById(R.id.singleFragmentLayout) != null;
}

private void showSourcePane() {
    // TODO implementovať zobrazenie fragmentu so zdrojákom
}

Čo presne sa stane?

  1. Ak sa aktivita zobrazí na veľkom zariadení a v horizontálnom režime, metóda setContentView() automaticky načíta a spracuje layoutový súbor s kvalifikátorom sw600-land. Dôležitý trik č. 2 je, že v tomto layoutovom súbore sa nenachádza widget s identifikátorom singleFragmentLayout. Absencia znamená, že sme v dvojpanelovom režime a oba fragmenty načítame staticky.
  2. Ak je aktivita na menšom zariadení, metóda setContentView() nafúkne layout bez fragmentov, ale zato s identifikátorom singleFragmentLayout. Znamená to, že sme v jednofragmentovom režime a o obsah sa postaráme dynamicky.

Vyskúšajte si to na dvoch rozličných emulovaných zariadeniach!

Príprava na dynamické nahrádzanie fragmentov (DNF)

Ešte skôr než pristúpime k dynamickému nahrádzaniu fragmentov, potrebujeme urobiť závažnú zmenu. Naša aktivita momentálne dedí od ActionBarActivity, čo zmeníme na dedenie od Activity.

public class MainActivity extends Activity

Okrem toho musíme explicitne zaviesť zobrazovanie lišty akcií v manifeste. Do elementu <manifest> dodáme nasledovný element:

<uses-sdk android:minSdkVersion="11" />

Týmto elementom vyhlásime, že naša aplikácia potrebuje na spustenie aspoň API Level 11, teda Android 4.0 a novší.

Rovnako musíme upraviť tému aplikácie. Zmeňme atribút android:theme v elemente <application> na hodnotu:

android:theme="@android:style/Theme.Holo.Light.DarkActionBar"

Dynamické nahrádzanie fragmentov

Správca fragmentov a transakcie

Dynamické nahrádzanie fragmentov nejde len tak, napriamo. Potrebujeme manažment, ktorý sa o to postará a transakcie, ktoré zaručia, že používateľské rozhranie neostane v rozdrbanom nekonzistentnom stave.

Fragment Manager

FragmentManager je objekt, ktorý sa stará o fragmenty v konkrétnej aktivite. Obsluhuje ich životnosť, vytvára ich, ak treba a upratuje, ak sa aktivita plánuje zničiť. Inštanciu manažéra fragmentov možno získať v aktivite volaním getFragmentManager().

Transakcie

V našom prípade aktivita nemá v layoutovom súbore žiadne widgety, pretože sme sa dohodli na dynamickom pridaní fragmentu. Akákoľvek zmena (pridanie, ubranie, či zmena fragmentu) sa musí diať v rámci transakcie, teda postupnosti krokov, ktorá musí uspieť celá.

V našom prípade bude transakcia pozostávať z jediného kroku: pridania existujúceho fragmentu. Plán práce je nasledovný:

  1. Získame inštanciu správcu cez getFragmentManager().
  2. Začneme v ňom transakciu pomocou metódy beginTransaction().
  3. V transakcii pridáme, odoberieme, či nahradíme fragmenty.
  4. Transakciu commitneme, teda potvrdíme jej vykonanie.

Ukážka kódu?

private void showSourcePane() {
    FragmentManager fragmentManager = getFragmentManager();
    FragmentTransaction tx = fragmentManager.beginTransaction();
    tx.replace(R.id.singleFragmentLayout, new SourceFragment())
    tx.commit();
}

V kóde používame na pridanie fragmentu metódu replace(), ktorá pridá do aktivity nový fragment a voliteľne ním nahradí iný, existujúci fragment. Na pridanie potrebujeme uviesť identifikátor layoutu, do ktorého pridáme fragment a inštanciu fragmentu. ID fragmentu prevezmeme z layoutového súboru aktivity (pamätáte, definovali sme ho, aby sme odlíšili dvojfragmentové zobrazenie od jednofragmentového) a inštanciu vytvoríme vďaka (áno, je tu znovu) verejnému jednoparametrovému konštruktoru.

Skrátený zápis transakcií

V kóde sa často používa vlastnosť fluent API, ktorá skracuje zápis:

private void showSourcePane() {
    getFragmentManager()
            .beginTransaction()
            .replace(R.id.singleFragmentLayout, new SourceFragment())
            .commit();
}

Skúsme spustiť aplikáciu!

Aplikácia beží, ale skúsme otočiť zariadenie!

Životný cyklus aktivity

Prečo sa po otočení telefónu odstránil text z textového políčka? Veď sme kedysi tvrdili, že o stav widgetov sa postará samotný Android! Príčinou je náš kód: ak sa otočí zariadenie, nastane zmena konfigurácie systému, čo povedie k zničeniu aktivity a jej novému vytvoreniu. V metóde onCreate() voláme showSourcePane(), kde sa v rámci transakcie vždy vytvorí nový objekt fragmentu.

Ako to môžeme opraviť? Musíme odlíšiť, či sa aktivita vytvára nanovo alebo obnovuje po reštarte, a ako je známe, odlíšime to podľa nullovosti parametra savedInstanceState. Ak je parameter nenullový, znamená to obnovenie aktivity z bundlu vrátane fragmentu a preto ho nemusíme vytvárať nanovo.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    if(savedInstanceState == null) {
        if (isSinglePane()) {
            showSourcePane();
        }
    }
}

Teraz môžeme otáčať aplikáciu do vôle a text sa vždy zachová.

Prepínanie layoutov

Ako budeme prepínať režim úpravy textu a náhľad? Jednoduchý spôsob môže byť pomocou dvoch tlačidiel na lište akcií. Pridajme na lištu dve tlačidlá, ktoré budeme striedavo zobrazovať a prijímať. Do súboru menu_main.xml teda pridajme nasledovný obsah.

<menu xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:tools="https://schemas.android.com/tools" tools:context=".MainActivity" tools:ignore="AppCompatResource">

    <item android:id="@+id/previewAction"
        android:title="Preview"
        android:showAsAction="always"
    />

    <item android:id="@+id/sourceAction"
        android:title="Source"
        android:showAsAction="always"
    />
</menu>

Každé tlačidlo reprezentované položkou item budeme vždy vykresľovať na lište (atribút showAsAction s hodnotou always).

Dodajme teraz obsluhu tlačidiel lišty akcií

Obsluha lišty akcií

Obsluha tlačidiel bude jednoduchá: vždy, keď klikneme na tlačidlo náhľadu, zavoláme metódu showPreviewPane() (ešte neexistuje) a pre metódu tlačidlo zdrojového kódu vyvoláme showSourcePane(), kde dynamicky nahradíme akýkoľvek fragment v aktivite fragmentom so zdrojákom.

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    int id = item.getItemId();
    switch(id) {
        case R.id.sourceAction:
            showSourcePane();
            return true;
        case R.id.previewAction:
            showPreviewPane();
            return true;
    }

    return super.onOptionsItemSelected(item);
}

Zobrazenie fragmentu s náhľadom

Plán práce

Poďme teraz dopracovať zobrazovanie fragmentu s náhľadom. Plán práce?

  1. Vytvoriť inštanciu náhľadového fragmentu.
  2. Dopraviť do nej HTML kód z textového políčka fragmentu zdrojáku.
  3. V rámci transakcie nahradiť existujúci fragment zdrojáku náhľadovým fragmentom.

Fragment a získavanie informácií z okolia

Ak chceme do čerstvo vytváraného fragmentu dopraviť dáta zvonku, môžeme pokojne využiť konštruktor. V našom prípade chceme dotlačiť reťazec obsahujúci dáta s HTML zdrojákom, teda String. Je tu však ale ALE: a to udržiavanie stavu. V bežnom objektovo orientovanom jazyku by sme vytvorili takýto konštruktor:

private String htmlSource;

public PreviewFragment(String htmlSource) {
    this.htmlSource = htmlSource;
}

Jednoduché poznačenie parametra do inštančnej premennej však v Androide nemusí fungovať: fragment a jeho stav (teda inštančné premenné) sa totiž môže zničiť spolu s aktivitou, čo nás bude nútiť vymyslieť kadejaké okľuky v duchu onSaveInstanceState().

V Androide však existuje technika na obídenie tohto problému. Každý fragment so sebou nesie argumenty, čo je bundle, v ktorom možno prenášať parametre potrebné pre vytvorenie objektu fragmentu. A aby nebolo vytváranie argumentov príliš neprehľadné, odporúča sa namiesto konštruktora definovať vo fragmente továrenskú statickú metódu, ktorá vytvorí fragment na základe typovaných parametrov.

Továrenská metóda vyzerá nasledovne:

public static final String ARG_HTML_SOURCE = "HTML_SOURCE";

public static PreviewFragment newInstance(String htmlSource) {
    Bundle arguments = new Bundle();
    arguments.putSerializable(ARG_HTML_SOURCE, htmlSource);

    PreviewFragment fragment = new PreviewFragment();
    fragment.setArguments(arguments);

    return fragment;
}

Fragment potom vieme vytvoriť nasledovne:

PreviewFragment fragment = PreviewFragment.newInstance("<b>BOLD</b>");

Využitie argumentov v kóde?

Ako potom využijeme argumenty v kóde? Jednoducho vytiahneme bundle s argumentami cez getArguments() a užijeme ho dľa ľubovole. Urobme vo fragmente s náhľadom metódu:

private String getHtmlSource() {
    Bundle arguments = getArguments();
    if(arguments != null && arguments.containsKey(ARG_HTML_SOURCE)) {
        return arguments.getString(ARG_HTML_SOURCE);
    }
    return "<i>No Markdown source</i>";
}

Načítavanie dát potom upravíme v metóde onCreateView() nasledovne:

this.previewWebView.loadData(getHtmlSource(), "text/html; charset=UTF-8", URL_ENCODING);

Komunikácia medzi fragmentami: preprava dát medzi HTML editorom a náhľadom

Teraz už len potrebujeme vytiahnuť dáta z fragmentu so zdrojovým kódom a prepraviť ich do náhľadového fragmentu. Druhá polovica cesty je už hotová, pretože dáta dotlačíme do fragmentu cez newInstance(). Ostáva dopracovať úvodnú časť.

Problém, ktorý riešime, spočíva vo všeobecnosti v komunikácii medzi fragmentami a existuje viacero spôsobov jeho realizácie, ktoré sa odvíjajú od miery vzájomného prepojenia fragmentov.

  1. buď aktivita priamo vyberá HTML text z fragmentu so zdrojákom a pchá ho do náhľadového fragmentu
  2. alebo objekt fragmentu zdrojového kódu je priamo prepojený s objektom fragmentu náhľadu
  3. alebo aktivita a fragmenty komunikujú cez poslucháča

Predveďme si teraz prvý spôsob. Druhý spôsob je použiteľný len vtedy, ak sú oba fragmenty zobrazené vedľa seba, čo si ukážeme neskôr, podobne ako tretí spôsob, ktorý je mimoriadne flexibilný, ale o niečo náročnejší na implementáciu.

V tomto prípade teda aktivita vytiahne priamo zo svojho layoutu widget zodpovedajúci textovému políčku so zdrojovým kódom. Náhľad do layoutového súboru fragment_source.xml napovie, že identifikátor tohto EditTextu je sourceEditText. Ak teda aktivita zobrazuje zdrojový kód, má vo svojom layoute fragment so zdrojovým kódom a layout tohto fragmentu v sebe nesie EditText. Keďže metóda findById() hľadá widget po celom strome, bez ohľadu na hĺbku, dokážeme si ho takto vytiahnuť v aktivite, pričom vôbec nemusíme brať ohľad na to, že je tento widget v skutočnosti vo fragmente.

V kóde teda vytiahneme textové políčko, z neho vyberieme text, vytvoríme inštanciu náhľadového fragmentu, do ktorej dopravíme HTML kód a v transakcii následne zameníme fragmenty.

private void showPreviewPane() {
    String htmlSource = getHtmlSource();
    if(htmlSource == null) {
        return;
    }

    PreviewFragment previewFragment = PreviewFragment.newInstance(htmlSource);
    getFragmentManager()
            .beginTransaction()
            .replace(R.id.singleFragmentLayout, previewFragment)
            .addToBackStack(DEFAULT_BACKSTACK_NAME)
            .commit();
}

private String getHtmlSource() {
    EditText sourceEditText = (EditText) findViewById(R.id.sourceEditText);
    if(sourceEditText == null) {
        Log.w(MainActivity.class.getName(), "HTML source fragment is not loaded");
        return null;
    }

    return sourceEditText.getText().toString();
}

Vyskúšajte si aplikáciu!

Životný cyklus fragmentov

Čo sa stane s fragmentom, ktorý bol nahradený iným? V štandardnom prípade je odpoveď jednoduchá: jednoducho sa zničí a tesne predtým sa zavolajú metódy jeho životného cyklu (napr. onPause() nasledovaná onStop()). Chcete to vidieť? Prepnite sa zo zdrojového fragmentu do náhľadu a naspäť: uvidíte, že zdrojový text sa stratil. Fragment so zdrojákom sa pri prvej transakcii (od HTML k náhľadu) zničil, a pri druhej transakcii nanovo vytvoril. Súčasne s druhou transakciou sa zničí aj fragment náhľadu.

Ako sa dajú takéto situácie ošetriť? Ukladaním stavu. Fragment má síce metódu onSaveInstanceState(), ktorá slúži podobnému účelu ako jej súdružka v aktivite, ale pri ničení fragmentu sa nebude volať. Ide totiž o podobnú situáciu, ako v prípade aktivity, ktorá je explicitne ukončená (napr. stlačením hardvérového tlačidla Back) a je jasné, že používateľ sa už k nej nevráti.

Stav nahradených fragmentov možno ukladať dvoma doplňujúcimi sa spôsobmi:

  • využijeme back stack, teda zásobník histórie fragmentov, kam sa nahrádzaný fragment uloží pre prípadné budúce použitie.
  • ak chceme, aby obsah fragmentu pretrval, ide o perzistentný stav, teda by sme ho mali ukladať do perzistentného úložiska (napr. do databázy, či do súboru)

Back Stack

Hardvérové tlačidlo Back funguje podobne ako v prehliadačoch -- jednoducho čakáme, že jeho stlačením sa dostaneme na predošlú „stránku“. V Androide však neprechádzame históriou stránok, ale aktivít. Vo vnútri platformy je toto správanie možné vďaka zásobníku aktivít, tzv back stack. Vždy, keď príde na popredie nová aktivita, aktuálna sa zaradí na vrchol zásobníka (operácia push). Tlačidlom Back jednoducho zahodíme aktuálnu aktivitu a na jej miesto príde predošlá aktivita z vrcholu zásobníka. (Celý zásobník možno vidieť pomocou hardvérového tlačidla Recent Apps).

Toto tlačidlo pracuje nielen nad aktivitami, ale s trochou programovania ho môžeme nastaviť tak, aby pracovalo i s fragmentami. Podobne ako aktivity, i fragmenty (teda presnejšie ich stav) možno ukladať na zásobník histórie a vrátiť sa späť pomocou Backu. Z technického hľadiska určme, že každá transakcia v správcovi fragmentov sa má zapamätať do histórie a v prípade potreby ju vieme vrátiť, teda vykonať jej kroky v opačnom poradí.

V našom prípade si môžeme zapamätať prechod medzi fragmentom zdrojového kódu a náhľadom a z náhľadu sa vrátiť do HTML zdrojáku buď pomocou tlačidla na lište akcií alebo cez tlačidlo Back. Dôležité je, aby sa táto transakcia vložila na back stack, čo dosiahneme v dvoch krokoch:

  1. pri tvorbe transakcie vyžiadame uloženie na back stack
  2. ešte predtým sa musíme pozrieť, či náhodou v zásobníku nie je už uložený fragment, ku ktorému sa chceme vrátiť.
    • Ak áno, vytiahneme ho z histórie, teda zo zásobníka a zobrazíme ho.
    • Ak nie, vytvoríme novú inštanciu a zobrazíme ju.

Uloženie na back stack

Uloženie na back stack dosiahneme volaním metódy .addToBackStack(DEFAULT_BACKSTACK_NAME) na transakcii, ktorou zobrazíme náhľad. Konštanta v parametri uvádza názov stavu uloženého v back stacku, ktorý sa tradične ignoruje (teda udáva sa null, resp. analogická konštanta.)

public static final String DEFAULT_BACKSTACK_NAME = null;

Kód teda bude vyzerať nasledovne:

private void showPreviewPane() {
    ...

    getFragmentManager()
            .beginTransaction()
            .replace(R.id.singleFragmentLayout, previewFragment)
            .addToBackStack(DEFAULT_BACKSTACK_NAME)
            .commit();
}

Už teraz môžeme skúsiť spustiť aplikáciu. Ak uvedieme nejaký ukážkový text, ihneď sa prepneme do náhľadového režimu, a následne klikneme na hardvérové tlačidlo Back, zobrazí sa opäť pôvodný fragment so zdrojovým kódom.

Vyhľadávanie v back stacku

Čo však nefunguje, je prepínanie medzi tlačidlami. Netreba sa ľakať, pretože sme doposiaľ implementovali len prvý krok z nášho plánu. Poďme teraz na druhý krok:

private void showSourcePane() {
    SourceFragment sourceFragment = (SourceFragment) getFragmentManager().findFragmentByTag(FRAGMENT_TAG_SOURCE);
    if(sourceFragment == null) {
        sourceFragment = new SourceFragment();
    }

    getFragmentManager()
            .beginTransaction()
            .replace(R.id.singleFragmentLayout, sourceFragment, FRAGMENT_TAG_SOURCE)
            .commit();
}

V zásobníku histórie, teda v back stacku vieme hľadať fragment podľa akéhosi tagu. Čo to je? Je to ľubovoľný textový identifikátor, ktorý vieme pripojiť k fragmentu pridávaného do správcu fragmentov. Všimnime si, že v metóde replace() pribudol tretí parameter s konštantou:

public static final String FRAGMENT_TAG_SOURCE = "SourceFragment";

Znamená to, že fragment so zdrojovým kódom, ktorý sme vložili do správcu (a ešte predtým z neho odstránili fragment s náhľadom), dostal svoj tag. Na základe tohto tagu sa vieme na začiatku metódy rozhodnúť, či sa už v back stacku nachádza historický záznam o kedysi navštívenom zdrojákovom fragmente. Ak v záznamoch nájdeme.. err... záznam o niekdajšom zdrojákovom fragmente, jednoducho ho vytiahneme z histórie a zobrazíme. V opačnom prípade jednoducho vytvoríme nový objekt zdrojákového fragmentu a v rámci transakcie ho zobrazíme.

Teraz sa môžeme veselo prepínať medzi náhľadom a zdrojákom, či už cez tlačidlá alebo cez hardvérové tlačidlo Back.

Bonus: ukladanie perzistentného stavu do preferences

Ak chceme, aby text v zdrojovom fragmente dokázal prežiť vypnutie zariadenia, musíme sa postarať o jeho perzistenciu. Keďže dáta, ktoré chceme ukladať, sú tvorené len samotným HTML textom, nemusíme štartovať mašinériu SQL databázy alebo loaderov. Na druhej strane, nemusíme ani riešiť ukladanie do súboru a podobne.

Najjednoduchší spôsob perzistencie dát ponúka trieda https://developer.android.com/reference/android/content/SharedPreferences.html.

SharedPreferences predstavuje perzistentnú hashmapu, teda perzistentný objekt na ukladanie kľúčov a ich hodnôt.

Ukladanie perzistentného stavu vo fragmente

Ukladanie perzistentného stavu vo fragmente sa riadi nasledovným plánom:

  1. v metóde onPause(), teda ak sa fragment pozastavuje (pretože sa pozastavuje aktivita, v ktorej sa nachádza) uložíme HTML zdroják do shared preferences.
  2. získame inštanciu objektu SharedPreferences
  3. pomocou metódy edit() získame editor tohto objektu, ktorý zaručí ukladanie zmien v hashmape s dodržaním konzistentnosti
  4. zmeny vykonáme pomocou metód putXXX()
  5. po spracovaní zmien ich komitneme

Editor preferencií, reprezentovaný triedou SharedPreferences.Editor, zodpovedá akejsi transakcii zmien, kde uvedieme všetky zmeny a na konci ich komitneme.

Príklad použitia v kóde SourceFragmentu?:

@Override
public void onPause() {
    getPreferences().edit()
            .putString(PREFERENCES_KEY_DRAFT_SOURCE, sourceEditText.getText().toString())
            .commit();

    super.onPause();
}

Metóda getPreferences() je pomocná. Jej účelom je získať z fragmentu inštanciu obaľujúcej aktivity a na nej získať objekt SharedPreferences v štandardnom režime privátneho prístupu. Parameter Activity.MODE_PRIVATE hovorí, že k danej perzistentnej hashmape bude mať právo pristupovať len táto jedna aktivita.

private SharedPreferences getPreferences() {
    return getActivity()
            .getPreferences(Activity.MODE_PRIVATE);
}

Kľúč pre uloženie HTML zdrojáku sme definovali v konštante:

private static final String PREFERENCES_KEY_DRAFT_SOURCE = "draftSource";

Načítanie perzistentného stavu vo fragmente

Opačný proces je jednoduchší: získame objekt preferencií a spod príslušného kľúča vytiahneme uložený zdroják.

@Override
public void onResume() {
    super.onResume();

    String htmlSource = getPreferences().getString(PREFERENCES_KEY_DRAFT_SOURCE, DEFAULT_HTML_SOURCE);
    sourceEditText.setText(htmlSource);
}

Konštantna DEFAULT_HTML_SOURCE je naša vlastná a reprezentuje reťazec, ktorý sa má vrátiť, ak hodnota v mape neexistuje:

public static final String DEFAULT_HTML_SOURCE = "";

Upravený kód pre SourcceFragment

package sk.upjs.ics.android.markdownr;

import android.app.Activity;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;

public class SourceFragment extends Fragment {
    private static final String PREFERENCES_KEY_DRAFT_SOURCE = "draftSource";
    public static final String DEFAULT_HTML_SOURCE = "";

    private EditText sourceEditText;

    public SourceFragment() {
        // Required empty public constructor
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        View frameLayout = inflater.inflate(R.layout.fragment_source, container, false);
        sourceEditText = (EditText) frameLayout.findViewById(R.id.sourceEditText);
        return frameLayout;
    }

    @Override
    public void onPause() {
        getPreferences().edit()
                .putString(PREFERENCES_KEY_DRAFT_SOURCE, sourceEditText.getText().toString())
                .commit();

        super.onPause();
    }

    @Override
    public void onResume() {
        super.onResume();

        String htmlSource = getPreferences().getString(PREFERENCES_KEY_DRAFT_SOURCE, DEFAULT_HTML_SOURCE);
        sourceEditText.setText(htmlSource);
    }

    private SharedPreferences getPreferences() {
        return getActivity()
                .getPreferences(Activity.MODE_PRIVATE);
    }

}

Komunikácia medzi fragmentami v dvojpanelovom režime

Ak teraz spustíme aplikáciu na tablete, napr. v Genymotion na zariadení Nexus 7 bežiacom na Androide 4.4.4 v horizontálnom režime, uvidíme síce panely bok po boku, ale pracovať s aplikáciou nebude možné. Pokus o stlačenie tlačidla Preview dokonca povedie k pádu aplikácie. Je to preto, že tlačidlá na lište akcií využívajú transakcie fragmentov, ale v horizontálnom režime sme fragmenty deklarovali staticky v layoutovom súbore.

Ako vyriešiť tento problém a nepokaziť pri tom už existujúce správanie? Potrebujeme zaviesť komunikáciu medzi fragmentami.

Nad tým sme už raz uvažovali:

  1. buď aktivita priamo vyberá HTML text z fragmentu so zdrojákom a pchá ho do náhľadového fragmentu
  2. alebo objekt fragmentu zdrojového kódu je priamo prepojený s objektom fragmentu náhľadu
  3. alebo aktivita a fragmenty komunikujú cez poslucháča

Prvý bod sme implementovali pri prepínaní fragmentov. Druhý bod prichádza na rad práve teraz.

Plán práce je nasledovný:

  1. Vo fragmente so zdrojovým HTML budeme sledovať zmeny na textovom políčku so zdrojákom.
  2. Následne získame inštanciu náhľadového fragmentu
  3. Pošleme do nej aktualizované HTML.

Sledovanie zmien

Do metódy onCreate() v triede SourceFragment dodajme pomocnú metódy addSourceTextWatcher(), v ktorej priradíme textovému políčku poslucháča na zmeny TextWatcher.

Metóda bude vyzerať nasledovne: poslucháč bude implementovať len metódu afterTextChanged(), ktorá sa zavolá po úprave textu v textovom políčku a vo vnútri zavoláme našu metódy notifySourceChanged(), ktorá bude predbežne prázdna.

private void addSourceTextWatcher() {
    sourceEditText.addTextChangedListener(new TextWatcher() {
        @Override
        public void afterTextChanged(Editable s) {
            notifySourceChanged();
        }

        @Override
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            // empty implementation
        }

        @Override
        public void onTextChanged(CharSequence s, int start, int before, int count) {
            // empty implementation
        }
    });
}

private void notifySourceChanged() {

}

Notifikácia zmien

Čo s metódou notifySourceChanged()? Poďme podľa plánu: získame inštanciu náhľadového fragmentu. A odkiaľ? Zo správcu fragmentov!

Správca fragmentov sa totiž stará nielen o fragmenty pridávané dynamicky v transakciách, ale aj o staticky deklarované fragmenty z layoutov.

PreviewFragment previewFragment = (PreviewFragment) getFragmentManager().findFragmentById(R.id.previewFragment);

Na tomto náhľadovom fragmente potom zavoláme metódu setHtmlSource(), ktorá aktualizuje náhľad. Prirodzene, ešte neexistuje, ale pokojne si ju môžeme dopracovať:

public class PreviewFragment extends Fragment {
    ....

    public void setHtmlSource(String htmlSource) {
        if(this.previewWebView == null) {
            return;
        }
        this.previewWebView.loadData(htmlSource, "text/html; charset=UTF-8", URL_ENCODING);
    }
}

Metóda na notifikáciu potom bude vyzerať nasledovne:

private void notifySourceChanged() {
    PreviewFragment previewFragment = (PreviewFragment) getFragmentManager().findFragmentById(R.id.previewFragment);
    if(previewFragment == null) {
        return;
    }
    previewFragment.setHtmlSource(sourceEditText.getText().toString());
}

Aplikácia teraz beží a podporuje oba režime: panelový i dvojpanelový. Hlavné je však neklikať v dvojpanelovom režime na tlačidlá!

Bonus: Komunikácia medzi fragmentami v dvojpanelovom režime (II.)

Naše fragmenty spolu radostne komunikujú, ale má to istú nevýhodu pre udržovateľnosť. Dokumentácia hovorí, že fragment by mal byť implementovaný ako znovupoužiteľná a samostatne fungujúca jednotka, ktorá nezávisí priamo na ostatných komponentoch používateľského rozhrania.

V našom prípade to nie je celkom tak: fragment SourceFragment vie notifikovať náhľadový fragment PreviewFragment len vtedy, ak priamo získa jeho inštanciu cez správcu fragmentov.

Alternatívny prístup pre komunikáciu medzi fragmentami využíva tzv. komunikačný interfejs.

Náš zdrojákový fragment definuje interfejs, povedzme SourceChangedListener reprezentujúci poslucháča na zmeny HTML zdrojáku vo fragmente. Následne sa môže ktorýkoľvek poslucháč zaregistrovať na fragmente, prijímať upozornenia na zmeny a následne sa podľa toho zariadiť podľa ľubovole.

V našom prípade bude poslucháčom samotná aktivita, ktorá bude zmeny preklápať do náhľadového fragmentu. SourceFragment tak bude komunikovať s náhľadovým fragmentom (alebo kýmkoľvek iným) cez prostredníka, teda poslucháča, ktorým bude aktivita.

Interfejs deklarujme (pre poriadok) vo vnútri SourceFragmentu:

public interface SourceChangedListener {
    public void onSourceChanged(String source);
}

A ako budeme registrovať poslucháčov? Vytvorme vo fragmente metódu setSourceChangedListener():

public void setSourceChangedListener(SourceChangedListener sourceChangedListener) {
    this.sourceChangedListener = sourceChangedListener;
}

Táto metóda jednoducho poznačí poslucháča do inštančnej premennej:

private SourceChangedListener sourceChangedListener;

Prepojenie aktivity s fragmentom

Hlavným poslucháčom bude aktivita, ktorá sa prepája s fragmentom v metóde onAttach().

Metóda onAttach() je prvá metóda životného cyklu fragmentu. Vykonáva sa v momente, keď sa fragment prvýkrát spája s aktivitou.

Ak je aktivita poslucháčom, jednoducho ju zaregistrujeme a budeme jej preposielať informácie o zmenách.

@Override
public void onAttach(Activity activity) {
    super.onAttach(activity);
    if(activity instanceof SourceChangedListener) {
        setSourceChangedListener((SourceChangedListener) activity);
    }
}

Notifikácia o zmenách potom bude vyzerať nasledovne:

private void notifySourceChanged() {
    this.sourceChangedListener.onSourceChanged(this.sourceEditText.getText().toString());
}

Prepojenie aktivity s fragmentom z opačnej strany

Prepojenie aktivity a fragmentu z opačnej strany dosiahneme implementovaním interfejsu SourceChangedListener na aktivite a doplnením kódu do metódy onSourceChanged()

public class MainActivity extends Activity implements SourceFragment.SourceChangedListener {


    @Override
    public void onSourceChanged(String source) {
        PreviewFragment previewFragment = (PreviewFragment) getFragmentManager()
                .findFragmentById(R.id.previewFragment);
        if(previewFragment == null) {
            return;
        }
        previewFragment.setHtmlSource(source);
    }
}

Metóda onSourceChanged() jednoducho vytiahne fragment zo správcu fragmentov a prepošle mu zmenený zdrojový kód.

Bonus: dynamické skrývanie tlačidiel

Poďme vyriešiť dynamické skrývanie tlačidiel: ak je zobrazený zdroják, schovajme tlačidlo Náhľadu, a naopak. A ako bonus: v dvojfragmentovom režime schováme všetky tlačidlá:

Základný problém, ktorý budeme musieť vyriešiť, je rozhodnúť sa, ktorý fragment je aktuálne zobrazený. Použijeme na to kratučkú metódu:

private boolean isSourceFragmentShown() {
    return findViewById(R.id.sourceEditText) != null;
}

Ak sa v layoute aktivity nachádza textové políčko so zdrojákom, vidíme zdrojákový fragment.

Dynamická úprava action baru

Poďme teraz dynamicky upraviť action bar. Metóda onPrepareOptionsMenu() sa volá vo chvíli, keď sa majú vykresliť tlačidlá lišty akcií. V tejto metóde môžeme dynamicky skrývať tlačidlá:

@Override
public boolean onPrepareOptionsMenu(Menu menu) {
    MenuItem sourceActionItem = menu.findItem(R.id.sourceAction);
    MenuItem previewActionItem = menu.findItem(R.id.previewAction);

    if(isSinglePane()) {
        if(isSourceFragmentShown()) {
            sourceActionItem.setVisible(false);
        } else {
            previewActionItem.setVisible(false);
        }
    } else {
        sourceActionItem.setVisible(false);
        previewActionItem.setVisible(false);
    }
    return true;
}

V kóde vytiahneme z menu, reprezentujúce tlačidlá lišty akcií, objekty MenuItem prislúchajúce jednotlivým tlačidlám a v ifoch sa rozhodneme, čo skryjeme. Metóda musí vrátiť true, ak úspešne spracovala skrývanie a zobrazovanie položiek.

Na to, aby to fungovalo, musíme ešte dopracovať jednu vec: vždy, keď zmeníme zobrazený fragment, musíme tlačidlá lišty akcií zobraziť nanovo. Metóda invalidateOptionsMenu() hovorí, že treba lištu akcií načítať nanovo, pretože súčasný stav už nie je aktuálny.

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    int id = item.getItemId();
    switch(id) {
        case R.id.sourceAction:
            showSourcePane();
            invalidateOptionsMenu();
            return true;
        case R.id.previewAction:
            showPreviewPane();
            invalidateOptionsMenu();
            return true;
    }

    return super.onOptionsItemSelected(item);
}

To je všetko, spustite aplikáciu a všímajte si skrývanie tlačidiel!

Externé knižnice v Jave a podpora formátu Markdown (bonus)

Máme hotový editor HTML, ale povedzme si úprimne, zadávať HTML značky z mobilnej klávesnice nie je žiadne terno. Ak využijeme omnoho jednoduchšiu syntax pre formátovanie Markdown, písanie pôjde ako po masle.

Markdown môže byť prevedený do mnohých iných formátov, ale tradične sa výsledky prevádzajú do HTML. (Dôkazom je celý tento tutoriál.) V Jave existuje množstvo prevodných knižníc, a práve jednu spomedzi nich, Markdown Papers použijeme i v našom projekte.

Externé knižnice

Java je známa bohatstvom svojich knižníc, čoho priamym dôkazom je centrálne úložisko knižníc Maven. V Androide môžete z tejto klenotnice priamo čerpať. Knižnicu v Jave možno priamo používať v Androiďáckych aplikáciách. Dôležité je, aby využívala len tie triedy a metódy, ktoré sú v Androide dostupné. (Nečakajte teda, že si priamo spustíte aplikačný server Tomcat či swingovú aplikáciu.)

Zostavovací nástroj Gradle a externé knižnice

Android Studio zostavuje aplikácie s využitím zostavovacieho nástroja Gradle, ktorý je známy zo sveta Javy. Vďaka nemu môžeme veľmi jednoducho zaviesť do projektu akúkoľvek knižnicu. Dodajme teraz do projektu závislosť v podobe knižnice Markdown Papers.

Zavedenie závislosti

Základným konfiguračným súborom je súbor build.gradle, ktorý nájdeme v strome projektu v sekcii Gradle Scripts. Ale pozor! V projekte existujú dva takéto súbory: jeden pre projekte a druhý pre modul, v ktorom sa deklarujú závislosti.

Dodajme do sekcie dependencies závislosť na knižnici pomocou jednoznačného identifikátora v centrálnom úložisku knižníc. Sekci

dependencies { ...

compile 'org.tautua.markdownpapers:markdownpapers-core:1.4.2'

}

Studio potom upozorní na nutnosť aktualizovať nastavenia projektu (Sync Now), kde stiahne z internetu JAR pre knižnicu a zavedie ho do projektu

Ako zistíme, či knižnica je naozaj v projekte?

Ak sa prepneme na zobrazenie projektu kliknutím na záhlavie Android a prepnutím na Project, uvidíme alternatívny pohľad na kostru projektu:

V tomto pohľade uvidíme knižnicu markdown-papers-core-1.4.2, ktorú môžeme ihneď používať.

Použitie triedy v kóde

Knižnica dá k dispozícii triedu Markdown s jedinou metódou:

public void transform(Reader in, Writer out) throws ParseException {

S týmto môžeme radostne previesť Markdown zdroják na HTML. Do aktivity teda dodajme metódu:

public String toHtml(String markdown) {
    try {
        StringReader input = new StringReader(markdown);
        StringWriter output = new StringWriter();

        Markdown markdownConverter = new Markdown();
        markdownConverter.transform(input, output);

        return output.toString();
    } catch (ParseException e) {
        return "Syntax error";
    }
}

Následne budeme potrebovať dve úpravy:

@Override
public void onSourceChanged(String source) {
    ...
    previewFragment.setHtmlSource(toHtml(source));
}

a úpravu pri dynamickom vytváraní fragmentu:

private void showPreviewPane() {
    ...
    String htmlSource = toHtml(sourceEditText.getText().toString());
    ...
}