كيفية التنقل بين صفحات kotlin compose باستخدام hilt و viewmodel وبدونهم

التنقل بين الصفحات في Compose: Hilt و ViewModel vs الطرق التقليدية
كيفية تحسين تجربة المستخدم باستخدام التنقل في Kotlin Compose

كيفية إدارة الحالة والتنقل في Kotlin Compose بسهولة

في Jetpack Compose، يمكن التعامل مع التنقل في الصفحة باستخدام NavHost وNavController دون الحاجة إلى استخدام مكتبات مثل Hilt أو ViewModel. يمكننا تحقيق ذلك ببساطة عن طريق معالجة المنطق في العناصر المركبة والتحكم في التنقل بين الشاشات باستخدام NavController.في هذا المقال سنرى كيفية إنشاء تطبيق بسيط يتكون من صفحتين، صفحة "رئيسية" وصفحة "خضراء"، وسنشرح كيفية التنقل بينهما دون استخدام ViewModel أو Hilt.


تثبيت المكتبات داخل libs.versions.toml

اضف المكتبات التاليه اذا لم تكن موجوده لديك في ملفات المشروع


[versions]
hiltCompiler = "2.51.1"
hiltNavigationCompose = "1.0.0"
kotlin = "1.9.0"
navigationCompose = "2.8.0"
  
[libraries]
androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" }
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "hiltCompiler" }

hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hiltCompiler" }
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "navigationCompose" }

[plugins]
dagger-hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hiltCompiler" }

تهيئة ملف build.gradle.kts (app)
قم باضافات المكتبات اللازمه وايضا plugins في المكان المخصص لها كما هو موضح 

plugins {
    alias(libs.plugins.dagger.hilt.android)
    kotlin("kapt")
}

...

dependencies {
    implementation("androidx.constraintlayout:constraintlayout-compose:1.0.1")
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.5")
    implementation(libs.androidx.navigation.compose)
    implementation(libs.androidx.hilt.navigation.compose)
    implementation(libs.hilt.android)
    kapt(libs.hilt.compiler)
}
تهيئة ملف build.gradle.kts (compose)
تاكد من وجود الاكواد التاليه بداخله وخصوصا السطر الاخير
plugins {
    alias(libs.plugins.android.application) apply false
    alias(libs.plugins.jetbrains.kotlin.android) apply false
    alias(libs.plugins.dagger.hilt.android) apply false
}

كيفية التنقل بين الصفحات بدون hilt و viewmodel



class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge() // تفعيل التصميم الحافة إلى الحافة
        setContent {
            val navController = rememberNavController()
            NavHost(navController = navController, startDestination = "home") {
                composable("home") { CounterController(navController) }
                composable("green") { SecondScreen(navController) }
            }
        }
    }
}

@Composable
fun CounterController(state: ItemUiState = ItemUiState(), onClick: (ItemModel) -> Unit = {}, navController: NavHostController) {
    Scaffold(
        content = { paddingValues ->
            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .fillMaxHeight()
                    .background(Color.Blue)
                    .padding(paddingValues),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally,
            ) {
                Button(onClick = {
                    navController.navigate("green")
                }) {
                    Text("Click")
                }
            }
        }
    )
}
في هذا المثال، قمنا بإنشاء NavHost وربطناه بوحدة التحكم NavController التي تتحكم في التنقل في الصفحة. يحتوي التطبيق على شاشتين:الشاشة "الرئيسية" حيث نعرض زرًا ينقل المستخدم إلى الشاشة التالية.الشاشة "الخضراء" هي الصفحة الثانية التي سيتم إعادة توجيهك إليها.

عند الضغط على الزر، نذهب إلى الصفحة المسماة "الخضراء" باستخدام navController. التنقل 
navController.navigate("green")


// بهذا الشكل ترجع خطوه واحده
nav.popBackStack()

// تعنى ارجع الى ان تصل لصفحه home وقف لان القيمه false ولكن اذا كانت true سوف يرجع اليها ويغلقها ايضا
nav.popBackStack("home",false)

// للرجوع خطوه واحده
nav.navigateUp()

تحسين عمليات الانتقال بين الصفحات عن طريق sealed class

package com.example.compose
import androidx.compose.runtime.Composable
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable

enum class Screen {
    HOME,
    Details,
}

sealed class NavigationItem(val route: String) {
    object Home : NavigationItem(Screen.HOME.name)
    object Details : NavigationItem(Screen.Details.name)
}

