خطوات بسيطة لتنفيذ قواعد البيانات في Android مع Room وHilt

تحسين تطبيقات Android باستخدام Room وHilt لإدارة البيانات
إتقان Room وHilt: أدوات فعالة لقواعد البيانات في Android

إتقان التعامل مع قواعد البيانات في Android باستخدام Room و Hilt بسهولة

في عالم تطوير تطبيقات Android، تُعتبر إدارة قواعد البيانات من التحديات الأساسية التي تواجه المطورين. بينما يوفر SQLite حلاً تقليدياً، إلا أن استخدامه مباشرةً قد يكون معقدًا ويستهلك الوقت. هنا يأتي دور مكتبة Room كجزء من Android Jetpack، لتُبسِّط التعامل مع قواعد البيانات، وتُقدِّم طبقة تجريدية تحمي من الأخطاء الشائعة. أما Hilt، فهو إطار عمل يُسهِّل حقن التبعيات (Dependency Injection) لتنظيم الكود وجعله أكثر كفاءة.

في هذا المقال، سنستعرض كيفية دمج Room وHilt لإنشاء قواعد بيانات قوية ومُحسَّنة في تطبيقات Android، مع خطوات عملية وأمثلة واضحة.

المكتبات اللازمه للتعامل مع البيانات المحليه room في jetpack


[versions]
roomKtx = "2.6.1"
roomCompiler = "2.6.1"

[libraries]
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "roomCompiler" }
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "roomKtx" }

// imp
    implementation(libs.androidx.room.ktx)
    kapt(libs.androidx.room.compiler)


إنشاء Model للبيانات التي يتم تخزينها داخل Room

@Entity
data class RoomDataModel (
    val name: String,
    val description: String,
    val dateAdded : Long,

    @PrimaryKey(autoGenerate = true)
    val id:Int = 0
)

إنشاء Model للبيانات التي يتم تخزينها داخل Room


@Entity

data class RoomDataModel (
    val name: String,
    val description: String,
    val dateAdded : Long,

    @PrimaryKey(autoGenerate = true)

    val id:Int = 0
)

@Entity: يشير إلى أن هذه الفئة تمثل جدولاً في قاعدة البيانات. تصبح كل سمة في هذه الفئة عمودًا في الجدول.

RoomDataModel: هو كيان بيانات (نموذج) يحتوي على الأعمدة التالية:

dateAdded: تاريخ إضافة التعليق التوضيحي باعتباره طويلًا.

id: يتم إنشاء المفتاح الأساسي (@PrimaryKey) تلقائيًا (autoGenerate = true).


تنفيذ الاستعلامات للتعامل مع البيانات في Room - Data Access Object

@Dao
interface RoomDao {

//    @Insert
    @Upsert
    suspend fun upsert(roomDataModel: RoomDataModel)

    @Delete
    suspend fun delete(roomDataModel: RoomDataModel)

    @Query("SELECT * FROM RoomDataModel ORDER BY dateAdded")
    fun getDataFromRoomByDate(): Flow<List<RoomDataModel>>  // إزالة suspend

    @Query("SELECT * FROM RoomDataModel ORDER BY name ASC")
    fun getDataFromRoomByTitle(): Flow<List<RoomDataModel>>  // إزالة suspend

}
RoomDao: هي واجهة تحتوي على طرق تمكننا من التفاعل مع قاعدة البيانات.
@Upsert: يستخدم لإدراج بيانات جديدة أو تحديث البيانات الموجودة إذا كانت موجودة.
@Delete: حذف موديل محدد من قاعدة البيانات.
@Query: يستخدم لاسترداد البيانات. في هذا المثال:

getDataFromRoomByDate: استرداد الملاحظات مرتبة حسب تاريخ الإضافة.
getDataFromRoomByTitle: استرداد الملاحظات مرتبة حسب الاسم.
تسترد كل طريقة تدفق Kotlin، وهو نوع يسمح بجلب البيانات التفاعلية.

كود قاعدة البيانات 

@Database(
    entities = [RoomDataModel::class],
    version = 1
)
abstract class NotesDataBase : RoomDatabase() {
    abstract val dao: RoomDao
}
@Database: يحدد هذا التوجيه قاعدة البيانات ويحتوي على:
entities: يشير إلى الكيان (الجدول) المستخدم في قاعدة البيانات. في هذا المثال، الجدول هو RoomDataModel.
الإصدار: يشير إلى إصدار قاعدة البيانات الذي يجب تغييره إذا تم تعديل بنية قاعدة البيانات (مثل إضافة أعمدة جديدة).
NotesDataBase: وهي فئة تمثل قاعدة البيانات الفعلية التي تم إنشاؤها باستخدام مكتبة الغرفة. يجب أن ترث من RoomDatabase.

