Dydaktyka / Programowanie urządzeń mobilnych 2

Manuskrypt HTML

Kontynuacja kursu mobilnego obejmująca nawigację, architekturę aplikacji, lokalne bazy danych, pobieranie danych z usług zewnętrznych, elementy Android Jetpack i wstrzykiwanie zależności.

Manuskrypt HTML

W01 - Organizacja kursu i nawigacja Compose

Podstawowe informacje o kursie

Organizacja kursu

Zajęcia obejmują łącznie 75 godzin pracy studenta, z czego 15 godzin przeznaczonych jest na wykłady (45 minutowy wykład na tydzień), a 30 godzin na zajęcia laboratoryjne. Dodatkowo przewidziano 30 godzin pracy własnej studenta, niezbędnej do samodzielnego opracowania materiału, przygotowania projektów oraz pogłębiania wiedzy. Kurs kończy się uzyskaniem 3 punktów ECTS.

W trakcie zajęć wykorzystywane są materiały dydaktyczne dostępne pod adresem: https://github.com/RafLew84/ProgUM. Obowiązkowa i zalecana literatura:

Zasady zaliczenia

  • Warunkiem zaliczenia laboratorium jest uzyskanie oceny pozytywnej z list zadań.
  • Na zajęcia przewidzianych jest 6 list zadań.
  • Z każdej listy wystawiana jest osobna ocena.
  • Nie jest konieczne zaliczenie wszystkich list aby otrzymać ocenę pozytywną z laboratorium. Dopuszczalne jest nieoddanie/niezaliczenie jednej listy - za tą listę otrzymuje się ocenę 2,0.
  • Każda lista posiada informację o liczbie punktów wymaganych na konkretną ocenę
  • Każda lista posiada termin zwrotu.
  • Za każdy tydzień opóźnienia otrzymana ocena jest obniżana o 1,0.
  • Listy oddawane są podczas zajęć laboratoryjnych.
  • Do każdej listy prowadzący zadaje 4 pytania.
  • Liczba punktów za listę jest przyznawana na podstawie poprawności wykonania zadań oraz odpowiedzi ustnej.
  • Ocena końcowa jest średnią arytmetyczną ze wszystkich ocen z list.
  • Na ocenę 3,0 wymagana jest średnia co najmniej 3,0.
  • Na zajęciach laboratoryjnych dopuszczalne są trzy nieobecności nieusprawiedliwione.

Treści Programowe

  1. Zasady zaliczenia, Treści Programowe, Zaawansowana Nawigacja.
  2. Wprowadzenie do Wielowątkowości: Coroutines. Wątek główny.
  3. Coroutines. Współbieżność, Równoległość, Asynchroniczność.
  4. Podstawy Architektury Aplikacji: Wzorce MVx (MVC, MVP, MVVM).
  5. Reaktywne Zarządzanie Stanem: Flow, StateFlow, SharedFlow.
  6. Zaawansowane Zarządzanie Stanem: withContext, StateIn, ShareIn, FlowOn, combine.
  7. Coroutines: Kanały - Asynchroniczna Wymiana Danych Między Coroutines.
  8. Zapis Danych do Pliku: SharedPreferences, DataStore.
  9. Baza Danych SQLite + ROOM: Entity, Dao, Database, CRUD, Operacje Asynchroniczne.
  10. Praca z Zewnętrznymi Źródłami Danych: Retrofit2, Operacje Asynchroniczne.
  11. Wstrzykiwanie Zależności: Dagger, Hilt.
  12. Czysta Architektura - Warstwa Domeny i Wzorzec Use Case.
  13. Wzorzec Single Source of Truth - Strategia Offline Caching.
  14. Backend w Chmurze: Wprowadzenie do Firebase i Firestore

Android Studio - Rozpoczęcie i konfiguracja projektu - Przypomnienie

Podczas zajęć tworzymy projekty w Android Studio, w Kotlinie, z interfejsem budowanym w Jetpack Compose (bez widoków XML). Najprościej zacząć od kreatora: New Project -> Empty Activity. Ustaw Language: Kotlin, Minimum SDK: 28+ i zaakceptuj ustawienia. Taki szablon tworzy gotowy projekt z poprawną konfiguracją Compose i przykładowym setContent { … }.

