Hlavní navigace

Nekonečný list pomocí RecyclerView a Microstream (Android)

Článek chce provést čtenáře návrhem jednoduché aplikace pro Android, která ukáže téměř nekonečný list záznamů. Ukážeme i propojení vytvořeného listu s perzistentní vrstvou, což umožní uložit záznamy do databáze.
Zdeněk Jonáš
Doba čtení: 6 minut

Sdílet

Článek si neklade za cíl vytvoření tutoriálu, který lze copy paste přenést a ani nechce jenoduše překládat dokumentaci uvedenou na stránkách developer.android.com . Článek chce vysvětlit základní princip fungování RecyclerView komponenty a předkládá k tomu zároveň vzorový fungující projekt v Githubu.

Zároveň předpokládá, že čtenář už má určité zkušenosti s vývojem v Javě. Minimální znalosti by měly zahrnovat: vím co je to package, stream api, už jsem někdy pracoval s maven/gradle a umím použít git. Vím, co jsou to závislosti knihoven, tranzitivní závislosti atd.

Android studio lze stáhnout na developer.android.com/studio/. Je založeno na IntelliJ. Pro zjednodušení našeho článku nebudeme aplikaci vytvářet od začátku, ale použijeme již fungující kód přímo z GitHubu.

Pokud potřebujeme na Androidu zobrazit dlouhý list různých položek, máme obecně dvě možnosti, použít list view a nebo recycler view. List view se hodí pro zobrazení omezeného počtu dat, protože všechny položky načte po inicializaci ihned do paměti. Takže pokud je zapotřebí zobrazit 10 položek, je to ideální volba. Pokud dopředu nevíme, kolik těchto položek bude, nebo jsou ty položky samy o sobě velké, například obsahují obrázek a chceme je postupně načítat z webu/paměti, pak je recycler view právě to, co k tomu potřebujeme.

Recycler View

RecyclerView je list po sobě jdoucích úseků (view), které jsou před zobrazením načítány a po zobrazení uvolňovány z paměti, jak ilustruje následující obrázek.


RecycleView potřebuje vytvořit následující:

  • vytvořit item-view, které se bude opakovaně zobrazovat,
  • vytvořit obrazovku s komponentou Recycler View,
  • vytvořit strukturu objektů ve kterých budou uloženy informace,
  • vytvořit holder, který bude mapovat data na aktuální item-view,
  • vytvořit Adapter, který bude načítat konkrétní data z Databáze a pomocí Holderu je bude mapovat na item-view a následně uvolňovat z paměti data pro již shlédnuté elementy, uložení dat do DB

V tomto článku není vložen kód přímo, pracuji s projektem v Githubu. Má to mít tu výhodu, že tento projekt tam budu udržovat funkční i nadále a pokud bych nějaký kod vložil přímo sem, nemůžu garantovat, že bude na za dva a více let fungovat. Android je neustále ve vývoji a tak nemůžeme vyloučit, že api, které využijeme, nebude v následujících verzích změněno.

Layout

Layout je struktura objektů zobrazených na displeji telefonu, zapsaných pomocí XML. Pro náš příklad potřebujeme dva layouty:

  • layout pro item-view: využíváme zde ConstraintLayout, vytvoříme tak malou vizitku pro každého našeho zákazníka. Toto view bude zobrazeno za sebou v nekonečném listu,
  • layout pro obrazovku: vložíme RecyclerView na celou obrazovku.

Abychom mohli recyclerView použít, potřebujeme vytvořit Adaptér pro tuto obrazovku, která obsahuje tzv. Holder.
Holder je třída odpovědná za mapování aktuálních dat pro jednotlivá item-view. Adaptér je třída, která obsluhuje celé view a pro jednotlivá item-view volá Holder. Adaptér implementuje RecyclerView.Adapter<Textends ViewHolder>. V tomto případě implementujeme následující metody:

  • onCreateViewHolder – vytvoří instanci Holderu pro konkrétní item-view. V tomto případě máme pouze jeden typ item-view. (lze mít i více),
  • onBindViewHolder – nové view se nachází kousek před zobrazením a je zapotřebí do něj nahrát data. View dostane holder jako parametr a zároveň dostane pozici v seznamu. Díky tomu ví, zda přijde například 99. nebo již stá položka,
  • onViewRecycled – view opustilo obrazovku, můžeme uvolnit data s ním spojená z paměti,
  • getItemCount – zde dáme maximální počet polí, co chceme zobrazit. Můžeme dát bez problémů 15 000, uděláme to tak, že množství nebude pro nás problém.

Vytvoření dat

V reálném světě bychom informace o našich zákaznících stahovali například někde z našeho zabezpečeného úložiště. Pro dnešní příklad si tato data vygenerujeme. K tomu použijeme knihovnu známou ze světa jazyka Python. Její klon pro Javu se jmenuje JavaFaker. Vložíme tedy do souboru build.gradle novou závislost:

implementation 'com.github.javafaker:javafaker:0.18'