RoomDao: يُستخدم للحصول على كائن RoomDao الذي يحتوي على طرق للتفاعل مع البيانات.
بناء قاعة البيانات في ملفات المستخدم عن طريق Dependency Injection باستخدام Hilt

الحالات التي سوف نتعامل معها في ViewModel


data class RoomState(
    val notes: List<RoomDataModel> = emptyList(),
    val title: MutableState<String> = mutableStateOf(""),
    val description: MutableState<String> = mutableStateOf("")
)

RoomState: تمثل هذه الفئة حالة التطبيق.
notes: قائمة التعليقات المخزنة في قاعدة البيانات.
العنوان والوصف: تم إدخال الحقول النصية في شاشة إضافة ملاحظة.
تم استخدام MutableState لان النص سوف يتغير معنا

التعامل مع الاحداث التي تحدث في Viewmodel


يتيح لك ذلك تحديد الأحداث التي يمكن أن تحدث في التطبيق. هذه الأحداث هي استجابات لتفاعل المستخدم مع واجهة المستخدم أو بعض منطق التطبيق. يتيح لنا استخدام الواجهة المغلقة تحديد عدد محدود من الأنواع التي تنفذ هذه الواجهة، مما يجعل إدارة الأحداث أسهل وأكثر أمانًا.
sealed interface RoomEvents {
    object SortNote: RoomEvents
    data class DeleteNote(val note: RoomDataModel): RoomEvents
    data class SaveNote(val title:String, val description:String): RoomEvents
}
object SortNote :
يتم استخدام هذا الحدث عندما يقوم المستخدم بتغيير طريقة ترتيب الملاحظات (حسب التاريخ أو حسب العنوان).
استخدم كائنًا بدلاً من فئة لأن الحدث لا يتطلب أي بيانات إضافية ويتم إنشاؤه ككائن ثابت واحد (Singleton).

data class DeleteNote(val note: RoomDataModel):
يتم تشغيل هذا الحدث عندما يقوم المستخدم بحذف تعليق معين. نحتاج إلى تمرير RoomDataModel للتعليق التوضيحي الذي نريد حذفه.
نظرًا لأن حذف Note يتطلب بيانات، يتم تعريفه على أنه فئة بيانات لتمرير الملاحظات.

data class SaveNote(val title:String, val description:String):
يتم تشغيل هذا الحدث عندما تقوم بحفظ ملاحظة جديدة أو تعديل ملاحظة قديمة. نحتاج إلى تمرير العنوان والوصف كبيانات إلى الحدث.
مثل حذف Note، يتم استخدام فئة البيانات لحفظ البيانات.

إنشاء ViewModel للتعامل مع البيانات

@HiltViewModel
class RoomViewModel @Inject constructor(
    private val dao: RoomDao
) : ViewModel() {

    private val isSortedByDateAdded = MutableStateFlow(true)

    private var notes = isSortedByDateAdded.flatMapLatest { sort ->
        if (sort) {
            dao.getDataFromRoomByDate()
        } else {
            dao.getDataFromRoomByTitle()
        }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
    }

    val _state = MutableStateFlow(RoomState())

    val state = combine(_state, isSortedByDateAdded, notes) { state, isSortedByDateAdded, notes ->
        state.copy(notes = notes)
    }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(500), RoomState())

    fun clearText() {
        _state.update { currentState ->
            currentState.copy(
                title = mutableStateOf(""),
                description = mutableStateOf("")
            )
        }
    }

    fun onEvent(event: RoomEvents) {
        when (event) {
            is RoomEvents.DeleteNote -> {
                viewModelScope.launch {
                    dao.delete(event.note)
                }
            }
            is RoomEvents.SaveNote -> {
                val note = RoomDataModel(
                    name = state.value.title.value,
                    description = state.value.description.value,
                    dateAdded = System.currentTimeMillis()
                )
                viewModelScope.launch {
                    dao.upsert(note)
                }
                clearText()
            }
            RoomEvents.SortNote -> {
                isSortedByDateAdded.value = !isSortedByDateAdded.value
            }
        }
    }
}
RoomViewModel: ViewModel الذي يدير منطق التطبيق.
dao: RoomDao: استخدم حقن Hilt للتفاعل مع قاعدة البيانات.
isSortedByDateAdded: المتغير الذي يتحكم في كيفية فرز التعليقات (حسب التاريخ أو حسب الاسم).
notes: يتحكم في الترتيب الذي يتم به استرداد الملاحظات من قاعدة البيانات.
state: تمثل حالة التطبيق الكاملة، حيث يتم دمج الملاحظات وisSortedByDateAdded لتحديث الحالة.
ClearText(): يعيد تعيين الحقول النصية (العنوان والوصف) إلى القيم الافتراضية الفارغة.
onEvent(event: RoomEvents): يُستخدم للتعامل مع أحداث التطبيق مثل الحذف أو الحفظ أو الترتيب.

