الفرق بين استخدام الـ SQLite و الـ Room

مقالة توضح الفرق بين استخدام الـ SQLite و الـ Room بالامثله.

Mohammad Laifمنذ 3 أشهر

بسم الله الرحمن الرحيم

السلام عليكم ورحمة الله وبراكته

هذه المقالة توضح الفرق بين استخدام الـ SQLite واستخدام الـ Room في انشاء قاعدة البيانات لتطبيقات الاندرويد.
 

ماذا ستقرئ في هذه المقالة؟

  • لمحة سريعة عن المشروع.
  • مقدمة بسيطة عن الـ SQLite.
  • مكونات الـ SQLite.
  • مقدمة بسيطة عن الـ Room.
  • مكونات الـ Room.
  • انشاء قواعد البيانات (كلاً من الـ Room و الـ SQLite).
  • استخدام قواعد البيانات كلاً من الـ Room و الـ SQLite).

 

لمحة سريعة عن المشروع

المشروع عباره عن تطبيق لحفظ الملاحظات. مقسم الى جزئين. تستطيع تصفحهم كما تشاء لمعرفة الفرق بين الاثنين عبر رابط المشروع من هنا: Room Vs SQLite.
الجزء الاول:

  • كلاس الـ MainActivity يوجد بها طريقة استخدام الـ Room.
  • مجلد الـ Room ويحتوي على المتطلبات اللازمة من كلاسات للـ Room.

الجزء الثاني:

  • كلاس الـ Main2Activity يوجد بها طريقة استخدام الـ SQLite
  • مجلد الـ SQLite ويحتوي على المتطلبات اللازمة من كلاسات للـ SQLite.

 

(اذا اردت معرفة المزيد حول الـ Room تفضل اطلع على دورتي السابقة في هذا الرابط: Android Architecture Components وتشرح الـ Room بشكل كامل ومفصل)

 

مقدمة بسيطة عن الـ SQLite

الـ SQLite عبارة عن مكتبة مفتوحة المصدر لقاعدة البيانات الـ SQL تأتي كحزمة مثبته (built in) في نظام الاندرويد. فطريقة بناء قاعدة البيانات باستخدام الـ SQLite تعتبر طويلة نسبياً ومع ازدياد المهام للمشروع فالاكواد ايضاً تزداد بشكل كبير جداً. ففي تطبيق لدي "" تعدت الاكواد المسؤولة عن التعامل مع قاعدة البيانات الالف الاسطر. وايضاً المشاكل التي تنتج عنها كثيره ويصعب حلها سريعاً, واي تعديل يطرئ على التطبيق يصاحبه تعديلات كثيره في هذه الاكواد.

 

مكونات الـ SQLite

في الـ SQLite نستخدم المكونات التالية:

  • انشاء كلاس DatabaseContract فيها نقوم بكتابة الـ Schema لقاعدة البيانات المراد انشائها.
  • انشاء كلاس DatabaseHelper تستخدم لإنشاء قاعدة البيانات كجداولها واجراء التغييرات اللازمه كعمل Update لها. 
  • انشاء كلاس DatabaseProvider تستخدم لكتابة الدوال المسؤله عن الحفظ والتحديث والحذف لعناصرنا في قاعدة البيانات.
  • نقوم باستخدام Content Provider (بمساعدة الداله getContentResolver في اي Context كـ Activity و Fragment) حتى نتمكن من استخدام هذه الدوال.
  • والـ LoaderManager الذي من خلاله نقوم بتحميل البيانات على شكل Cursor من قاعدة البيانات.
  • وايضاً استخدام الـ ContentValues لتجهيز عناصرنا قبل حفظهم او تحديثهم.

 

التعامل مع هذه الاشياء يضيع ساعات بل ايام وحتى اسابيع من عمل المبرمج.

 

مقدمة بسيطة عن الـ Room

جائت مكتبة الـ Room لإختصار الكثير من الاكواد والوقت للمبرمج. فهي عباره عن طبقة تغلف الـ SQLite لاغير. فهي تتميز بالسرعه في الانشاء و اجراء الاختبارات عليها بشكل سلسل وسهوله اكتشاف الاخطاء بها.

 

