Android alkalmazásfejlesztés 3. rész: egy hasznos példa

A következőkben néhány hasznos dolgot fogok nektek bemutatni egy egyszerű, mégis komplex példán keresztül. – írta: Divinity, 6 éve

Fragments - Sliding tabs

Mielőtt belekezdenénk ebbe a részben, tudom ajánlani a Material Design oldalán megtalálható Tabs fejezetet, hogy tisztában legyen mindenki azzal, mire is álmodták meg ezt.

Ahogy a második részben meglett említve, a fragmentek az actvivity-ket bontják részekre. Tehát a mi esetünkben egy központi activity (a példában MainActivity) és két fragment (a példában DialerFragment és ContactsFragment) reprezentálja az alkalmazás alapfelépítését.

A jobb átláthatóság érdekében célszerű az activity-ket és a fragmenteket külön szervezni a kód többi részétől. Jelen esetben egy presenter package fogja tárolni ezt a három osztályt. Az alap generált MainActivity már megvan, így azt átmozgatjuk, majd létrehozzuk a fent említett 2 új osztályt.
A fragment létrehozásakor egyből kiválaszthatjuk az ős osztályt, ami a Fragment osztály az android.support.v4.app packageből. Az IDE két lehetőséget fog kínálni a Fragment osztályra, de a fent említettet kell választani. Az android.app csomagban lévő Fragment osztály már deprecated, így a support library által adottat kell használni.

Miután mind a két fragmentet legeneráltuk, készíteni kell hozzájuk egy-egy layoutot is. Először csak egy-egy alap layout-ot csinálunk, majd ahogy haladunk előre, bővítjük a dolgot.
res/layout → new/Layout Resource File

Itt is maradhat minden az alapon, amit az IDE ajánl, mind a két layout file számára. Ha legeneráltuk őket, akkor a megkülönböztetés érdekében egy-egy TextView-al kiegészítjük őket.
Erre mindössze annyi miatt van szükség, hogy majd meg tudjuk őket különböztetni a már működő tabokon.

Most, hogy készen vannak a layoutok, ideje kicsit visszakanyarodni a fragment osztályokhoz. Az IDE alapvetően egy-egy üres osztályt generált mindössze az ős osztály beállításával. Ezt három dologgal kell kiegészíteni. Szükség van egy konstruktorra, valamint felül kell írni az onCreate és az onCreateView metódusokat. A számunkra fontos dolog az onCreateView-ban történik, itt állíthatjuk be, hogy melyik fragment melyik layoutot használja.
Ez így néz ki:

package hu.logout.example.divinity.phone.presenter;

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

import hu.logout.example.divinity.phone.R;

public class ContactsFragment extends Fragment {

public ContactsFragment() {}

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}

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

}

Ezt természetesen a másik fragmentnél is el kell játszani, mindössze az onCreateView nem az R.layout.contacts_layout-ot fogja inflatelni, hanem a dialer_layout-ot. Tehát a return ágba ez kerül: return inflater.inflate(R.layout.dialer_layout, container, false);

Ekkorra eljutottunk odáig, hogy van két külön fragment két külön layouttal. Jó kezdés de messze még a cél. Ideje váltani a fragment-eket összefogó activity-re. Mielőtt nekilátunk, most van szükség felhasználni az elkészített új stílust. Az AndroidManifest.xml-ben az adott activity-hez felveszünk egy új pontot, mégpedig az android:theme attribútumot az új action bar nélküli stílus értékével.

Mehetünk is tovább, folytatjuk az összefogó layouttal. A MainActivity layoutja az osztály onCreate metódusából könnyen megtudható. Ennek valahogy így kell kinéznie:

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

Tehát ebben az esetben az activity_main névre hallgató file-t kell keresni. Ebben a file-ban alapvetően van egy gyökér elem és egy TextView (az IDE által generálva). Egyikre se lesz szükségünk.
Gyökér elemnek egy speciális layout-ot, a CoordinatorLayout-ot használjuk. Ez a FrameLayout egy áttervezett megvalósítása, lényegében az ilyen és ehhez hasonló esetekben használjuk. Egy konténer, ami több speciális interakcióval bíró view-t tartalmaz.

<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".presenter.MainActivity">
...
</android.support.design.widget.CoordinatorLayout>

Megvan a root, már csak a tartalom hiányzik. Elsősorban tab-okat kell definiálni. Alapvetően egy AppBarLayout fogja közre a tabokat. Ez lényegében egy vertikális linear layout, amit speciálisan erre a feladatra készítettek fel.

<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
...
</android.support.design.widget.AppBarLayout>

