Android Architecture Components: Navigation

Jest to cykl artykułów poświęcony komponentom architektury Androida. Wszystkie artykuły znajdziesz na tej stronie.

Wiele współczesnych aplikacji korzysta z fragmentów oraz jednej aktywności. Takie zaprojektowanie aplikacji jest dobre, ale stanowi pewne problemy. Wraz z rozwojem aplikacji musimy zwrócić uwagę na takie elementy jak: transakcji fragmentów, przekazywanie argumentów w między komponentami, obsługa nawigacji w górę, w tył, radzenie sobie z tylnym stosem i wreszcie testowanie nawigacji w aplikacji. Czy możemy uprościć ten proces? Opisywałem te problemy już na blogu w powyższych linkach. W tym artykule spojrzymy na to wszystko z innej strony wykorzystując do tego bibliotkę Navigation.

Android Architecture Components: Navigation

Komponenty architektury nawigacji upraszczają implementację nawigacji w aplikacji na system Android. Ma to na celu pomagać w poruszaniu się między miejscami docelowymi. Biblioteka została zaprojektowany z myślą o koncepcji aplikacji Single Activity App (jedna aktywność — wiele fragmentów). Nawigacja odnosi się do interakcji, która umożliwia użytkownikom poruszanie się po różnych częściach aplikacji.

Biblioteka Navigation zapewnia wiele korzyści, ponieważ wspiera takie działania jak:

  • Transakcje fragmentów.
  • Obsługę przycisków wstecz.
  • Animację i przejścia.
  • Obsługę głębokiego linkowania.
  • Safe Args — wtyczka Gradle, która zapewnia bezpieczne przekazywania danych między miejscami docelowymi.

Przejdźmy do praktyki

Na początku stworzymy jedną aktywność i dwa fragmenty. Po otwarciu aktywności wyświetli nam się fragment (ListNews) z listą np.: artykułów. Po kliknięciu dowolny element zostaniemy przeniesieni do drugiego fragmentu (Details).

W pierwszej kolejności potrzebujemy wykresu nawigacji. Aby go utworzyć, klikamy (Android Studio) prawym przyciskiem myszy na folder res, a następnie New -> Android Resource File -> Resource Type -> Navigation.Wykres naszej nawigacji będzie wyglądał następująco:

<?xml version="1.0" encoding="utf-8"?>
<navigation 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"
    app:startDestination="@id/listNews">
    <fragment
        android:id="@+id/listNews"
        android:name="net.myenv.architecturecomponents.Navigation.ListNews"
        android:label="News"
        tools:layout="@layout/fragment_list_news">
        <action
            android:id="@+id/goToDetails"
            app:destination="@id/details" />
    </fragment>
    <fragment
        android:id="@+id/details"
        android:name="net.myenv.architecturecomponents.Navigation.Details"
        android:label="Details"
        />
</navigation>

Czym jest ten wykres nawigacyjny? To plik zawierający zestaw miejsc docelowych i odpowiednich działań. Aplikacja może mieć jeden lub więcej wykresów nawigacyjnych.

Ponadto możesz użyć Edytora nawigacji w Android Studio, aby przeglądać i edytować swoje wykresy nawigacyjne.

Mamy 2 fragmenty (nasze miejsca docelowe) i jedną akcję we fragmencie ListNews. Każdy cel reprezentuje ekran, do którego można nawigować. Domyślnie komponent zawiera obsługę aktywności i fragmentów.

Miejsca docelowe mają następujące atrybuty:

  • android:id — unikalna nazwa zasobu dla tego miejsca docelowego.
  • android:name — ścieżka do klasy.
  • android:label — tytuł, który będzie wyświetlany na pasku akcji lub w edytorze nawigacyjnym.
  • tools:layout — podgląd miejsca docelowego, który jest używany przez edytor.

W sekcji action mamy atrybut id app:destination — mówi on nam, do którego punktu ma przejść aplikacja po wykonaniu odpowiedniej akcji, np.: po naciśnięciu przycisku.

Zwróć uwagę również na atrybut app:startDestination w sekcji navigation, który określa, który fragment będzie początkowym miejscem docelowym (czyli ten, który wyświetli się po uruchomieniu aplikacji).

