Po pewnej przerwie udało mi się napisać ostatni post z cyklu o Android (przynajmniej na jakiś czas).
Kilka słów komentarza do pewnych “odkryć”:
tranzakcje - zmiany w UI rejestrowane są jako tranzakcje, co więcej możemy je dodać do stosu back, co pozwala nam symulować zmianę stanu tej samej aktywności, jakby to była nawigacja pomiędzy różnymi ekranami. Brawo! Takiego gotowego API nie widziałem jeszcze na platformach znanych przeze mnie wcześniej.
fragmenty - było już o nich zdaje się niejeden już raz, mam mieszane uczucia co do niektórych rozwiązań. Np. widział ktoś by onClick w XML fragmentu nie mógł działać jak należy, tylko próbował coś wywoływać na aktywności? Podobnie opakowywanie obiektów wizualnych w klasy fragmentów wydają się komplikacją (np. Dialog w DialogFragment). Pewnie wynika to z zaszłości, kiedy nie było fragmentów… Cykl życia fragmentu w aktywności też jest dość złożony. Obfituje w sporą liczbę callbacków, trzeba wiedzieć, co wywoła się w jakiej kolejności we fragmencie, a co w aktywności. Owszem mamy może pewną optymalizację dla wydajności przy wpinaniu i wypinaniu fragmentu do aktywności, ale mam wrażenie jakby użytkownik platformy poznawał jej trzewia, rozróżniając trzy stany istnienia fragmentu. Aha, nieobowiązkowe nadpisywanie layoutu w niektórych miejscach własnymi zasobami XML kojarzy mi się z kontrolkami XAML obsługującymi szablony. Powiązania fragmentu z ActionBar też czasami dość przekombinowane, a nawigacja wg. listy dropdown czy zakładek wymaga sporo powtarzającego się za każdym razem kodu.
Podstawy
Zastosowania
- adaptatywność UI
- zakładki
- okna dialogowe
- customizacja ActionBar
- …
Obsługa kliknięcia przycisku we fragmencie
- onClick - handler musiałby być w aktywności
- implementacja interfejsu OnClickListener na klasie fragmentu + jawne powiązanie fragmentu z każdym przyciskiem - lepiej
public class XFragment extends Fragment implements View.OnClickListener {
…
@Override
public View onCreateView(…) {
…
Button button = (Button) theView.findViewById(R.id.button1);
button.setOnClickListener(this);
return theView;
}
public void myButtonClickHandler(View view) {
}
public void onClick(View view) {
myButtonClickHandler(view);
}
}
Tranzakcje fragmentów
Fragmenty mogą być dynamicznie zarządzane przez aktywność
- zmiany w UI bez opuszczania aktywności
- FragmentTransactions - wszystkie zmiany muszą zachodzić w tranzakcji
FragmentManager fm = getFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
…
ft.commit();
Dynamiczne dodawanie fragmentów do aktywności
- tworzymy instancję fragmentu
- występują wszystkie callbacki fragmentu przy inicjalizacji/wyświetlaniu
- równoważne z <fragment> w layout
XFragment frag = new XFragment();
FragmentManager fm = getFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.add(R.id.myGroup, frag, “xxxtag”);
ft.commit();
Dynamiczne usuwanie fragmentów z aktywności
- potrzebujemy referencji na usuwany fragment (FragmentManager.findFragmentByTag/findFragmentById)
- instancja usuniętego fragmentu nie może być już używana w aktywności
…
ft.remove(frag);
…
Zastępowanie fragmentów
YFragment diffFrag = new YFragment();
FragmentManager fm = getFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.myGroup, diffFrag);
ft.commit();
Trzy rozłączne poziomy egzystencji fragmentu
- obiekt Java
- powiązanie z aktywnością
- wyrenderowane UI
Attach/Detach
UI fragmentu może być zarządzane niezależnie od relacji z aktywnością
- Użyteczne dla częstego przesuwania fragmentów (unikamy narzutu pełnego demontowania i ustawiania)
- FragmentTransaction.detach(fragment)
- demontaż UI
- instancja fragmentu pozostaje nienaruszona i powiązana z aktywnością (można też nadal ją wyszukać przez FragmentManager)
- otrzymywane callbacki: onPause, onStop, onDestroyView
- nieotrzymywany callback: onDetach
- FragmentTransaction.attach(fragment)
- rekonstrukcja UI
- otrzymywane callbacki: onCreateView, onActivityCreated, onStart, onResume
- nieotrzymywany callback: onAttach
Tranzakcje fragmentów i przycisk back
- domyślnie tranzakcje nie są świadome przycisku back
- tranzakcje mogą być umieszczone na stosie back
- addToBackStack tworzy nowy wpis (można wrócić do poprzedniego stanu naciskając back)
- musi to być wywołane przed zatwierdzeniem tranzakcji
- opcjonalnie możemy dołączyć nazwę dla wspisu
…
ft.addToBackStack(“ScreenX”); //jeśli chcemy programowo cofnąć się do tego wpisu
…
Programowa nawigacja po stosie back
- FragmentManager.popBackStack() - symulacja naciśnięcia back
- FragmentManager.popBackStack z nazwą ekranu - powrót do danej tranzakcji
fm.popBackStack(“ScreenX”, 0); //powrót do wskazanego ekranu
fm.popBackStack(“ScreenX”, POP_BACK_STACK_INCLUSIVE); //powrót do ekranu poprzedzającego wskazany
Dostęp do stosu back za pomocą FragmentManager
- notyfikacje o zmianach w stosie
- implementujemy interfejs Fragment.OnBackStackChangedListener
- przekazujemy implementację do addBackStackChangedListener
- dostęp do wpisów stosu
- getBackStackEntryCount - liczba wpisów
- getBackStackEntryAt(index) - pobranie wpisu o podanym index (na szczycie: getBackStackEntryCount() - 1)
public class XActivity extends Activity implements FragmentManager.OnBackStackChangedListener {
…
@Override
public void onCreate(…) {
…
FragmentManager fm = getFragmentManager();
fm.addOnBackStackChangedListener(this);
}
…
public void onBackStackChanged() {
}
}
Cykl życia fragmentów i specjalizacja
inicjalizacja:
- callbacki cyklu życia takie jak dla aktywności
- onCreate, onStart, onResume
- każdy callback aktywności wołany przed callbackiem fragmentu o tej samej nazwie
- callbacki specyficzne dla ustawienia/wyświetlenia fragmentu
- onAttach - otrzymuje referencję na aktywność, która zawiera fragment
- onCreateView
- onActivityCreated - zakończenie tworzenia aktywności zawierającej fragment
- callbacki aktywności dla ustawienia fragmentu
- onAttachFragment - za każdym razem, gdy fragment jest wpinany do aktywności
- Activity: onCreate
- Fragment: onAttach
- Activity: onAttachFragment
- Fragment: onCreate
- Fragment: onCreateView
- Fragment: onActivityCreated
- Activity: onStart
- Fragment: onStart
- Activity: onResume
- Fragment: onResume
dla każdego fragmentu: 2-5
demontaż:
- callbacki znane z aktywności
- onPause, onStop, onDestroy
- każdy callback aktywności wywoływany po callbacku o takiej samej nazwie
- callbacki specyficzne dla niszczenia fragmentu
- onDestroyView - widok zwracany przez onCreateView nie jest już przypięty do fragmentu
- onDetach - fragment nie jest już przypięty do widoku
Rodzaje fragmentów
- Fragment
- ListFragment
- WebViewFragment
- DialogFragment
ListFragment
- automatyczne wyświetlenie danych z ListAdapter
- SimpleAdapter - dla tablic i podobnych (np. ArrayAdapter)
- SimpleCursorAdapter - dla danych używających kursora
- przy domyślnym layout nie ma potrzeby nadpisywania onCreateView
- ListFragment dostarcza domyślny widok
Definiowanie layoutu dla elementów listy
- zasób layout
- konstruktory SimpleAdaptor i SimpleCursorAdaptor przyjmują resource id
Modyfikacja wyglądu listy
- definicja layout przy pomocy zasobu
- umieszczenie ListView z id android:list
- opcjonalne umieszczenie TextView z id android:empty
- załadowanie layoutu w onCreateView
<LinearLayout …>
<ListView android:id=”@id/android:list” … />
<TextView android:id=”@id/android:empty” …/>
</LinearLayout>
WebViewFragment
- wrapper na WebView
- niewielka własna funkcjonalność
- getWebView() - dostęp do zawartego w sobie WebView
public class XWebFragment extends WebViewFragment {
…
public void onActivityCreated(…) {
…
WebView webView = getWebView();
webView.loadData(htmlString, “text/html”, null);
}
}
DialogFragment
- możliwość podania w onCreateView własnego layout przez zasób
- wyświetlenie za pomocą metody show
- będzie wewnętrznie zarządzać tranzakcją fragmentów, jeśli zostanie przekazany FragmentManager
- może być zmiksowany jako część zarządzanej przez aplikację tranzakcji fragmentów
- musi być wywołana jako ostatnia w tranzakcji, ponieważ wewnętrznie wywołuje commit
- używanie styli i tematów
- ustawienie musi się odbyć w onCreate
- setStyle
- style
- STYLE_NORMAL - domyślna ramka z tytułem
- STYLE_NO_TITLE - domyślna ramka bez tytułu
- STYLE_NO_FRAME - brak domyślnej ramki i tytułu, onCreateView tworzy layout
- STYLE_NO_INPUT - tak jak STYLE_NO_FRAME, ale nie otrzymuje wejścia
- tematy pochodzą ze standardowych zasobów
- android.R.style.Theme_Holo
- android.R.style.Theme_Holo_Light_Dialog
- obiekt Dialog jest automatycznie tworzony
- dostępny za pomocą metody getDialog()
- tworzony jest przed callbackiem onCreateView (możemy wywołać tam getDialog())
- dostępna jest większość zachowań Dialog
- setTitle(“xxx”)
- setCancelableOnTouchOutside(false) - każde wejście idzie do dialogu
- istniejące klasy Dialog mogą być opakowane w DialogFragment
- nadpisanie callbacka onCreateDialog
- wywoływany po onCreate i przed onCreateView
- najczęściej używamy tego do opakowania wbudowanej klasy AlertDialog
- może być używany jako normalny fragment
- musi używać layout z onCreateView
- onCreateDialog nie jest wywoływane
- getDialog zwraca null
public class XDialogFragment extends DialogFragment implements View.OnClickListener {
@Override
public View onCreateView(…) {
View theView = inflater.inflate(R.layout.xdialogfragment, container, false);
View yesButton = theView.findViewById(R.id.btnYes);
yesButton.setOnClickListener(this);
yesButton.requestFocus();
…
return theView;
}
public void onClick(View view) {
int viewId = view.getId();
switch(viewId) {
case R.id.btnYes:
break;
case R.id.btnNo:
break;
}
dismiss();
}
}
Wyświetlenie
FragmentManager fm = getFragmentManager();
XDialogFragment fragment = new XDialogFragment();
// fragment.show(fm, “XDialog”)
FragmentTransaction ft = fm.beginTransaction();
fragment.show(ft, “XDialog”);
//nie wywołujemy commit (dialog w środku sam wywołuje)
Opakowywanie innej klasy Dialog
public YDialogFragment extends DialogFragment implements DialogInterface.OnClickListener {
public Dialog onCreateDialog(Bundle state) {
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
builder.setMessage(“abc ?”);
builder.setPositiveButton(“OK”, this);
builder.setNegativeButton(“Cancel”, this);
Dialog theDialog = builder.create();
theDialog.setCanceledOnTouchOutside(false);
return theDialog;
}
public void onClick(DialogInterface dialogInterface, int i) {
switch(i) {
case Dialog.BUTTON_POSITIVE:
break;
case Dialog.BUTTON_NEGATIVE:
break;
}
}
}
Użycie jako standardowy fragment
W onCreateView sprawdzamy czy getDialog nie zwraca null i wykonujemy operacje na obiekcie dialogu tylko jeśli jest.
FragmentManager fm = getFragmentManager();
XDialogFragment fragment = new XDialogFragment();
FragmentTransaction ft = fm.beginTransaction();
ft.add(android.R.id.content, fragment, “XDialog”);
ft.addToBackStack(“xxx”);
ft.commit();
Fragmenty i ActionBar
Opcje dodawane do ActionBar przez fragment
- Metoda onOptionsItemSelected jest wywoływana najpierw w aktywności. Jeśli zwróciła ona false, to jest wywoływany jej odpowiednik na fragmencie.
Nawigacja pomiędzy różnymi fragmentami
- lista - lista dropdown dostępnych fragmentów
- zakładki - oddzielna zakładka dla każdego fragmentu, framework sam zarządza tranzakcjami
public class ActivityListNavigation extends Activity implements ActionBar.OnNavigationListener {
…
public void onCreate(…) {
…
String[] listMembers = { “option 1”, “option 2” };
ActionBar actionBar = getActionBar();
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
ArrayAdapter<String> list = new ArrayAdapter<String>(this, R.layout.simple_spinner_dropdown_item, listMembers);
actionBar.setListNavigationCallbacks(list, this);
}
public boolean onNavigationItemSelected(int i, long l) {
Fragment fragment = null;
switch(i) {
case 0:
fragment = new Fragment1();
break;
case 1:
fragment = new Fragment2();
break;
}
FragmentManager fm = getFragmentManager();
FragmentTransaction ft = fm.beginTransaction();
ft.replace(R.id.content, fragment);
ft.commit();
return true;
}
}
public class ActivityTabbed extends Activity implements ActionBar.TabListener {
…
Fragment _fragment1;
Fragment _fragment2;
public void onCreate(…) {
…
ActionBar actionBar = getActionBar();
actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS);
createTab(actionBar, “xxx”);
createTab(actionBar, “yyy”);
}
private void createTab(ActionBar actionBar, String displayName) {
ActionBar.Tab newTab = actionBar.newTab();
newTab.setText(displayName);
newTab.setTabListener(this);
actionBar.addTab(newTab);
}
public void onTabSelected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) {
CharSequence displayName = tab.getText();
if (displayName.equals(“xxx”)) {
if (_fragment1 == null) {
_fragment1 = new Fragment1();
fragmentTransaction.add(R.id.content, _fragment1);
}
else {
fragmentTransaction.attach(_fragment1);
}
} else if (displayName.equals(“yyy”)) {
}
}
public void onTabUnselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) {
CharSequence displayName = tab.getText();
if(displayName.equals(“xxx”)) {
fragmentTransaction.detach(_fragment1);
} else if (displayName.equals(“yyy”)) {
}
}
public void onTabReselected(ActionBar.Tab tab, FragmentTransaction fragmentTransaction) {
}
}