(اذا اردت معرفة المزيد حول الـ Room انصحك بهذه الدورة  قد اعدتها مسبقاً عن: Android Architecture Components وتشرح الـ Room بشكل كامل ومفصل)

 

مكونات الـ Room

في الـ Room نستخدم المكونات التالية:

  • واجهة DAO والتي بها صيغ الـ SQL و دوال التعامل مع البيانات. وحلت بديلاً لكلاس الـ DatabaseProvider.
  • كلاس AppDatabase لإنشاء قاعدة البيانات. حلت بديلاً لكلاس الـ Databasehelper.
  • كلاس ViewModel وبها عنصر الـ LiveData وتعتبر حلقة الوصل بين قاعدة البيانات والتطبيق وبها دوال التعامل مع البيانات من حفظ وتحديث وحذف. حلت بديلاً لكلاس الـ DatabaseProvider و تلاشى استخدام الـ Loader Manager و Content Provider.

 

هناك بعض المكونات الاختياريه لم اتطرق لهم مثل الـ Repository و الـ Paging. تستطيع تصفحهم بالدروس التاليه:

استخدام الـ Paging في الـ DAO و انشاء المستودع بـ Paging و انشاء الـ ViewModel بـ Paging و انشاء Adapter للـ PagedList و استخدام الـ ViewModel مع الـ Paging في الـ Activity

 

انشاء قواعد البيانات

 

في الـ Room نحتاج الى انشاء:

  • كلاس Entity.
  • واجهة DAO.
  • كلاس ViewModel.
  • كلاس AppDatabase.

 

في الـ SQLite نحتاج الى انشاء:

  • كلاس DatabaseContract.
  • كلاس DatabaseHelper.
  • كلاس DatabaseProvider.
  • نعمل implementation للواجهه LoaderManager.
  • نتعامل مع الـ Content Provider.
  • نعمل عناصر ContentValues.

 

مقارنه بالشفرات البرمجيه للـ Room و SQLite:

في الروم نحتاج كبداية انشاء الـ Entity وهي عباره عن كلاس Model معدله بعض الشئ لتتناسب مع الـ Room كالتالي (اطلع على درس انشاء كلاس الـ Entity):

@Entity(tableName = "note_table_name")
public class NoteEntity {
    @PrimaryKey(autoGenerate = true)
    @ColumnInfo(name = "note_id")
    private int mId;
    private String mTitle;
    private String mBody;

    // Room Constructor
    public NoteEntity(@NonNull int id, String title, String body) {
        mId = id;
        mTitle = title;
        mBody = body;
    }

    // Our Constructor
    @Ignore
    public NoteEntity(String title, String body) {
        mTitle = title;
        mBody = body;
    }

    public int getId() {
        return mId;
    }

    public void setId(int id) {
        mId = id;
    }

    public String getTitle() {
        return mTitle;
    }

    public void setTitle(String title) {
        mTitle = title;
    }

    public String getBody() {
        return mBody;
    }

    public void setBody(String body) {
        mBody = body;
    }
}

 

وتعادل هذه الكلاس في الـ SQLite الكلاسين:

كلاس الموديل:

public class Note {
    private String mTitle;
    private String mBody;

    public Note(String title, String body) {
        mTitle = title;
        mBody = body;
    }

    public String getTitle() {
        return mTitle;
    }

    public void setTitle(String title) {
        mTitle = title;
    }

    public String getBody() {
        return mBody;
    }

    public void setBody(String body) {
        mBody = body;
    }

 

كلاس الـ DatabaseContract:

public class DatabaseContract {

    // Private Constructor
    private DatabaseContract(){}

    // Content Provider
    public static final String CONTENT_AUTHORITY = "com.mzdhr.roomvssqlite";

    // Base URI's
    public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY);

    // Possible Paths
    public static final String PATH_NOTES = "notes";


    public static final class NoteEntry implements BaseColumns{

        // Note Table Uri
        public static final Uri CONTENT_URI_NOTE = Uri.withAppendedPath(BASE_CONTENT_URI, PATH_NOTES);

