![]() |
كيفية تحسين تجربة المستخدم باستخدام التنقل في 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")
}
}
}
}
}
}