UWAGA!!! Nie używaj Empty Views Activity - to rozpoczyna projekt oparty o widoki, gdzie ui definiujemy jako pliki XML.

Dodawanie zależności

Wszystkie zależności dopisujemy w bloku dependencies { … } pliku build.gradle(.kts)(Module:App). Przykładowy plik:

UWAGA!!!! NIE KOPIUJ PLIKÓW KONFIGURACYJNYCH - zawierają informacje UNIKALNE DLA PROJEKTU !!!!

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlin.compose)
}

android {
    namespace = "com.example.test"
    compileSdk = 36

    defaultConfig {
        applicationId = "com.example.test"
        minSdk = 28
        targetSdk = 36
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_11
        targetCompatibility = JavaVersion.VERSION_11
    }
    kotlinOptions {
        jvmTarget = "11"
    }
    buildFeatures {
        compose = true
    }
}

dependencies {
    // tutaj dodajemy zależności

    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.lifecycle.runtime.ktx)
    implementation(libs.androidx.activity.compose)
    implementation(platform(libs.androidx.compose.bom))
    implementation(libs.androidx.ui)
    implementation(libs.androidx.ui.graphics)
    implementation(libs.androidx.ui.tooling.preview)
    implementation(libs.androidx.material3)
    testImplementation(libs.junit)
    androidTestImplementation(libs.androidx.junit)
    androidTestImplementation(libs.androidx.espresso.core)
    androidTestImplementation(platform(libs.androidx.compose.bom))
    androidTestImplementation(libs.androidx.ui.test.junit4)
    debugImplementation(libs.androidx.ui.tooling)
    debugImplementation(libs.androidx.ui.test.manifest)
}

UWAGA!!! Pamiętaj aby wykonać synchronizację (Rys. 1.1) projektu po jakiejkolwiek zmianie w plikach z katalogu gradle

Okno programu z widokiem na plik konfiguracyjny. Okno programu z widokiem na plik konfiguracyjny. Okno programu z widokiem na plik konfiguracyjny. Okno programu z widokiem na plik konfiguracyjny.
Rys. 1.1. Okno programu z widokiem na plik konfiguracyjny.

Szuflada Nawigacyjna

W tej części skupimy się na praktycznej implementacji nawigacji w aplikacji Jetpack Compose. Zakładając, znajomość podstawowych bloków konstrukcyjnych: NavHost i NavController, przyjrzymy się, jak zintegrować je z zaawansowanymi komponentami Material 3, takimi jak szuflada nawigacyjna.

Przeanalizujmy kod, który implementuje jeden z najczęstszych wzorców nawigacyjnych w aplikacjach mobilnych: szufladę nawigacyjną (znaną również jako hamburger menu)(Rys. 1.2).

Szuflada nawigacyjna. Szuflada nawigacyjna. Szuflada nawigacyjna. Szuflada nawigacyjna.
Rys. 1.2. Szuflada nawigacyjna.

Załączony kod to kompletna, choć minimalistyczna, aplikacja demonstrująca łączenie komponentów Jetpack Navigation, Scaffold oraz ModalNavigationDrawer. Przyjrzyjmy się jej kluczowym elementom krok po kroku.

Aby móc pracować z compose navigation musimy dodać odpowiednią zależność do projektu

dependencies {
    implementation("androidx.navigation:navigation-compose:2.9.2")
}

Pierwszym elementem jest centralizacja tras nawigacyjnych w obiekcie data object AppDestinations.

data object AppDestinations {
    const val HOME = "home"
    const val PROFILE = "profile"
    const val SETTINGS = "settings"
}

Centralizacja zapewnia bezpieczeństwo typów, ułatwia refaktoryzację (zmieniamy nazwę w jednym miejscu).