        // Note Table
        public static final String NOTE_TABLE_NAME = "note_table_name";
        public static final String _ID = BaseColumns._ID;
        public static final String COLUMN_NOTE_TITLE = "note_title";
        public static final String COLUMN_NOTE_BODY = "note_body";

    }
}

 

كلاس الـ Dao في الـ Room (اطلع على درس انشاء واجهة الـ DAO و استخدام الـ LiveData في الـ DAO)​​​:

@Dao
public interface NoteDao {
    @Query("SELECT * FROM note_table_name")
    LiveData<List<NoteEntity>> getAllNotes();

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    void insertNote(NoteEntity noteEntity);

    @Update(onConflict = OnConflictStrategy.REPLACE)
    void updateNote(NoteEntity noteEntity);

    @Delete
    void deleteNote(NoteEntity noteEntity);
}

 

وتعادل هذه الكلاس في الـ SQLite الكلاس التاليه:

كلاس الـ DatabaseProvider:

public class DatabaseProvider extends ContentProvider {
    private static final String TAG = DatabaseProvider.class.getSimpleName();

    private static final int NOTES = 1100;
    private static final int NOTE_ID = 1101;

    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    private DatabaseHelper mDatabaseHelper;

    static {
        sUriMatcher.addURI(DatabaseContract.CONTENT_AUTHORITY, DatabaseContract.PATH_NOTES, NOTES);
        sUriMatcher.addURI(DatabaseContract.CONTENT_AUTHORITY, DatabaseContract.PATH_NOTES + "/#", NOTE_ID);
    }

    @Override
    public boolean onCreate() {
        mDatabaseHelper = new DatabaseHelper(getContext());
        return true;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        SQLiteDatabase database = mDatabaseHelper.getReadableDatabase();
        Cursor cursor;
        int match = sUriMatcher.match(uri);

        switch (match) {
            case NOTES:
                cursor = database.query(DatabaseContract.NoteEntry.NOTE_TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder);
                break;

            case NOTE_ID:
                selection = DatabaseContract.NoteEntry._ID + "=?";
                selectionArgs = new String[]{String.valueOf(ContentUris.parseId(uri))};
                cursor = database.query(DatabaseContract.NoteEntry.NOTE_TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder);
                break;

            default:
                throw new IllegalArgumentException(TAG + " Cannot query unknown Uri ---> " + uri);
        }

        cursor.setNotificationUri(getContext().getContentResolver(), uri);
        return cursor;
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        return null;
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
        final int match = sUriMatcher.match(uri);

        switch (match) {
            case NOTES:
                return insertNote(uri, contentValues);

            default:
                throw new IllegalArgumentException(TAG + " Insertion is not supported for " + uri);
        }
    }

    private Uri insertNote(@NonNull Uri uri, @Nullable ContentValues contentValues) {
        SQLiteDatabase database = mDatabaseHelper.getWritableDatabase();
        long id = database.insert(DatabaseContract.NoteEntry.NOTE_TABLE_NAME, null, contentValues);
        if (id == -1) {
            Log.e(TAG, "insert: Failed to insert a row for " + uri);
        }

        getContext().getContentResolver().notifyChange(uri, null);
        return ContentUris.withAppendedId(uri, id);
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        final int match = sUriMatcher.match(uri);

        switch (match) {
            case NOTE_ID:
                selection = DatabaseContract.NoteEntry._ID + "=?";
                selectionArgs = new String[]{String.valueOf(ContentUris.parseId(uri))};
                return deleteNote(uri, selection, selectionArgs);

            default:
                throw new IllegalArgumentException(TAG + "delete: Deletion is not supported for " + uri);
        }
    }