@Composable
fun AppNavHost(
    navController: NavHostController,
    startDestination: String = NavigationItem.Home.route,
) {
    NavHost(
        navController = navController, startDestination = startDestination
    ) {
        composable(NavigationItem.Home.route) { CounterController(navController) }
        composable(NavigationItem.Details.route) { SecondScreen(navController) }
    }
}

// Main
setContent {
    AppNavHost(
       navController = rememberNavController(),
           )
     }


شرح تحسين عمليات الانتقال باستخدام sealed  و enum

لنبدأ بتحديد الشاشات المتاحة للتنقل في التطبيق باستخدام فئة enum . في هذا المثال، لدينا شاشتان رئيسيتان: الصفحة الرئيسية وصفحة التفاصيل.
شاشة فئة التعداد عبارة عن تعداد يحتوي على نوعين من الشاشات، الصفحة الرئيسية والتفاصيل. يمكننا استخدام هذا التعداد لتحديد أنواع الشاشات التي نريد التنقل بينها.

استخدم الفصول المغلقة لتنظيم المساراتبدلاً من استخدام نص عادي لتحديد مسارات التنقل، نستخدم فئات مغلقة لتحديد عناصر التنقل المختلفة. يساعد هذا في جعل التطبيق أكثر قابلية للصيانة ويقلل الأخطاء الناتجة عن مسارات الكتابة.

في هذا المثال، يعتبر NavigationItem فئة مغلقة تحتوي على كائنين: الصفحة الرئيسية والتفاصيل. يحتوي كل كائن على خاصية المسار التي تحدد المسار إلى كل شاشة. الشاشة. منزل. الاسم والشاشة. التفاصيل. name يُرجع الاسم النصي لكل عنصر في التعداد، والذي سنستخدمه كمسار إلى كل شاشة.

AppNavHost هي الوظيفة المسؤولة عن تحديد نظام الملاحة بأكمله. في هذا، نستخدم NavHost للتعرف على الشاشات المختلفة وإدارة التنقل بينها.

كيفية استخدام navigation مع hilt

اولا تحتاج لعمل AndroidEntryPoint في main
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
            setContent {
                AppNavHost(
                    navController = rememberNavController(),
                )
            }
    }
}
وتحتاج لإنشاء class جديد باي اسم ولكنه يرث من Application
@HiltAndroidApp
class EntryPage() : Application() {}

والانتقال الى ملف manifest واضافة الصفحة الرئيسيه التي ترث من application

<uses-permission android:name="android.permission.INTERNET"/>

    <application
        android:allowBackup="true"
        android:name=".EntryPage"
...
/>

لماذا يجب عمل  HiltAndroidApp و AndroidEntryPoint ؟

@HiltAndroidApp عبارة عن خريطة تخبر Hilt بتكوين نظام إدارة بيانات الاعتماد الخاص بالتطبيق. عندما يتم تنفيذ هذه الخريطة في فئة تطبيق، يقوم Hilt بإنشاء المكونات الأساسية للتطبيق، مثل SingletonComponent، الذي يحتوي على جميع التبعيات المشتركة بين جميع الأنشطة والفئات في التطبيق.بدون هذا التعيين، لن يتمكن Hilt من إدارة التبعيات على مستوى التطبيق ولن يتمكن من تسليم الأنشطة أو أي مكونات أخرى.
(تعني ان هذا هو الجزء الرئيسي من تطبيقك ممكن اعتبار انه وعاء وكل شيئ اخر سوف يكون بداخله)

@AndroidEntryPoint هو تعيين يخبر Hilt بأن هذا النشاط (أو أي مكون آخر) يتطلب بيانات اعتماد يديرها Hilt.عند استخدام هذه الخريطة في نشاط مثل MainActivity، يقوم Hilt بإنشاء تعليمات برمجية في الخلفية لتهيئة التبعيات وإتاحتها للاستخدام في النشاط. باستخدام هذا التعيين، يمكن لـ Hilt إدخال التبعيات مباشرة في الأنشطة (مثل ViewModels) أو أي مكون آخر تمت تهيئته بواسطة Hilt.
(تعني ان هذا هو نقطة البدايه لديك)

كيفة التنقل الى الصفحات مع نقل البيانات

data class ItemUiState(
    val args :String = ""
)

// ViewModel
init {
        val passedArgs: String? = savedStateHandle["name"]
        if (passedArgs != null) {
            updateText(passedArgs)
        }

        getData()
    }


    fun updateText(newText: String) {
        _state.update {
            it.copy(args = newText)
        }
    }
    