W aplikacji zagnieżdżamy kilku komponentów . Hierarchia wygląda następująco:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainApp() {
    val navController = rememberNavController()
    val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
    val scope = rememberCoroutineScope()

    ModalNavigationDrawer(
        drawerState = drawerState,
        drawerContent = {
            DrawerContent(navController = navController, drawerState = drawerState)
        }
    ) {
        Scaffold(
            topBar = {
                TopAppBar(
                    title = { Text("Aplikacja z Szufladą") },
                    navigationIcon = {
                        IconButton(onClick = {
                            scope.launch { drawerState.apply { if (isClosed) open() else close() } }
                        }) { Icon(Icons.Filled.Menu, contentDescription = "Menu") }
                    }
                )
            }
        ) { paddingValues ->
            NavHost(
                navController = navController,
                startDestination = AppDestinations.HOME,
                modifier = Modifier.padding(paddingValues)
            ) {
                composable(AppDestinations.HOME) { HomeScreen() }
                composable(AppDestinations.PROFILE) { ProfileScreen() }
                composable(AppDestinations.SETTINGS) { SettingsScreen() }
            }
        }
    }
}
  1. ModalNavigationDrawer: Jest to komponent najwyższego poziomu, który zarządza logiką pokazywania i ukrywania wysuwanej szuflady. Przyjmuje on dwa kluczowe parametry:
    • drawerState: Stan szuflady (otwarta/zamknięta).
    • drawerContent: Funkcja kompozycyjna definiująca zawartość samej szuflady (DrawerContent).
  2. Scaffold: Umieszczony wewnątrz ModalNavigationDrawer, Scaffold zapewnia standardową strukturę ekranu. W naszym przypadku używamy go do zdefiniowania topBar (górnego paska aplikacji).
  3. NavHost: Na końcu, wewnątrz Scaffold, umieszczamy nasz NavHost. To on zarządza faktyczną podmianą treści ekranu (HomeScreen, ProfileScreen itd.). Zwróćmy uwagę na kluczowe powiązanie: modifier = Modifier.padding(paddingValues). Przekazujemy tu wypełnienie (paddingValues) otrzymane ze Scaffold, co zapewnia, że nasza treść nie zostanie przysłonięta przez TopAppBar.

W funkcji MainApp zauważymy trzy kluczowe zmienne stanu.

val navController = rememberNavController()
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()

Pierwsze dwie są oczywiste: jedna zarządza stosem nawigacji, druga stanem szuflady. Ale dlaczego potrzebujemy scope?

Odpowiedź leży w naturze drawerState. Metody drawerState.open() i drawerState.close() są funkcjami zawieszającymi (suspend functions) - więcej o tych funkcjach w kolejnych rozdziałach. Nie można ich po prostu wywołać z dowolnego miejsca - muszą być uruchomione wewnątrz korutyny. Zmiana drawerState automatycznie spowoduje rekompozycję i wizualne otwarcie szuflady.

{scope.launch { drawerState.apply { if (isClosed) open() else close() } }})

Przejdźmy do zarządzania nawigację z wnętrza szuflady. W funkcji MainApp dodaliśmy ModalNavigationDrawer, który posiada parametr drawerContent - jako ten parametr przekazujemy funkcję DrawerContent, zajrzyjmmy do jej wnętrza:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DrawerContent(navController: NavController, drawerState: DrawerState) {
    val scope = rememberCoroutineScope()

    ModalDrawerSheet {
        Column(modifier = Modifier.padding(16.dp)) {
            Text("Menu", style = MaterialTheme.typography.headlineSmall)
            Spacer(modifier = Modifier.height(16.dp))

            NavigationDrawerItem(
                icon = { Icon(Icons.Default.Home, contentDescription = "Strona główna") },
                label = { Text("Strona główna") },
                selected = false,
                onClick = {
                    navController.navigate(AppDestinations.HOME)
                    scope.launch { drawerState.close() }
                }
            )
            NavigationDrawerItem(
                icon = { Icon(Icons.Default.Person, contentDescription = "Profil") },
                label = { Text("Profil") },
                selected = false,
                onClick = {
                    navController.navigate(AppDestinations.PROFILE)
                    scope.launch { drawerState.close() }
                }
            )
            NavigationDrawerItem(
                icon = { Icon(Icons.Default.Settings, contentDescription = "Ustawienia") },
                label = { Text("Ustawienia") },
                selected = false,
                onClick = {
                    navController.navigate(AppDestinations.SETTINGS)
                    scope.launch { drawerState.close() }
                }
            )
        }
    }
}