    private int deleteNote(@NonNull Uri uri, @NonNull String selection, @NonNull String[] selectionArgs) {
        int rowsDeleted;
        SQLiteDatabase database = mDatabaseHelper.getWritableDatabase();
        rowsDeleted = database.delete(DatabaseContract.NoteEntry.NOTE_TABLE_NAME, selection, selectionArgs);

        if (rowsDeleted != 0) {
            getContext().getContentResolver().notifyChange(uri, null);
        }
        return rowsDeleted;
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
        final int match = sUriMatcher.match(uri);

        switch (match) {
            case NOTE_ID:
                selection = DatabaseContract.NoteEntry._ID + "=?";
                selectionArgs = new String[]{String.valueOf(ContentUris.parseId(uri))};
                return updateNote(uri, values, selection, selectionArgs);

            default:
                throw new IllegalArgumentException("Update is not supported for " + uri);
        }
    }

    private int updateNote(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        if (values.size() == 0) {
            return 0;
        }

        SQLiteDatabase database = mDatabaseHelper.getWritableDatabase();
        int rowsUpdated = database.update(DatabaseContract.NoteEntry.NOTE_TABLE_NAME, values, selection, selectionArgs);

        // Notify the UI
        if (rowsUpdated != 0) {
            getContext().getContentResolver().notifyChange(uri, null);
        }

        return rowsUpdated;
    }
}

 

كلاس الـ AppDatabase في الـ Room (اطلع على درس انشاء كلاس قاعدة البيانات الـ Room):

@Database(entities = {NoteEntity.class}, version = 1, exportSchema = false)
public abstract class AppDatabase extends RoomDatabase{

    private static final String TAG = AppDatabase.class.getSimpleName();
    private static final Object LOCK = new Object();
    private static final String DATABASE_NAME = "flashcardsdb";
    private static AppDatabase sInstance;

    public static AppDatabase getInstance(Context context) {
        if (sInstance == null) {
            synchronized (LOCK) {
                Log.d(TAG, "getInstance: Creating a new database instance");
                sInstance = Room.databaseBuilder(
                        context.getApplicationContext(),
                        AppDatabase.class,
                        AppDatabase.DATABASE_NAME
                ).build();
            }
        }
        Log.d(TAG, "getInstance: Getting the database instance, no need to recreated it.");
        return sInstance;
    }


    public abstract NoteDao noteDao();

}

 

وتعادل هذه الكلاس في الـ SQLite الكلاس التاليه:

كلاس الـ DatabaseHelper:

public class DatabaseHelper extends SQLiteOpenHelper{

    private static final String DATABASE_NAME = "SQLiteNoteDatabase.db";
    private static final int DATABASE_VERSION = 1;

    // SQL COMMANDS
    private String SQL_CREATE_NOTE_TABLE;

    public DatabaseHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        SQL_CREATE_NOTE_TABLE = "CREATE TABLE " + DatabaseContract.NoteEntry.NOTE_TABLE_NAME + " ("
                + DatabaseContract.NoteEntry._ID +  " INTEGER PRIMARY KEY AUTOINCREMENT, "
                + DatabaseContract.NoteEntry.COLUMN_NOTE_TITLE +  " TEXT NOT NULL, "
                + DatabaseContract.NoteEntry.COLUMN_NOTE_BODY +  " TEXT NOT NULL DEFAULT '' )";

        sqLiteDatabase.execSQL(SQL_CREATE_NOTE_TABLE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {

    }
}

 

كلاس الـ ViewModel في الـ Room (اطلع على درس انشاء الـ ViewModel بـ LiveData):

public class MainActivityViewModel extends AndroidViewModel {
    private NoteDao mNoteDao;
    private LiveData<List<NoteEntity>> mAllNotes;

    public MainActivityViewModel(@NonNull Application application) {
        super(application);
        AppDatabase appDatabase = AppDatabase.getInstance(application);
        mNoteDao = appDatabase.noteDao();
        mAllNotes = mNoteDao.getAllNotes();
    }

    public LiveData<List<NoteEntity>> getAllNotes() {
        return mAllNotes;
    }

    public void insert(final NoteEntity noteEntity) {
        AsyncTask.execute(new Runnable() {
            @Override
            public void run() {
                mNoteDao.insertNote(noteEntity);
            }
        });
    }

    public void update(final NoteEntity noteEntity) {
        AsyncTask.execute(new Runnable() {
            @Override
            public void run() {
                mNoteDao.updateNote(noteEntity);
            }
        });
    }