Tato knihovna je určena pro vytváření testovacích dat. Krátký pseudokód ilustruje, jak knihovna funguje:

Faker faker = new Faker();
customer = new Customer();
customer.setFirstName(faker.name().firstName());
customer.setLastName(faker.name().lastName());
customer.setCity(faker.address().city());
customer.setStreet(faker.address().streetName());
...

Persistence dat

Sice bychom si mohli data neustále vytvářet, ale abychom dotáhli náš příklad alespoň trošku do reálného světa, budeme si již vytvořené zákazníky ukládat. K tomu použijeme Microstream nativní Java persistence databázi, která nám umožní ukládat Java třídy přímo, bez nutné konverze. Microstream je velmi zajímavý projekt, který umožňuje ukládání všech Java tříd přímo a výhoda pro Android je, že neobsahuje žádné další tranzitivní závislosti, tudíž náš výsledný APK soubor nenaroste do obludných rozměrů.

Vložíme do našeho build.gradle další dvě závislosti:

implementation 'one.microstream:storage.embedded:02.01.00-MS-GA'
implementation 'one.microstream:storage.embedded.configuration:02.01.00-MS-GA'

a do projektového build.gradle vložíme microstream repository:

allprojects {
    repositories {
        google()
        jcenter()
        maven {
            url 'https://repo.microstream.one/repository/maven-public/'
        }
    }
}

Tato databáze ukládá celý strom Java tříd, potřebuje proto od nás, abychom jí řekli, který objekt je pro ni kořenem (root). Jelikož máme pouze list našich zákazníků, vytvoříme si další třídu, která bude tvořit tento root.

public class CustomerRoot {
    private Map<Integer, Lazy<Customer>> customerMap;

    public CustomerRoot() {
        customerMap = new HashMap<>();
    }

    public Map<Integer, Lazy<Customer>> getCustomerMap() {
        return customerMap;
    }
}

Třída bude obsahovat HashMapu, která nám lehce umožní získat naše zákazníky přes klíč. Jelikož potřebujeme po zhlédnutí záznamu uvolnit opět naši paměť a při načítání nechceme ihned načíst všechny objekty do paměti, použíjeme techniku Lazy. Ta nám umožní načíst data do paměti v momentě, kdy je budeme potřebovat, a máme potom možnost je opět z paměti uvolnit a v paměti uchovat pouze referenci, která zabírá pouze několik bajtů.

Repository

Repository je pro nás třída, která se bude starat o otevření databáze, načtení všech údajů a v našem případě i o vygenerování nových zákazníků.

Použití lze vidět ve třídě CustomerRepository. Repository je naše třída, co se stará o načítání zákazníků. Obsahuje metodu findCustomerById(Integer id), která buďto najde existujícího zákazníka a nebo ho vytvoří.

Konstruktor třídy bude vypadat takto:

public CustomerRepository(Context context) {
    Path filesDir = context.getFilesDir().toPath();
    storage = EmbeddedStorage.start(customerRoot, filesDir);
    faker = new Faker();
}

Konstruktoru předáme jako parametr context, aby mohli najít úložiště dat pro naši aplikaci. Dále otevřeme Microstream storage, kterému předáme náš root a cestu k místu, kde si data přejeme fyzicky uložit, a následně vytvoříme instance fakeru.

Dále do tohoto repository přidáme tři metody:

Tento kód si prohlédněte na Githubu. Nemá smysl zde opisovat jednotlivé řádky, zaměříme se pouze na ty důležité.

Lazy lazyCustomer = customerRoot.getCustomerMap().get(id);

Tímto získám lazy instanci našeho zákazníka, která je buďto null, pokud zákazník nebyl nalezen, a nebo není null, pokud zákazník nalezen v databázi byl. V tomto případě stačí zavolat get() a instance se načte z paměti.

customer = lazyCustomer.get();

Pokud je null, vytvoříme si instanci novou a pomocí fakeru vytvoříme data, která potom uložíme do storage.

if (lazyCustomer != null) {
    customer = lazyCustomer.get();
} else {
     customer = new Customer();
     customer.setFirstName(faker.name().firstName());
    …
     customerRoot.getCustomerMap().put(id, Lazy.Reference(customer));
     storage.store(customerRoot.getCustomerMap());
)

Uvolnění objektu z paměti je opět relativně jednoduché. Zavoláme nad lazy instancí jednoduše clear(). Clear uvolní data pouze z paměti, v databázi zůstávají, takže pokud zavolám znovu get(), dostanu opět uložené hodnoty.

tip_Kubernetes

public void clearLazyRefence(Integer customerId) {
    Lazy lazy = customerRoot.getCustomerMap().get(customerId);
    if (lazy != null) {
        lazy.clear();
    }
}

A nakonec, metoda pro zavření storage obsahuje jednu řádku, a to:

storage.shutdown();

Závěr

Článek si kladl za cíl, provést vás vzorovým projektem v GitHubu a ukázat vám princip fungování RecyclerView na Androidu a databázi Microstream vhodnou pro podobná řešení.