كود صفحة عرض البيانات

@Composable
fun RoomScreen(
    navController: NavHostController,
    viewModel: RoomViewModel = hiltViewModel()
) {
    val state = viewModel.state.collectAsState()

    Scaffold(
        topBar = {
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(56.dp)
                    .background(MaterialTheme.colorScheme.onBackground)
                    .padding(15.dp),
                verticalAlignment = Alignment.CenterVertically
            ) {
                Text(text = "Room Database", modifier = Modifier.weight(1f))

                IconButton(onClick = {
                    viewModel.onEvent(RoomEvents.SortNote)
                }) {
                    Icon(Icons.Rounded.Build, contentDescription = "menu", modifier = Modifier)
                }
            }
        },
        floatingActionButton = {
            FloatingActionButton(onClick = {
                viewModel.clearText()
                navController.navigate(Routes.AddTextScreenScreen)
            }) {
                Icon(Icons.Default.Add, contentDescription = "Add Note")
            }
        }
    ) { paddingValues ->
        LazyColumn(
            contentPadding = paddingValues,
            modifier = Modifier
                .fillMaxSize(1f)
                .padding(10.dp),
            verticalArrangement = Arrangement.spacedBy(15.dp)

        ) {
            items(state.value.notes) { note ->
                NoteItem(
                    onClick = { viewModel.onEvent(RoomEvents.DeleteNote(note)) },
                    note= note)
            }

        }
    }
}

@Composable
fun NoteItem(onClick: () -> Unit, note: RoomDataModel) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .clip(RoundedCornerShape(15.dp))
            .background(MaterialTheme.colorScheme.onBackground.copy().copy(alpha = 0.5f))
            .padding(15.dp)
    ) {
        Column(
            modifier = Modifier.weight(1f)
        ) {
            Text(
                text = note.name,
                fontSize = 18.sp,
                fontWeight = FontWeight.SemiBold,
                color = MaterialTheme.colorScheme.onPrimaryContainer
            )
            10.spacerHeight()
            Text(
                text = note.description,
                fontSize = 18.sp,
                fontWeight = FontWeight.SemiBold,
                color = MaterialTheme.colorScheme.onPrimaryContainer
            )
        }

        IconButton(onClick = {
            onClick()
        }) {
            Icon(
                Icons.Rounded.Delete,
                contentDescription = "delete",
                modifier = Modifier.size(35.dp),
                tint = MaterialTheme.colorScheme.onPrimaryContainer
            )
        }
    }


}

كود لصفحة اضافة البيانات

@Composable
fun AddNoteScreen(
    navController: NavHostController,
    viewModel: RoomViewModel = hiltViewModel()
) {
    val state = viewModel.state.collectAsState()

    Scaffold(
        topBar = {
            Row(
                modifier = Modifier
                    .fillMaxWidth()
                    .height(56.dp)
                    .background(MaterialTheme.colorScheme.onBackground)
                    .padding(15.dp),
                verticalAlignment = Alignment.CenterVertically
            ) {
                Text(text = "Add Note", modifier = Modifier.weight(1f))

                IconButton(onClick = {
                    navController.navigateUp()
                }) {
                    Icon(Icons.Rounded.Build, contentDescription = "menu", modifier = Modifier)
                }
            }
        },
        floatingActionButton = {
            FloatingActionButton(onClick = {
                viewModel.onEvent(RoomEvents.SaveNote(state.value.title.value,state.value.description.value))
                navController.navigateUp()
            }) {
                Icon(Icons.Default.Add, contentDescription = "Add Note")
            }
        }
    ){ pv ->
        Column(
            modifier = Modifier
                .padding(pv)
                .fillMaxSize(),
            verticalArrangement = Arrangement.Center
        ) {
            TextField(
                modifier = Modifier.fillMaxWidth().padding(16.dp),
                textStyle = TextStyle(
                    fontWeight = FontWeight.Bold,
                    fontSize = 20.sp
                ),
                placeholder = {
                    Text("Title")
                },
                value = state.value.title.value,
                onValueChange = {
//                    state.value.title.value = it

                    viewModel.onEvent(RoomEvents.SaveNote(
                        it,
                        state.value.description.value,
                    ))
                }
            )
            TextField(
                modifier = Modifier.fillMaxWidth().padding(16.dp),
                textStyle = TextStyle(
                    fontWeight = FontWeight.Bold,
                    fontSize = 20.sp
                ),
                placeholder = {
                    Text("Title")
                },
                value = state.value.description.value,
                onValueChange = {
//                    state.value.description.value = it
                        viewModel.onEvent(RoomEvents.SaveNote(
                            state.value.title.value,
                            it
                        ))


                }
            )
        }

    }
}

تعليقات