    public void delete(final NoteEntity noteEntity) {
        AsyncTask.execute(new Runnable() {
            @Override
            public void run() {
                mNoteDao.deleteNote(noteEntity);
            }
        });
    }
}

وتعادل هذه الكلاس من مكونات الـ SQLite كلاً من استخدامات:  LoaderManager و الـ Content Provider.

 

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

فبعد تجهيز كل هذه الكلاسات والدوال (للقاعدتين الـ Room و الـ SQLite) التي تعتبر كادوات تساعدنا في بناء تطبيقاتنا, فالان سنتعرف على اي من هذه الادوات اسهل في الاستخدام والتعامل والبناء.

 

استخدام الـ Room (اطلع على درس استخدام الـ ViewModel مع الـ LiveData في الـ Activity):

ننشئ حقل لكلاس الـ ViewModel:

private MainActivityViewModel mViewModel;

 

نعمل init لها:

mViewModel = ViewModelProviders.of(this).get(MainActivityViewModel.class);

 

ونجهز عنصرين:

NoteEntity noteEntity1 = new NoteEntity("This is first note", "Body for this note");
NoteEntity noteEntity2 = new NoteEntity("This is first note", "Body for this note");

 

طريقة الحفظ:

mViewModel.insert(noteEntity1);
mViewModel.insert(noteEntity2);

 

طريقة التحديث للعنصر الاول:

mViewModel.update(new NoteEntity(1,"User", "Body"));

 

طريقة الحذف للعنصر الثاني:

mViewModel.delete(noteEntity2);

 

طريقة عرض جميع العناصر:

mViewModel.getAllNotes().observe(this, new Observer<List<NoteEntity>>() {
    @Override
    public void onChanged(@Nullable List<NoteEntity> noteEntities) {
        for (int i = 0; i < noteEntities.size(); i++) {
            Log.d(TAG, "Title: " + noteEntities.get(i).getTitle() + " Body: " + noteEntities.get(i).getBody());
        }
    }
});

 

استخدام الـ SQLite:

اولاً لاننسى وضع الـ Provider بداخل الـ application في الـ AndroidManifest.xml هكذا:

<!-- SQLite Database Provider -->
<provider
android:name=".SQLite.DatabaseProvider"
android:authorities="com.mzdhr.roomvssqlite"
android:exported="false"/>

 

ونجهز عنصرين:

Note note1 = new Note("Note Title", "Note Body");
Note note2 = new Note("Note Title", "Note Body");

 

طريقة الحفظ:

insertNote(note1);
insertNote(note2);

    private void insertNote(Note note) {
        ContentValues contentValues = new ContentValues();
        contentValues.put(DatabaseContract.NoteEntry.COLUMN_NOTE_TITLE, note.getTitle());
        contentValues.put(DatabaseContract.NoteEntry.COLUMN_NOTE_BODY, note.getBody());

        Uri insertUri = getContentResolver().insert(DatabaseContract.NoteEntry.CONTENT_URI_NOTE, contentValues);

        if (insertUri == null) {
            Log.d(TAG, "insertNote: Insert failed!");
        } else {
            Log.d(TAG, "insertNote: Insert successful");
            int insertedNoteId = Integer.valueOf(insertUri.getLastPathSegment());
            Log.d(TAG, "insertNote: Insert ID: " + insertedNoteId);
        }

    }

 

طريقة التحديث للعنصر الاول:

Note updateNote = new Note("User", "This is user one");
updateNote(updateNote, 1);

    private void updateNote(Note note, int noteId) {
        ContentValues contentValues = new ContentValues();
        contentValues.put(DatabaseContract.NoteEntry.COLUMN_NOTE_TITLE, note.getTitle());
        contentValues.put(DatabaseContract.NoteEntry.COLUMN_NOTE_BODY, note.getBody());

        Uri updateUri = ContentUris.withAppendedId(DatabaseContract.NoteEntry.CONTENT_URI_NOTE, noteId);

        int rowsUpdated = getContentResolver().update(updateUri, contentValues, null, null);
        if (rowsUpdated == 0) {
            Log.d(TAG, "updateNote: update failed");
        } else {
            Log.d(TAG, "updateNote: update successful");
        }
    }

 

طريقة الحذف للعنصر الثاني:

deleteNote(2);