Wewnątrz ModalDrawerSheet definiujemy elementy będące wyświetlane w szufladzie jako NavigationDrawerItem. Jest to element zaprojektowany według tego samego wzorca (slot-based-layouts). Posiada zdefiniowane sloty w które można wstawić elementy (icon, label, divider), upraszczając tworzenie całego layoutu. Po kliknięciu elementu w szufladzie wykonujemy dwie akcje:

  • Nawigujemy: Wywołujemy navController.navigate(), aby zmienić zawartość NavHost.
  • Zamykamy szufladę: Uruchamiamy korutynę (scope.launch), aby wywołać drawerState.close().

Jak widzimy, nawigacja w Jetpack Compose to znacznie więcej niż tylko wywoływanie navController.navigate(). To przemyślana integracja kontrolera nawigacji z innymi komponentami interfejsu, takimi jak ModalNavigationDrawer. Istotnym elementem jest zrozumienie, jak zarządzać wieloma stanami (navController, drawerState) oraz jak obsługiwać asynchroniczne wywołania (funkcje suspend do otwierania/zamykania szuflady) za pomocą odpowiednich narzędzi, takich jak rememberCoroutineScope.

Na kolejnych wykładach zagłębimy się w korutyny, które są fundamentem działania tego przykładu.

Pełny Kod Przykładu

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            MyApplicationTheme {
                MainApp()
            }
        }
    }
}

data object AppDestinations {
    const val HOME = "home"
    const val PROFILE = "profile"
    const val SETTINGS = "settings"
}

@Composable
fun HomeScreen() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Text("Strona Główna", fontSize = 24.sp)
    }
}

@Composable
fun ProfileScreen() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Text("Profil Użytkownika", fontSize = 24.sp)
    }
}