// Navigation
@Composable
fun AppNavHost(
    navController: NavHostController,
    startDestination: String = NavigationItem.Home.route,
) {
    NavHost(
        navController = navController, startDestination = startDestination
    ) {
        composable(NavigationItem.Home.route,) { CounterController(navController) }
        composable(
            route = "${NavigationItem.Details.route}/{name}",
            arguments = listOf(navArgument("name") { type = NavType.StringType })        )
        { SecondScreen(navController) }
    }
}

// First Screen
@Composable
fun CounterController(navController: NavHostController) {
    val viewModel: CounterViewModel = hiltViewModel()
    val state by viewModel.state.collectAsState()

    ShowItems(
        state = state,
        onClick = {name -> navController.navigate("${NavigationItem.Details.route}/$name") },
        onChange = { name -> viewModel.updateText(name) })
}

Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .fillMaxHeight()
                    .background(Color.Blue)
                    .padding(paddingValues),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally,
            ) {
                TextField(
                    value = state.args,
                    onValueChange = onChange
                )
                Button(
                    onClick = {
                        onClick(state.args)
                    }
                )
                {
                    Text("Click") }
            }


// Second Screen
@Composable
fun SecondScreen(nav: NavHostController) {
    val viewModel: CounterViewModel = hiltViewModel()
    val state by viewModel.state.collectAsState()

    val context = LocalContext.current
    Scaffold(
        content = {
                paddingValues ->
            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .fillMaxHeight()
                    .background(Color.Green)
                    .padding(paddingValues),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally,
            ) {
                Text(state.args)
                10.spacerHeight()
                Button(onClick = {
                    Toast.makeText(context, state.args, Toast.LENGTH_SHORT).show()
                })
                {
                    Text("Click") }
            }
        }
    )
}

شرح الكود نقل البيانات اثناء التنقل بين الصفحات في كومبوس

1-سوف نقوم بزيادة عنصر الى القائمه الخاصه بنا باي اسم وهنا كان args
2 - داخل init في ViewModel سوف نستقبل البيانات ونقوم بعمل updateText لتحديث البيانات
3 - في صفحة Navigation سوف نرسل الاسم الذي نريده عن طريق key باسم name بعد اسم route وايضا سوف نضع له arguments ونحدد النوع
4 - سوف نقوم بعمليه الارسال كما هو موضح في صفحة CounterController
5 - Second Screen نستقبل فيها البيانات ونعرضها من ViewModel كما هو موضح

كيف الانتقال بين الصفحات عن طريق serialization
اولا قم بتثبيت المكتبة اللازمه
dependencies {
    implementation(libs.kotlinx.serialization.json)
}

plugins {
...
    alias(libs.plugins.kotlin.serialization)
}

#----- gradle/libs.versions.toml

[versions]
serialization = "1.6.3"

[libraries]
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization"


[plugins] 
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } }

تعديل عمليات الانتقال

@Composable
fun AppSerializableHost(navController: NavHostController,) {
    NavHost(
        navController = navController, startDestination = ScreenTest
    ) {
        composable<ScreenHome>{ SecondScreen(nav = navController) }
        composable<ScreenHomeTwo>{ it -> TutorialsScreen(it=it) }
        composable<ScreenTest>{ DataStoreInput() }

    }
}

@Serializable
object ScreenHome

@Serializable
object ScreenTest

@Serializable
data class ScreenHomeTwo(
    val title: String,
    val id: Int
)
الان الصفحات التي سوف ننتقل لها سوف نقوم عمل لها object اذا كانت الصفحه لا تحتوي على بيانات واذا كنا نريد ارسال بيانات سوف نستعمل data ونمرر it الى الصفحة التي نريد ارسال البيانات لها

كيفية استقال البيانات وعرضها في التصميم
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TutorialsScreen(modifier: Modifier = Modifier,it : NavBackStackEntry) {

    val args = it.toRoute<ScreenHomeTwo>()

    val sheetState = rememberModalBottomSheetState()
    val scope = rememberCoroutineScope()
    var bottomSheetShow by remember {
        mutableStateOf(false)
    }

    Scaffold { paddingValues ->
        if (bottomSheetShow) {
            ModalBottomSheet(
                sheetState = sheetState,
                onDismissRequest = { bottomSheetShow = false }
            ) {
                Column(
                    horizontalAlignment = Alignment.CenterHorizontally,
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(15.dp)
                ) {
                    Text("Hi I'm ${args.title} my id is ${args.id}")
                    Text("----------")
                    Text("You Can close Bottom From Here")
                    Spacer(modifier = Modifier.height(100.dp))
                    Button(onClick = {
                        scope.launch {
                            sheetState.hide()
                        }.invokeOnCompletion {
                            bottomSheetShow = false
                        }
                    }) {
                        Text("Close")
                    }
                }
    }
}
    }
}
تعليقات