    private void deleteNote(int noteId) {
        Uri deleteUri = ContentUris.withAppendedId(DatabaseContract.NoteEntry.CONTENT_URI_NOTE, noteId);
        int rowsDeleted = getContentResolver().delete(deleteUri, null, null);
        if (rowsDeleted == 0 ) {
            Log.d(TAG, "deleteNote: Delete failed");
        } else {
            Log.d(TAG, "deleteNote: delete successful");
        }
    }

 

طريقة عرض جميع العناصر:

عمل implementaion للواجهه LoaderManager للاكتفتي المراد استخدامها فيها:

public class Main2Activity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor>{
...
}

 

تجهيز حقل للـ Loader:

private static final int ALL_NOTE_LOADER = 100;

 

تشغيل هذا الـ Loader:

getLoaderManager().initLoader(ALL_NOTE_LOADER, null, this).forceLoad();

 

عمل Override للدوال:

    private void updateNote(Note note, int noteId) {
        ContentValues contentValues = new ContentValues();
        contentValues.put(DatabaseContract.NoteEntry.COLUMN_NOTE_TITLE, note.getTitle());
        contentValues.put(DatabaseContract.NoteEntry.COLUMN_NOTE_BODY, note.getBody());

        Uri updateUri = ContentUris.withAppendedId(DatabaseContract.NoteEntry.CONTENT_URI_NOTE, noteId);

        int rowsUpdated = getContentResolver().update(updateUri, contentValues, null, null);
        if (rowsUpdated == 0) {
            Log.d(TAG, "updateNote: update failed");
        } else {
            Log.d(TAG, "updateNote: update successful");
        }
    }

    /**
     * Loader Section
     */
    @Override
    public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
        String[] projection = {
                DatabaseContract.NoteEntry.COLUMN_NOTE_TITLE,
                DatabaseContract.NoteEntry.COLUMN_NOTE_BODY,
        };

        return new CursorLoader(this,
                DatabaseContract.NoteEntry.CONTENT_URI_NOTE,
                projection,
                null,
                null,
                null
        );
    }

    @Override
    public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {

        if (cursor == null || cursor.getCount() < 1) {
            Log.d(TAG, "onLoadFinished: Cursor is null or there is less than 1 row in the cursor");
            return;
        }

        for (int i = 0; i < cursor.getCount(); i++) {
            // Getting Indexes
            cursor.moveToNext();
            int noteTitleIndex = cursor.getColumnIndex(DatabaseContract.NoteEntry.COLUMN_NOTE_TITLE);
            int noteBodyIndex = cursor.getColumnIndex(DatabaseContract.NoteEntry.COLUMN_NOTE_BODY);

            // Getting Values
            String noteTitle = cursor.getString(noteTitleIndex);
            String noteBody = cursor.getString(noteBodyIndex);

            Log.d(TAG, "Title: " + noteTitle + " Body: " + noteBody);
        }
    }

    @Override
    public void onLoaderReset(Loader<Cursor> loader) {

    }

 

يتضح لنا في الاخير ان الـ Room اسهل بكثير في التعامل, بل لاتاخد شئ لإنشائها. والى الهجره من الـ SQLite الى الـ Room في مقالة مستقبليه ان شاء الله.

 

نهاية المقالة

اذا اعجبتك المقالة لاتنسى الضغظ على زر اعجبني ولنشر الفائدة قم بمشاركته مع من تحب, ولاتنسى كذلك تتبع المقالة حتى تطلع على التغييرات والتحديثات المتعلقه بها مستقبلاً.

كلمات دليلية: android room sqlite
5
إعجاب
634
مشاهدات
1
مشاركة
2
متابع
متميز
محتوى رهيب

التعليقات (0)

لايوجد لديك حساب في عالم البرمجة؟

تحب تنضم لعالم البرمجة؟ وتنشئ عالمك الخاص، تنشر المقالات، الدورات، تشارك المبرمجين وتساعد الآخرين، اشترك الآن بخطوات يسيرة !