@Composable
fun SettingsScreen() {
    Box(
        modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center
    ) {
        Text("Ustawienia", fontSize = 24.sp)
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MainApp() {
    val navController = rememberNavController()
    val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
    val scope = rememberCoroutineScope()

    ModalNavigationDrawer(
        drawerState = drawerState,
        drawerContent = {
            DrawerContent(navController = navController, drawerState = drawerState)
        }
    ) {
        Scaffold(
            topBar = {
                TopAppBar(
                    title = { Text("Aplikacja z Szufladą") },
                    navigationIcon = {
                        IconButton(onClick = {
                            scope.launch { drawerState.apply { if (isClosed) open() else close() } }
                        }) { Icon(Icons.Filled.Menu, contentDescription = "Menu") }
                    }
                )
            }
        ) { paddingValues ->
            NavHost(
                navController = navController,
                startDestination = AppDestinations.HOME,
                modifier = Modifier.padding(paddingValues)
            ) {
                composable(AppDestinations.HOME) { HomeScreen() }
                composable(AppDestinations.PROFILE) { ProfileScreen() }
                composable(AppDestinations.SETTINGS) { SettingsScreen() }
            }
        }
    }
}



@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DrawerContent(navController: NavController, drawerState: DrawerState) {
    val scope = rememberCoroutineScope()

    ModalDrawerSheet {
        Column(modifier = Modifier.padding(16.dp)) {
            Text("Menu", style = MaterialTheme.typography.headlineSmall)
            Spacer(modifier = Modifier.height(16.dp))

            NavigationDrawerItem(
                icon = { Icon(Icons.Default.Home, contentDescription = "Strona główna") },
                label = { Text("Strona główna") },
                selected = false,
                onClick = {
                    navController.navigate(AppDestinations.HOME)
                    scope.launch { drawerState.close() }
                }
            )
            NavigationDrawerItem(
                icon = { Icon(Icons.Default.Person, contentDescription = "Profil") },
                label = { Text("Profil") },
                selected = false,
                onClick = {
                    navController.navigate(AppDestinations.PROFILE)
                    scope.launch { drawerState.close() }
                }
            )
            NavigationDrawerItem(
                icon = { Icon(Icons.Default.Settings, contentDescription = "Ustawienia") },
                label = { Text("Ustawienia") },
                selected = false,
                onClick = {
                    navController.navigate(AppDestinations.SETTINGS)
                    scope.launch { drawerState.close() }
                }
            )
        }
    }
}

Zagnieżdźona Nawigacja

Przejdźmy do drugiego przykładu w którym zapoznamy się z ideą tworzenia zagnieżdżonych grafów nawigacyjnych.

Załóżmy, że nasza aplikacja ma więcej niż trzy proste ekrany. Prawie każda komercyjna aplikacja ma co najmniej dwa odrębne przepływy (flows):

  • Przepływ Uwierzytelniania: Logowanie, Rejestracja, Resetowanie Hasła.
  • Główny Przepływ Aplikacji: Ekran główny, Profil, Ustawienia, itd.

Problem polega na tym, że te dwa przepływy mają zupełnie inne zasady. Co najważniejsze: gdy użytkownik pomyślnie się zaloguje, powinien przejść do ekranu głównego, a cały przepływ uwierzytelniania powinien zniknąć z historii. Naciśnięcie przycisku Wstecz na ekranie głównym nie powinno cofać do ekranu logowania, lecz zamykać aplikację. Osiągnąć to można za pomocą zagnieżdżonej nawigacji.

Przeanalizujmy prosty przykład pokazujący takie rozwiązanie. Zamiast traktować NavHost jak jeden wielki kontener, traktujemy go jak folder, który może zawierać zarówno pojedyncze pliki (ekrany), jak i inne foldery (zagnieżdżone grafy).

Zamiast używać surowych ciągów znaków (Stringów) bezpośrednio w kodzie, dobrą praktyką jest stworzenie obiektu, który trzyma wszystkie trasy w jednym miejscu:

data object AppDestinations {
    // Grafy
    const val AUTH_GRAPH = "auth_graph"
    const val MAIN_APP_GRAPH = "main_app_graph"

    // Ekrany uwierzytelniania
    const val LOGIN = "login"
    const val REGISTER = "register"
    const val FORGOT_PASSWORD = "forgot_password"

    // Główne ekrany aplikacji
    const val WELCOME = "welcome"
    const val PROFILE = "profile"
}

Zauważmy, że nasza aplikacja będzie miała dwa główne podgrafy: AUTH_GRAPH i MAIN_APP_GRAPH

Spójrzmy na główny NavHost w SimpleNestedNavApp. Jest on uderzająco prosty:

NavHost(
    navController = navController,
    startDestination = AppDestinations.AUTH_GRAPH
) {
    // Graf uwierzytelniania (logowanie, rejestracja, itp.)
    authGraph(navController)

    // Główny graf aplikacji po zalogowaniu
    mainAppGraph(navController)
}

Zauważmy:

  1. startDestination nie jest ekranem. Jest to AUTH_GRAPH, czyli cały zagnieżdżony graf. Aplikacja uruchamia się, wchodząc do folderu uwierzytelniania.
  2. Wewnątrz NavHost nie ma ani jednego composable() definiującego ekran. Zamiast tego, są tylko dwie funkcje (authGraph i mainAppGraph), które definiują całe grupy ekranów.

Główny NavHost nie musi wiedzieć nic o ekranie logowania czy profilu; musi tylko wiedzieć o istnieniu przepływu uwierzytelniania i przepływu głównego.

Gdy aplikacja rośnie, NavHost staje się nieczytelny. Rozwiązaniem jest podział na zagnieżdżone grafy za pomocą funkcji rozszerzających dla NavGraphBuilder:

fun NavGraphBuilder.authGraph(navController: NavController) {
    navigation(
        startDestination = AppDestinations.LOGIN,
        route = AppDestinations.AUTH_GRAPH
    ) {
        composable(AppDestinations.LOGIN) {
            LoginScreen(navController) }
        composable(AppDestinations.REGISTER) {
            RegisterScreen(navController) }
        composable(AppDestinations.FORGOT_PASSWORD) {
            ForgotPasswordScreen(navController) }
    }
}

fun NavGraphBuilder.mainAppGraph(navController: NavController) {
    navigation(
        startDestination = AppDestinations.WELCOME,
        route = AppDestinations.MAIN_APP_GRAPH
    ) {
        composable(AppDestinations.WELCOME) {
            WelcomeScreen(navController) }
        composable(AppDestinations.PROFILE) {
            ProfileScreen(navController) }
    }
}

Analiza tego bloku jest kluczowa:

  • fun NavGraphBuilder.authGraph(...): To czysta organizacja kodu. Zamiast zaśmiecać NavHost, grupujemy logikę w oddzielnej funkcji.
  • navigation(...): To jest właściwy konstruktor zagnieżdżonego grafu.
  • route = AppDestinations.AUTH_GRAPH: Nadajemy całemu folderowi nazwę. Teraz możemy nawigować do niego.
  • startDestination = AppDestinations.LOGIN: Definiujemy, który ekran jest domyślny wewnątrz tego grafu.

Identyczną strukturę ma mainAppGraph, który grupuje ekrany WELCOME i PROFILE.

Mamy dwa oddzielne światy: AUTH_GRAPH i MAIN_APP_GRAPH. Jak przeskoczyć z jednego do drugiego i – co najważniejsze - posprzątać po sobie?

Spójrzmy na funkcję navigateToMainApp(), wywoływaną po pomyślnym logowaniu:

fun NavController.navigateToMainApp() {
    this.navigate(AppDestinations.MAIN_APP_GRAPH) {
        popUpTo(AppDestinations.AUTH_GRAPH) {
            inclusive = true
        }
    }
}

To jest najważniejszy fragment kodu w całym przykładzie. Rozbijmy go na części:

  • navigate(AppDestinations.MAIN_APP_GRAPH): Mówimy nawiguj do grafu głównego. NavController automatycznie skieruje nas do startDestination tego grafu (czyli WELCOME).
  • popUpTo(AppDestinations.AUTH_GRAPH): To jest polecenie sprzątające. Powraca na stosie aż znajdzie AUTH_GRAPH
  • inclusive = true: Po znalezieniu na stosie AUTH_GRAPH jest on również usuwany.

Prześledźmy wykonanie: Użytkownik klika Zaloguj. NavController następnie:

  • Znajduje na stosie powrotu graf AUTH_GRAPH.
  • Usuwa ze stosu LOGIN, REGISTER, FORGOT_PASSWORD i sam AUTH_GRAPH.
  • Dodaje na stos MAIN_APP_GRAPH (z ekranem WELCOME).

Stos powrotu jest czysty. Zawiera tylko MAIN_APP_GRAPH. Jeśli użytkownik naciśnie teraz przycisk Wstecz, nie wróci do ekranu logowania. Opuści aplikację. Osiągnęliśmy dokładnie taki przepływ, jakiego oczekują użytkownicy.

Zagnieżdżona nawigacja to podstawowe narzędzie do organizacji w każdej aplikacji, która ma więcej niż jeden logiczny przepływ. Jak widzieliśmy w kodzie, ten wzorzec zapewnia trzy kluczowe korzyści:

  1. Organizację: Grupuje powiązane ekrany.
  2. Modularność: Utrzymuje główny NavHost czysty i pozwala definiować przepływy w oddzielnych funkcjach.
  3. Kontrolę nad Stosem Powrotu: Umożliwia nawigowanie między całymi przepływami i usuwanie ich z historii jednym poleceniem.

Pełny kod przykładu

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            NestedComposeGraphTheme {
                SimpleNestedNavApp()
            }
        }
    }
}