Ha csak bemásoljuk ezt a kódot, akkor az android:paddingTop-ra nyílni fog az IDE, mivel nem találja a megadott értéket. Ezen máris segítünk!
A dimen egy olyan fájl, amiben előre definiált dimenziókat, értékeket tárolunk. Legyen az margó, képméret, betűméret stb. Ehhez létrehozzuk az adott resource file-t.

Majd ebben a fájlban felveszünk egy értéket, ezek után így kell kinéznie:

<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="appbar_padding_top">8dp</dimen>
</resources>

Máris nem sír az IDE és mehetünk tovább! (Természetesen kézzel is beírhattuk volna az értéket, de nagyobb alkalmazásoknál már a külső resource fájlokat célszerű használni, nem pedig beégetni ezeket az értékeket.)
Szükség van a tartalom tárolására ebben az esetben, pontosabban a fragmentek tárolására alkalmas view-ra is. Ez a ViewPager.

<android.support.v4.view.ViewPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />

Összeállítva az egész layout-ot valahogy így kell kinéznie:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".presenter.MainActivity">

<android.support.design.widget.AppBarLayout
android:id="@+id/appbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/appbar_padding_top"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">

<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?android:attr/actionBarSize"
android:layout_weight="1"
android:background="@color/colorPrimary"
app:layout_scrollFlags="scroll|enterAlways"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light"
app:title="@string/app_name">
</android.support.v7.widget.Toolbar>

<android.support.design.widget.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content">
</android.support.design.widget.TabLayout>

</android.support.design.widget.AppBarLayout>

<android.support.v4.view.ViewPager
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />

</android.support.design.widget.CoordinatorLayout>

Ha lefuttatjuk az alkalmazást, azt tapasztalhatjuk, hogy nem történik semmi. Ez így is van jól, hisz még csak az alapműködést raktuk össze, nem lettek összekötve a dolog. Ezt már az adott activity-ben kell megtennünk!
Kezdetben szükségünk van két változóra. Egy a TabLayout-nak, valamint egy a ViewPager-nek.

private TabLayout tabLayout;
private ViewPager viewPager;

Az onCreate metódusban be is állítjuk őket a megszokott módon, valamint összelinkeljük a tabLayout-ot a viewPager-rel:

viewPager = findViewById(R.id.container);
tabLayout = findViewById(R.id.tabs);
tabLayout.setupWithViewPager(viewPager);

Következő lépésként definiálni kell egy belső osztályt, ami a FragmentPagerAdapter leszármazottja. Majd ennek az osztálynak felül kell írni a getItem, a getCount és a getPageTitle metódusát. Majd pedig egy addFragment metódust hozunk neki létre, amivel beállíthatjuk a hozzá tartozó fragmenteket.
Kezdjük az elején! Két dologra van szükség az osztályon belül. Két listára, amik tartalmazzák a fragmenteket, valamint a fejlécüket.

class ViewPagerAdapter extends FragmentPagerAdapter {
private final List<Fragment> fragmentList = new ArrayList<>();
private final List<String> fragmentTitleList = new ArrayList<>();

Létrehozzuk a konstruktort:

public ViewPagerAdapter(FragmentManager manager) {
super(manager);
}

Majd pedig a felülírandó metódusok. Értelemszerűen a getItem visszaadja az adott fragment-et, tehát egyszerűen return fragmentList.get(position);. A getCount se igényel sokkal több gondolkodást: return fragmentList.size();. Majd pedig a getPageTitle szintén adja magát: return fragmentTitleList.get(position);
Összefoglalva:

@Override
public Fragment getItem(int position) {
return fragmentList.get(position);
}

@Override
public int getCount() {
return fragmentList.size();
}

@Override
public CharSequence getPageTitle(int position) {
return fragmentTitleList.get(position);
}

Most pedig a saját függvényük jön, ami szintén kitalálható a nevéből, mire lesz jó:

public void addFragment(Fragment fragment, String title) {
fragmentList.add(fragment);
fragmentTitleList.add(title);
}

Ha ezzel megvagyunk, már csak be kell állítani a viewPagert a frissen létrehozott adapterünkhöz.

private void setupViewPager(ViewPager viewPager) {
ViewPagerAdapter adapter = new ViewPagerAdapter(getSupportFragmentManager());
adapter.addFragment(new DialerFragment(), "Dialer");
adapter.addFragment(new ContactsFragment(), "Contacts");
viewPager.setAdapter(adapter);
}

Ha megvan, akkor ez is hívható az onCreate metódusban és kész is az első nagy lépés a teljes alkalmazáshoz!

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

viewPager = findViewById(R.id.container);

tabLayout = findViewById(R.id.tabs);
tabLayout.setupWithViewPager(viewPager);
setupViewPager(viewPager);
}

A cikk még nem ért véget, kérlek, lapozz!

Előzmények