Ok, mamy wykres nawigacyjny, teraz musimy go podłączyć do naszej aktywności, aby wszystko działało. Najpierw musimy dodać NavHostFragment do pliku activity_main.xml. W nim będziemy mieć dwa nowe atrybuty:

  • app:navGraph — określa, który wykres nawigacyjny będzie powiązany z hostem nawigacyjnym.
  • app:defaultNavHost — jeśli ustawimy na true, host nawigacyjny przechwyci przycisk Wstecz. Aby upewnić się, że przycisk wstecz działa poprawnie, musisz także nadpisać metodę onSupportNavigateUp() w aktywności.
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_basic_navigation);
    }
    @Override
    public boolean onSupportNavigateUp() {
        return Navigation.findNavController(this, R.id.nav_host_fragment).navigateUp();
    }
}
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <fragment
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/basic_navigation" />
</LinearLayout>
public class ListNews extends Fragment {
    public ListNews() {
        // Required empty public constructor
    }
    @SuppressLint("WrongViewCast")
    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_list_news, container, false);
        ListView list = view.findViewById(R.id.listView1);
        ArrayAdapter<String> adapter = new ArrayAdapter<String>(getActivity(), android.R.layout.simple_list_item_1, getList());
        list.setAdapter(adapter);
        list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
            @Override
            public void onItemClick(AdapterView<?> adapterView, View view, int i, long l) {
                Navigation.findNavController(view).navigate(R.id.goToDetails);
            }
        });
        return view;
    }
    private List<String> getList() {
        List<String> newsList = new ArrayList<>();
        for (int x=0;x<20;x++){
            newsList.add("Title "+x);
        }
        return newsList;
    }
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".Navigation.ListNews">
    <ListView
        android:id="@+id/listView1"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        />        <!--/>-->
</LinearLayout>
public class Details extends Fragment {
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_details, container, false);
        return view;
    }
    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
    }
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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">
    <TextView
        android:id="@+id/textview_details"
        android:layout_width="395dp"
        android:layout_height="wrap_content"
        android:text="Fragment Details"
        android:textSize="30dp"
        android:gravity="center"
        app:layout_constraintBottom_toTopOf="parent"
        app:layout_constraintTop_toBottomOf="parent"
        app:layout_constraintLeft_toRightOf="parent"
        app:layout_constraintRight_toLeftOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>

Dzięki komponentowi nawigacyjnemu nie ma już potrzeby ręcznej obsługi transakcji fragmentów, Aby przejść z fragmentu X do fragmentu Y, wystarczy wywołać odpowiednią akcję.

Android Architecture Components Navigation example

Przekazywanie danych między celami

Jeżeli chcielibyśmy przekazać dane między fragmentami, to wystarczy skorzystać z Bundle(). Spójrz na przykład jak przekazać pozycję klikniętego elementu listy. W pierwszej kolejności budujemy obiekt Bundle i przekazujemy go do miejsca docelowego za pomocą funkcji navigate():

list.setOnItemClickListener(new AdapterView.OnItemClickListener() {
    @Override
    public void onItemClick(AdapterView<?> adapterView, View view,
 int position, long id) {
        Bundle args = new Bundle();
        args.putInt("idNews", position);
        Navigation.findNavController(view).navigate(R.id.goToDetails, args);
    }
});
public class Details extends Fragment {
    private TextView textView;
    private int id;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            id = getArguments().getInt("idNews");
        }
    }
    @Nullable
    @Override
    public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_details, container, false);
        textView = view.findViewById(R.id.textview_details);
        return view;
    }
    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        textView.setText("Fragment Details for: "+String.valueOf(id) );
    }
}

Natomiast w klasie Details pobierz argument za pomocą metody getArguments(). Następnie pobraną wartość wyświetlamy.

Istnieje również inny sposób na przekazywanie danych. Jeżeli będziesz chciał przekazać stałą wartość (która zawsze będzie taka sama) to wystarczy, że to odpowiednio zaimplementujesz w kodzie XML, a dokładniej w wykresie nawigacji. Poniżej przykładowy kod, w którym przekazujemy argument o nazwie klucza „name”. Oprócz tego możemy ustawić kilka atrybutów – są intuicyjne więc nie ma potrzeby ich szczegółowo omawiać.

 <fragment
        android:id="@+id/listNews"
        android:name="net.myenv.architecturecomponents.Navigation.ListNews"
        android:label="News"
        tools:layout="@layout/fragment_list_news">
            <argument
                android:name="name"
                android:defaultValue="value"
                app:argType="string"
                app:nullable="false"
                />
    </fragment>

Aby odebrać argument w docelowym miejscu korzystasz jak wyżej przedstawiono z metody getArguments(). Zwróć uwagę, że wartość atrybutu app:argType zaczyna się zmałej litery. Gdybyś ustawił to na „String” otrzymałbyś błąd, który wiele nie mówi. Błąd wskazuje, że masz problem w pliku xml aktywności, co nie jest prawdą.