data object AppDestinations {
    // Grafy
    const val AUTH_GRAPH = "auth_graph"
    const val MAIN_APP_GRAPH = "main_app_graph"

    // Ekrany autentykacji
    const val LOGIN = "login"
    const val REGISTER = "register"
    const val FORGOT_PASSWORD = "forgot_password"

    // Główne ekrany aplikacji
    const val WELCOME = "welcome"
    const val PROFILE = "profile"
}

@Composable
fun SimpleNestedNavApp() {
    val navController = rememberNavController()
    NavHost(
        navController = navController,
        startDestination = AppDestinations.AUTH_GRAPH
    ) {
        // Graf autentykacji (logowanie, rejestracja, itp.)
        authGraph(navController)

        // Główny graf aplikacji po zalogowaniu
        mainAppGraph(navController)
    }
}

fun NavGraphBuilder.authGraph(navController: NavController) {
    navigation(
        startDestination = AppDestinations.LOGIN,
        route = AppDestinations.AUTH_GRAPH
    ) {
        composable(AppDestinations.LOGIN) {
            LoginScreen(navController) }
        composable(AppDestinations.REGISTER) {
            RegisterScreen(navController) }
        composable(AppDestinations.FORGOT_PASSWORD) {
            ForgotPasswordScreen(navController) }
    }
}

