Uvod
Fragment je modularni deo aktivnosti, koji ima svoj životni ciklus, prima sopstvene ulazne događaje, možete ga dodati ili ukloniti dok se aktivnost pokreće. Fragment objedinje View i logiku tako da se može jednostavno višekratno koristiti unutar jedne ili više različitih aktivnosti. U aktivnostima može biti više od jednog fragmenta tako da svaki fragment može da predstavlja neki View unutar jedne aktivnosti.
Prednost arhitekture koja koristi fragmenate je što nam fragmenti omogućavaju ponovnu upotrebu koda, sa jednostavnim postupkom pravljenja različitih prikaza za tablete (landscape) i mobilne uredjaje.
Komunikacija izmedju dva fragmenata je prilično komplikovana i može izvesti na dva načina:
- Preko zajedničke aktivnosti koristeći listener patern (objašnjeno u članaku: “Kreiranje custom listenera u Androidu”)
- Koristeći zajedničiki ViewModel (objašnjeno u članku “Komunikacija izmedju fragmenata koristeći ViewModel”)
Da bi u okviru fragmenta mogli da koristimo metodu
Fragmenat vs. Aktivnost
U aplikacijama koje koriste fragmente deo zaduženja aktivnosti se delegira u fragmente, pa bi prema toj podeli zaduženja koja ostaju u aktivnosti bi bila sledeća:
- Da sadrži navigaciju do drugih aktivnosti putem intent-a ili navigacijske komponente (“NavigationDrawer”,“ViewPager”…)
- Da skriva i prikazuje fragmenate (pomoću menadžera fragmenata)
- Da prima podatake iz drugih aktivnosti (intent)
- Da kumunicira sa fragmentima i posreduje komunikaciji između njih
Dok fragmenti preuzimaju obavezu da:
- Prikazuju odgovarajući sadržaj
- Event handling
- Pokretanje network request-a
- Preuzimanje i čuvanje podataka
Životni ciklus fragmenta direktno je pod uticajem životnog ciklusa aktivnosti. Kada je aktivnost pauzirana, onda su i svi su fragmenti u njoj pauzirani, a kada je aktivnost uništena, onda su i svi njeni fragmenti uništeni. Međutim, dok je aktivnost “živa”, možemo manipulisati sa svakim fragmentom nezavisno. Detaljan prikaz lifecycle fragmenta i aktivnosti možete pogledati ovde.
NAPOMENA:
Kada implementirate neku metodu životnog ciklusa fragmenta uvek treba da pozovete nadklasu (npr. super.onStart();):
Primer
1 2 3 4 5 |
@Override public void onStart() { super.onStart(); // naš kod } |
Kreiranje fragmenta
Extendovanje fragment klase
Kreiranje fragmenta se sastoji iz kreiranja odgovarajuće klase zadužene za logiku i pridodajući joj odgovarajući layout. Klasa zadužena za logiku mora da extenduje neku od sledećih klasa:
- Fragment je glavna klasa dok su ostale njegove podklase (pogledajte kako izgleda boilerplate kod koji generiše android studio).
- DialogFragment – Fragment koji prikazuje prozor za dijalog, koji lebdi na vrhu prozora njegove aktivnosti. Ovo se obično koristi za prikazivanje dijaloga upozorenja, dijaloga za potvrdu ili za traženje informacija od korisnika u okviru bez potrebe za prebacivanjem na drugu aktivnost, dozvoljavajući korisniku da se vrati na prethodni fragment.
- PreferenceFragmentCompat se koristi za kreiranje settings liste za našu aplikaciju odkle korisnici imaju mogućnost da promene funkcionalnost i ponašanje aplikacije. (pročitajte više u dokumentaciji).
- ListFragment se koristi za prikazivanje liste nekih podataka i ima već implementiran event listener na clik nekog člana iz liste, tako da je potrebno samo da definišemo metod onListItemClick() (primer).
Povezivanje fragmenta sa njegovim layout-om
Da biste obezbedili layout za fragment, morate da implementirate “onCreateView()” metodu, koju Android poziva kada dođe vreme da fragment iscrta svoj layout. Implementacija ovog metoda mora da vrati prikaz koji je root layout vašeg fragmenta. Povezivanje fragment klase i njenog view-a se vrši korišćenjenm inflate() metode u sklopu lifecycle metode onCreateViewa:
1 2 3 4 5 6 7 |
public static class NekiFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) { // Inflate the layout for this fragment return inflater.inflate(R.layout.example_fragment, container, false); } } |
Pogledajte više o postupku ubacivanja view-a iz xml-a u klasu “Layout inflation” u članku “Konvertovanje XML resursa u View objekat”.
Pored ovog načina možemo da kreiramo fragment na osnovu androidovog template-a pod nazivom Fragment(Blank) u čijem sklopu dolazi dosta pripremljeno boilerplate koda. Više o ovome pogledajte ovde.
Targetiranje elemenata u okviru layout-a
Da bi u okviru fragmenta mogli da koristimo metodu findViewById() potrebno je da prvo targetiramo njegov layout. To možemo uraditi na dva načina u zavisnosti gde nam u kodu treba:
-
U okviru metoda onCreate() i onCreateView() prvo je potrebno da inflate-ujemo layout, nakon čega možemo da koristimo metodu findViewById():
12345678910@Overridepublic View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {rootView =inflater.inflate(R.layout.fragment_fragment_d, container, false);TextView text1 = rootView.findViewById(R.id.tvParametar1);TextView text2 = rootView.findViewById(R.id.tvParametar2);return rootView;}Fragment za razliku od aktivnosti nije podklasa klase Context, što znači da nema pristup globalnim informacijama o okruženju aplikacije. To znači da fragment ne može da koristi this za dobijanje context-a. Iz tog razloga u sklopu onCreateView() metode je kao parametar prosledjen objekat LayoutInflater koji ima pristup kontekstu, te stoga ako nam je potreban context možemo njega da iskoristimo inflater.getContext().
-
U okviru metode onViewCreated() targetiramo fragmentov layout sa metodom getView(). Ova metoda je dostupna kada ekstendujemo našu klasu sa klasom Fragment i može da se pozove samo nakon kreiranja view-a, te je stoga ne možemo koristiti unutar onCreate() ili onCreateView() metode.
1234@Overridepublic void onViewCreated(View view, @Nullable Bundle savedInstanceState) {TextView title = (TextVIew) getView().findViewById(R.id.nekiId);}
Embendovanje fragmenta u aktivnostima
Aktvnost koja sadrži fragment mora da ekstenduje ili FragmentActivity ili njenu potklasu AppCompatActivity. Fragmente u aktivnost možemo da dodamo na dva načina:
- a) Statično ubacivanje fragmenta direktno u layout aktivnosti
- b) Programirano ubacivanje fragmenta u postojeću ViewGroup-u
a) Statično ubacivanje fragmenta direktno u layout aktivnosti
Statično ubacivanje fragmenta u aktivnost podrazumeva da se fragment ubaci u layout aktivnosti kao view.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="match_parent"> <fragment android:name="com.example.FragmentA" android:id="@+id/list" android:layout_weight="1" android:layout_width="0dp" android:layout_height="match_parent" /> <fragment android:name="com.example.FragmentB" android:id="@+id/viewer" android:layout_weight="2" android:layout_width="0dp" android:layout_height="match_parent" /> </LinearLayout> |
Fragment ubačen na ovaj način mora da ima definisan id u okviru XML-a, jer se preko njega targetira u okviru aktivnosti koristeći metodu findFragmentById():
1 2 3 4 5 6 7 |
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState == null) { NekiFragment nekiFragment = (NekiFragment)getSupportFragmentManager().findFragmentById(R.id.nekiFragment); } } |
Kada imamo referencu na fragment onda možemo da pozivamo njegove public metode ili properties-e.
b) Programirano ubacivanje fragmenta u aktivnost
Za programirano ubacivanje fragmenta u aktivnost je postupak sledeći:
1.) Kreiranje fragment kontejnera u XML-u
Ako vaša aktivnost dozvoljava da se fragmenti uklone i zamene, trebalo bi da dodate početni fragment (tzv. fragmentKontejner) u “onCreate()”. Taj kontejner se koristi da u njega možemo kasnije da ubacimo neki drugi fragment.
1 2 3 4 |
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/fragment_container" android:layout_width="match_parent" android:layout_height="match_parent" /> |
2.) Kreiranje fragmentManager-a
Fragment manager se instancira pozivajući metodu getSupportFragmentManager()
1 |
FragmentManager fragmentManager = getSupportFragmentManager(); |
Fragment manager
Fragment manager je objekat koji zadužen za rad sa fragmentima za navigaciju izmedju njih kao i referenciranje fragmenta u okviru aktivnosti:
Metod | Opis |
---|---|
addOnBackStackChangedListener | Dodaje listener kada dodje do promene back stack-a (više o ovome pogledajte ovde) |
beginTransaction() | Kreira novu transakciju. |
findFragmentById(int id) | Nalazi fragment koji je inflated-ovan direktno u XML layout aktivnosti. |
findFragmentByTag(String tag) | Nalazi fragment preko tag-a |
popBackStack() | Uklanja fragment sa backstack-a. |
executePendingTransactions() | Forsira izvršenje transakcije. |
3.) Kreiranje instance fragmenta
Kreiranje instance fragmenta je standardno korišćenjem new operatora:
1 |
NekiFragment nekiFragment = new NekiFragment(); |
Ukoliko želimo da prosledimo i neke podatke pri instanciranju onda oni moraju biti definisani u okviru konstruktorske metode:
1 2 3 4 5 |
public NekiFragment(int nekiBroj) { Bundle args = new Bundle(); args.putInt("nazivIntArgumenta", nekiBroj); setArguments(args); } |
Pa ih onda prosledjujemo kao parametar:
1 |
NekiFragment nekiFragment = new NekiFragment(nekiBroj); |
Preporuka je da se koristi tzv. newInstance pristup, kada se u okviru fragmenta definiše factory metoda koja služi za instaciranje fragmenta:
1 2 3 4 5 6 7 8 9 10 |
public static NekiFragment newInstance(int nekiBroj, string nekiTekst) { NekiFragment myFragment = new NekiFragment(); Bundle args = new Bundle(); args.putInt("nazivIntArgumenta", nekiBroj); args.putString("nazivStringArgumenta", nekiTekst); myFragment.setArguments(args); return myFragment; } |
Kasnije u fragmentu okviru metode onCreate() možemo da zatražimo podatke generisane pri instanciranju fragmenta upravo preko naziva argumenta:
1 2 |
getArguments().getInt("nazivIntArgumenta", 0); getArguments().getString("nazivStringArgumenta", ""); |
Drugi argument u get-eru je defaultna vrednost u slučaju da ne nadje argument.
4.) Izvršavanje neke od transakcija sa fragmentima (add, remove, replace)
Potoji API za rad sa fragmentima u aktivnosti (add, remove, or replace a fragment) i zove se FragmentTransaction. Instancu FragmentTransaction dobijamo koristeći fragmentManager.
1 |
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction(); |
Jedna od metoda koju može da izvrši FragmentTransaction je i add(). Ova metoda služi za ubacivanje našeg fragmenta u sklop nekog ViewGroup-a (kontejner). Taj ViewGroup se definiše kroz prvi parametar, dok se kroz drugi parametar definiše fragment koji dodajemo, a kroz treći parametar definišemo TAG koji će da obeleži dodati fragment (na osnovu taga kasnije možemo da ga targetiramo sa fragmentManager.findFragmentByTag(“TAGFRAGMENTA”);).
1 |
fragmentTransaction.add(R.id.fragment_container, nekiFragment, "TAGFRAGMENTA"); |
Da bi se klikom na “back” dugme vratili na prethodno stanje (tj. da ne bi smo zatvorili aktivnost što je podrazumevano) potrebno je da dadamo naš fragment na stack tzv. “backStack” sa metodom addToBackStack().
1 |
transaction.addToBackStack("TAGFRAGMENTA"); |
Po definisanju svih naredbi (može da ih bude više od jedne) je potrebno potvrditi akciju (commit-ovati), nakon čega će one biti i stvarno izvršene:
1 |
fragmentTransaction.commit(); |
Akcije koje smo commit-ovali se ne izvršavaju odmah već ih stavljaju na čekanje za izvršavanje na glavnoj niti (main thread). Akcije će se izvršiti tek kada nit bude spremna. Pogledajte primere transakcija ovde.
Targetiranje fragmenta iz aktivnosti
Za targetiranje fragmenta u okviru aktivnost koji je ubačen na ovaj način se koristi metoda findFragmentByTag() kojoj se prosledjuje parametar TAG (definisan kroz treći parametar metode replace()).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState == null) { // Dinamičko dodavanje fragmenta u frame container getSupportFragmentManager().beginTransaction(). replace(R.id.flContainer, new NekiFragment(), "NEKITAGKOJIOBELEZAVAFRAGMENT"). commit(); // Sada možemo da potražimo fragment preko tog TAG-a NekiFragment nekiFragment = (NekiFragment) getSupportFragmentManager().findFragmentByTag("NEKITAGKOJIOBELEZAVAFRAGMENT"); } } |
Navigacija izmedju fragmenata može biti definisana u sklopu aktivnosti koristeći samo FragmentManager, medjutim ništa nas ne sprečava da koristimo neki od sledećih pristupa:
Kreiranje od pripremljnog boilerplate “Fragment(Blank)”
Factory statička metoda newInstance() se koristi pri instanciranju fragmenta u okviru aktivnosi i omogućava jednostavno prosledjivanje parametara pri instanciranju fragmenta:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// TODO: Rename parameter arguments, choose names that match private static final String ARG_PARAM1 = "param1"; private static final String ARG_PARAM2 = "param2"; // TODO: Rename and change types of parameters private String mParam1; private int mParam2; // TODO: Rename and change types and number of parameters public static FragmentD newInstance(String param1, int param2) { FragmentD fragment = new FragmentD(); Bundle args = new Bundle(); args.putString(ARG_PARAM1, param1); args.putInt(ARG_PARAM2, param2); fragment.setArguments(args); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { mParam1 = getArguments().getString(ARG_PARAM1); mParam2 = getArguments().getInt(ARG_PARAM2); } } |
Sada u okviru fragmenta možemo da koristimo prosledjene parametre koristeći promenjive mParam1 i mParam2 a instanciranje Fragmenta u aktivnosti se vrši sa sledećim kodom:
1 |
Fragment fragment = NekiFragment.newInstance("Dragoljub", 45); |
NAPOMENA:
Ako je fragment podklasa ListFragment-a, kod njega se po default-u vraća ListView u metodi onCreateView(), tako da ne mora da se implementira deo vezan za povezivanje logike sa layout-om osim ako ne koristimo custom layout.
U sklopu boilerplate koda dolazi deo vezan za CustomListener koji se koriste za prosledjivanje podataka iz fragmenta u aktvinost (drugi fragment).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
// Interfejs public interface OnFragmentInteractionListener { // TODO: Update argument type and name void onFragmentInteraction(Uri uri); } private OnFragmentInteractionListener mListener; // Primer okidanja dogadjaja (može da se obriše i prilagodi svom zadatku // TODO: Rename method, update argument and hook method into UI event public void onButtonPressed(Uri uri) { if (mListener != null) { mListener.onFragmentInteraction(uri); } } //Setovanje listenera u okviru aktivnosti i obezbedjivanje da aktivnost mora da implementira interfejs @Override public void onAttach(Context context) { super.onAttach(context); if (context instanceof OnFragmentInteractionListener) { mListener = (OnFragmentInteractionListener) context; } else { throw new RuntimeException(context.toString() + " must implement OnFragmentInteractionListener"); } } @Override public void onDetach() { super.onDetach(); mListener = null; } |
Više o custom listener-ima pogledaj te u članku “Kreiranje custom listenera”.
Primer – ListFragment
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
public class MyListFragment extends ListFragment implements OnItemClickListener { @Override public View onCreateView(LayoutInflater inflater,ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.list_fragment, container, false); return view; } @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); ArrayAdapter adapter = ArrayAdapter.createFromResource(getActivity(), R.array.Planets, android.R.layout.simple_list_item_1); setListAdapter(adapter); getListView().setOnItemClickListener(this); } @Override public void onItemClick(AdapterView<?> parent, View view, int position,long id) { Toast.makeText(getActivity(), "Item: " + position, Toast.LENGTH_SHORT).show(); } } |
Primer
U ovome primeru je prikazano definisanje početnog fragmenta u aktivnost. U prazan tag kontejnera se dodajemo novi view naš fragment.
1 2 3 4 5 6 7 8 9 10 11 |
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); FragmentManager manager = getSupportFragmentManager(); FragmentTransaction transaction = manager.beginTransaction(); Fragment fragmentC = new FragmentC(); transaction.add(R.id.kontejner, fragmentC, "C"); transaction.commit(); } |
Primer
U ovome primeru pozivajući metodu showFragmentA dolazi do zamene fragmenta
1 2 3 4 5 6 7 |
public void showFragmentA (View view){ Fragment fragmentA = new FragmentA(); transaction = manager.beginTransaction(); transaction.replace(R.id.kontejner, fragmentA, "A"); transaction.addToBackStack("A"); transaction.commit(); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 |
import android.content.Context; import android.net.Uri; import android.os.Bundle; import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; /** * A simple {@link Fragment} subclass. * Activities that contain this fragment must implement the * {@link BlankFragmentA.OnFragmentInteractionListener} interface * to handle interaction events. * Use the {@link BlankFragmentA#newInstance} factory method to * create an instance of this fragment. */ public class BlankFragmentA extends Fragment { // TODO: Rename parameter arguments, choose names that match // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER private static final String ARG_PARAM1 = "param1"; private static final String ARG_PARAM2 = "param2"; // TODO: Rename and change types of parameters private String mParam1; private String mParam2; private OnFragmentInteractionListener mListener; public BlankFragmentA() { // Required empty public constructor } /** * Use this factory method to create a new instance of * this fragment using the provided parameters. * * @param param1 Parameter 1. * @param param2 Parameter 2. * @return A new instance of fragment BlankFragmentA. */ // TODO: Rename and change types and number of parameters public static BlankFragmentA newInstance(String param1, String param2) { BlankFragmentA fragment = new BlankFragmentA(); Bundle args = new Bundle(); args.putString(ARG_PARAM1, param1); args.putString(ARG_PARAM2, param2); fragment.setArguments(args); return fragment; } @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (getArguments() != null) { mParam1 = getArguments().getString(ARG_PARAM1); mParam2 = getArguments().getString(ARG_PARAM2); } } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { // Inflate the layout for this fragment return inflater.inflate(R.layout.fragment_blank, container, false); } // TODO: Rename method, update argument and hook method into UI event public void onButtonPressed(Uri uri) { if (mListener != null) { mListener.onFragmentInteraction(uri); } } @Override public void onAttach(Context context) { super.onAttach(context); if (context instanceof OnFragmentInteractionListener) { mListener = (OnFragmentInteractionListener) context; } else { throw new RuntimeException(context.toString() + " must implement OnFragmentInteractionListener"); } } @Override public void onDetach() { super.onDetach(); mListener = null; } /** * This interface must be implemented by activities that contain this * fragment to allow an interaction in this fragment to be communicated * to the activity and potentially other fragments contained in that * activity. */ public interface OnFragmentInteractionListener { // TODO: Update argument type and name void onFragmentInteraction(Uri uri); } } |
Back Stack
Stack je tip memorije u koji se smeštaju elementi jedan na drugi, tako da poslednji dodati element je na vrhu (analogija sa gomilom tanjira). Elementi se uklanjaju sa stacka obrnutim redosledom, tako što se poslednje dodati element uklanja prvi. U okviru Androida postoji stack u koji se smeštaju sve aktivnosti prema redosledu pozivanja. Pritiskanjem “back” dugmenta poslednja aktivnost sa stacka se briše. Medjutim u slučaju korišćenja fragmenta to nije defaultno ponašanje pa kada korisnik pritisne back sa stacka ne ukloni poslednje dodati fragment već cela aktivnost. To nije očekivano ponašanje te je potrebno je da tu funkcionalnost programer “ručno” doda.
addToBackStack
Da bi se i fragmenti ubacili na backStack potrebno je da se dodaju koristeći addToBackStack() metodu. Ovu metodu je potrebno dodati pre svakog commit-a. Tekst koji se prosledjuje je opcioni i koristi se ako kasnije želimo da prepoznamo tu transakciju, medjutim sasvim je ok da se prosledi i null kao parametar.
1 2 3 4 |
FragmentTransaction fts = getSupportFragmentManager().beginTransaction(); fts.replace(R.id.flContainer, new FirstFragment()); fts.addToBackStack("optional tag"); fts.commit(); |
FragmentManager.OnBackStackChangedListener
Ako aplikacija sa promenom fragmenata treba da ažurira i druge elemente korisničkog interfejsa (npr. actionBar) to znači da bi trebalo se reaguje nakon promene backStack-a. U tom slučaju je potrebno da se iskoristi interfejs addOnBackStackChangedListener i da se definiše callback metoda onBackStackChanged() koja treba da izvrši akciju nakon okidanja dogadjaja
Primer kada aktivnost implementira interfejs
1 2 3 4 |
@override public void onBackStackChanged() { // Ovde ide kod za update UI-a } |
Primer kada se callback metoda onBackStackChanged() definie “on the fly”:
1 2 3 4 5 6 |
getSupportFragmentManager().addOnBackStackChangedListener( new FragmentManager.OnBackStackChangedListener() { public void onBackStackChanged() { // Ovde ide kod za update UI-a } }); |