Zmiana tytułu na pasku akcji

Umiemy tworzyć już poszczególne miejsca docelowe i przechodzić między nimi. Natomiast gdy przechodzimy z jednego ekranu do drugiego, nie zmienia się nam tytuł na pasku akcji. W domyślnej konfiguracji tytuł to nazwa aplikacji. Aby zmienić to zachowanie, musimy wykonać prosty kod w aktywności, a dokładniej w metodzie onCreate().

Gdy korzystasz z Actionbar’a, użyj takiego kodu:

NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
NavigationUI.setupActionBarWithNavController(this, navController);
onSupportNavigateUp();

Metoda onSupportNavigateUp() jest wymagana, aby obsłużyć powrót do poprzedniego ekranu za pomocą strzałki, która znajduje się u góry ekranu.

Jeżeli korzystasz z Toolbar\a, skorzystaj z takiego kodu:

NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
Toolbar toolbar = findViewById(R.id.toolbar);
NavigationUI.setupWithNavController(toolbar, navController);

Pamiętaj, aby nie wywoływać i nie przysłaniać metody onSupportNavigateUp() w przypadku korzystania z Toolbar’a.

Aby tytuł wyświęcił się poprawnie, musisz mieć ustawiony atrybut android:label dla poszczególnych fragmentów w Twoim wykresie.

Android Architecture Components: Navigation toolbar

Obsługa przycisków w pasku akcji

Może zdarzyć się tak, że mamy na pasku akcji skróty, za pomocą których przechodzimy do innych ekranów na przykład do zakładki z zapisanymi artykułami. W jaki sposób możemy wykorzystać do tego celu bibliotekę Navigation?. Spójrz pierwsze na kod.

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item
        android:id="@+id/readLeter"
        android:icon="@android:drawable/btn_default"
        android:title="Read Leter" />
</menu>
<navigation>
...
    <fragment android:id="@+id/readLeter"
        android:label="Read leter"
        android:name="net.myenv.architecturecomponents.Navigation.ReadLeter"
        tools:layout="@layout/rea"/>
</navigation>
@Override
public boolean onOptionsItemSelected(MenuItem item) {
    super.onOptionsItemSelected(item);
    NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
    return NavigationUI.onNavDestinationSelected(item, navController);
}

Co my tutaj mamy? Na początku tworzymy plik menu, w którym umieszczamy skróty do innych fragmentów. W zakładce graph masz wycinek z pliku, który odpowiada za wykres nawigacji. W aktywności dodajemy, a dokładnie przysłaniamy metodę onOptionsItemSelected().

Pamiętaj, że identyfikatory muszą być nazwane tak samo. W przeciwnym wypadku biblioteka Navigation nie znajdzie odpowiednika i nie wykona żadnej akcji. Kontroler nawigacji zakłada, że ​​identyfikatory menu są takie same jak identyfikatory miejsc docelowych i dlatego wie, co robić, gdy element zostanie wybrany.

Inne widoki

Oprócz Toolbar’a i ActionBarar’a możemy obsługiwać za pomocą biblioteki Navigation takie komponenty jak:

Z czasem ta lista może się powiększać. Implementacja nie jest trudna. Podobna jest do wcześniejszego kodu z Toolbarem. Na przykład, gdybyś chciał skorzystać z paska dolnego to wystarczy, że w swoim XML z wyglądem aktywności dodasz BottomNavigationView, a po stronie kodu:

NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
BottomNavigationView bottomView = findViewById(R.id.bottom_navigation);
NavigationUI.setupWithNavController(bottomView, navController);

Podsumowanie

Korzystając z komponentu architektury Androida Navigation, redukujemy wiele kodu i zachowujemy całą naszą strukturę nawigacji w jednym scentralizowanym pliku XML, który jest znacznie łatwiejszy do zarządzania. Powyższy przykład nie przedstawia pełnych możliwości tej biblioteki. Dlatego bądź czujny i czekaj na kolejną porcję materiału 🙂

Co dalej?

  • Polub stronę MYENV na Facebooku oraz śledź mnie na Twitterze
  • Zachęcam do komentowania i pisania propozycji tematów, o których chcesz przeczytać
  • Poleć ten wpis za pomocą poniższych przycisków. Będę Ci za to bardzo wdzięczny 🙂
  • Życzę Ci miłego dnia i miłego kodowania 🙂