fun NavGraphBuilder.mainAppGraph(navController: NavController) {
    navigation(
        startDestination = AppDestinations.WELCOME,
        route = AppDestinations.MAIN_APP_GRAPH
    ) {
        composable(AppDestinations.WELCOME) {
            WelcomeScreen(navController) }
        composable(AppDestinations.PROFILE) {
            ProfileScreen(navController) }
    }
}

@Composable
fun LoginScreen(navController: NavController) {
    Column(
        modifier = Modifier.fillMaxSize().padding(16.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Logowanie", fontSize = 28.sp)
        Spacer(Modifier.height(24.dp))
        Button(onClick = { navController.navigateToMainApp() }) {
            Text("Zaloguj")
        }
        Spacer(Modifier.height(12.dp))
        Button(onClick = { navController.navigate(AppDestinations.REGISTER) }) {
            Text("Przejdź do Rejestracji")
        }
        Spacer(Modifier.height(12.dp))
        TextButton(onClick = { navController.navigate(AppDestinations.FORGOT_PASSWORD) }) {
            Text("Zapomniałem hasła")
        }
    }
}

@Composable
fun RegisterScreen(navController: NavController) {
    Column(
        modifier = Modifier.fillMaxSize().padding(16.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Rejestracja", fontSize = 28.sp)
        Spacer(Modifier.height(24.dp))
        Button(onClick = { navController.navigateToMainApp() }) {
            Text("Zarejestruj i zaloguj")
        }
        Spacer(Modifier.height(12.dp))
        TextButton(onClick = { navController.popBackStack() }) {
            Text("Wróć do logowania")
        }
    }
}

@Composable
fun ForgotPasswordScreen(navController: NavController) {
    Column(
        modifier = Modifier.fillMaxSize().padding(16.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Resetowanie Hasła", fontSize = 24.sp, textAlign = TextAlign.Center)
        Spacer(Modifier.height(24.dp))
        Button(onClick = { navController.popBackStack() }) {
            Text("Powrót")
        }
    }
}
@Composable
fun WelcomeScreen(navController: NavController) {
    Column(
        modifier = Modifier.fillMaxSize().padding(16.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Witaj w Aplikacji! 🎉", fontSize = 28.sp)
        Spacer(Modifier.height(24.dp))
        Button(onClick = { navController.navigate(AppDestinations.PROFILE) }) {
            Text("Zobacz mój profil")
        }
    }
}

@Composable
fun ProfileScreen(navController: NavController) {
    Column(
        modifier = Modifier.fillMaxSize().padding(16.dp),
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Ekran Profilu 🧑‍💻", fontSize = 28.sp)
        Spacer(Modifier.height(24.dp))
        Button(onClick = { navController.popBackStack() }) {
            Text("Wróć do ekranu powitalnego")
        }
    }
}

fun NavController.navigateToMainApp() {
    this.navigate(AppDestinations.MAIN_APP_GRAPH) {
        popUpTo(AppDestinations.AUTH_GRAPH) {
            inclusive = true
        }
    }
}