تخزين البيانات والبحث عنها

تجربة ميزة "الكتابة"
‫Jetpack Compose هي مجموعة أدوات واجهة المستخدم المُقترَحة لنظام التشغيل Android. تعرَّف على كيفية إضافة وظيفة البحث في ميزة "الإنشاء".

هناك العديد من الطرق لتخزين بياناتك، مثل قاعدة بيانات على الإنترنت أو قاعدة بيانات SQLite محلية أو حتى في ملف نصي. يعود إليك اختيار الحل الأنسب لتطبيقك. يشرح لك هذا الدرس كيفية إنشاء جدول افتراضي في SQLite يمكنه توفير عملية بحث قوية في النص الكامل. تتم تعبئة الجدول ببيانات من ملف نصي يحتوي على كلمة و تعريف في كل سطر في الملف.

إنشاء الجدول الافتراضي

يعمل الجدول الافتراضي بطريقة مشابهة لجدول SQLite، ولكنّه يقرأ ويكتب في عنصر في الذاكرة من خلال عمليات الاستدعاء، بدلاً من ملف قاعدة البيانات. لإنشاء جدول افتراضي، أنشئ فئة للجدول:

Kotlin

class DatabaseTable(context: Context) {

    private val databaseOpenHelper = DatabaseOpenHelper(context)

}

Java

public class DatabaseTable {
    private final DatabaseOpenHelper databaseOpenHelper;

    public DatabaseTable(Context context) {
        databaseOpenHelper = new DatabaseOpenHelper(context);
    }
}

أنشئ فئة داخلية في DatabaseTable تمدّد SQLiteOpenHelper. تحدِّد فئة SQLiteOpenHelper methods ملفتة للانتباه يجب إلغاء تحديدها حتى يمكن إنشاء جدول قاعدة البيانات وترقيته عند الضرورة. على سبيل المثال، إليك بعض الرموز البرمجية التي تحدّد جدول قاعدة بيانات سيتضمّن كلمات لتطبيق قاموس:

Kotlin

private const val TAG = "DictionaryDatabase"

// The columns we'll include in the dictionary table
const val COL_WORD = "WORD"
const val COL_DEFINITION = "DEFINITION"

private const val DATABASE_NAME = "DICTIONARY"
private const val FTS_VIRTUAL_TABLE = "FTS"
private const val DATABASE_VERSION = 1

private const val FTS_TABLE_CREATE =
        "CREATE VIRTUAL TABLE $FTS_VIRTUAL_TABLE USING fts3 ($COL_WORD, $COL_DEFINITION)"

class DatabaseTable(context: Context) {

    private val databaseOpenHelper: DatabaseOpenHelper

    init {
        databaseOpenHelper = DatabaseOpenHelper(context)
    }

    private class DatabaseOpenHelper internal constructor(private val helperContext: Context) :
            SQLiteOpenHelper(helperContext, DATABASE_NAME, null, DATABASE_VERSION) {
        private lateinit var mDatabase: SQLiteDatabase

        override fun onCreate(db: SQLiteDatabase) {
            mDatabase = db
            mDatabase.execSQL(FTS_TABLE_CREATE)
        }

        override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
            Log.w(
                    TAG,
                    "Upgrading database from version $oldVersion to $newVersion , which will " +
                            "destroy all old data"
            )

            db.execSQL("DROP TABLE IF EXISTS $FTS_VIRTUAL_TABLE")
            onCreate(db)
        }

    }
}

Java

public class DatabaseTable {

    private static final String TAG = "DictionaryDatabase";

    // The columns we'll include in the dictionary table
    public static final String COL_WORD = "WORD";
    public static final String COL_DEFINITION = "DEFINITION";

    private static final String DATABASE_NAME = "DICTIONARY";
    private static final String FTS_VIRTUAL_TABLE = "FTS";
    private static final int DATABASE_VERSION = 1;

    private final DatabaseOpenHelper databaseOpenHelper;

    public DatabaseTable(Context context) {
        databaseOpenHelper = new DatabaseOpenHelper(context);
    }

    private static class DatabaseOpenHelper extends SQLiteOpenHelper {

        private final Context helperContext;
        private SQLiteDatabase mDatabase;

        private static final String FTS_TABLE_CREATE =
                    "CREATE VIRTUAL TABLE " + FTS_VIRTUAL_TABLE +
                    " USING fts3 (" +
                    COL_WORD + ", " +
                    COL_DEFINITION + ")";

        DatabaseOpenHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
            helperContext = context;
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            mDatabase = db;
            mDatabase.execSQL(FTS_TABLE_CREATE);
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
            Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
                    + newVersion + ", which will destroy all old data");
            db.execSQL("DROP TABLE IF EXISTS " + FTS_VIRTUAL_TABLE);
            onCreate(db);
        }
    }
}

تعبئة الجدول الافتراضي

يحتاج الجدول الآن إلى بيانات لتخزينها. يوضّح لك الرمز البرمجي التالي كيفية قراءة ملف نصي (يقع في res/raw/definitions.txt) يحتوي على كلمات وتعريفاتها، وكيفية تحليل هذا الملف، وكيفية إدراج كل سطر من هذا الملف كصف في الجدول الافتراضي. يتم تنفيذ كل ذلك في سلسلة مهام أخرى لمنع قفل واجهة المستخدم. أضِف الرمز التالي إلى DatabaseOpenHelper الصف الداخلي.

ملاحظة: يمكنك أيضًا إعداد طلب إعادة اتصال لإعلام نشاط واجهة المستخدم بإكمال سلسلة المحادثات هذه.

Kotlin

private fun loadDictionary() {
    Thread(Runnable {
        try {
            loadWords()
        } catch (e: IOException) {
            throw RuntimeException(e)
        }
    }).start()
}

@Throws(IOException::class)
private fun loadWords() {
    val inputStream = helperContext.resources.openRawResource(R.raw.definitions)

    BufferedReader(InputStreamReader(inputStream)).use { reader ->
        var line: String? = reader.readLine()
        while (line != null) {
            val strings: List<String> = line.split("-").map { it.trim() }
            if (strings.size < 2) continue
            val id = addWord(strings[0], strings[1])
            if (id < 0) {
                Log.e(TAG, "unable to add word: ${strings[0]}")
            }
            line = reader.readLine()
        }
    }
}

fun addWord(word: String, definition: String): Long {
    val initialValues = ContentValues().apply {
        put(COL_WORD, word)
        put(COL_DEFINITION, definition)
    }

    return database.insert(FTS_VIRTUAL_TABLE, null, initialValues)
}

Java

private void loadDictionary() {
        new Thread(new Runnable() {
            public void run() {
                try {
                    loadWords();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }).start();
    }

private void loadWords() throws IOException {
    final Resources resources = helperContext.getResources();
    InputStream inputStream = resources.openRawResource(R.raw.definitions);
    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));

    try {
        String line;
        while ((line = reader.readLine()) != null) {
            String[] strings = TextUtils.split(line, "-");
            if (strings.length < 2) continue;
            long id = addWord(strings[0].trim(), strings[1].trim());
            if (id < 0) {
                Log.e(TAG, "unable to add word: " + strings[0].trim());
            }
        }
    } finally {
        reader.close();
    }
}

public long addWord(String word, String definition) {
    ContentValues initialValues = new ContentValues();
    initialValues.put(COL_WORD, word);
    initialValues.put(COL_DEFINITION, definition);

    return database.insert(FTS_VIRTUAL_TABLE, null, initialValues);
}

استخدِم الطريقة loadDictionary() كلما كان ذلك مناسبًا لتعبئة الجدول. يمكنك استخدام onCreate() في طريقة DatabaseOpenHelper مباشرةً بعد إنشاء الجدول:

Kotlin

override fun onCreate(db: SQLiteDatabase) {
    database = db
    database.execSQL(FTS_TABLE_CREATE)
    loadDictionary()
}

Java

@Override
public void onCreate(SQLiteDatabase db) {
    database = db;
    database.execSQL(FTS_TABLE_CREATE);
    loadDictionary();
}

بعد إنشاء الجدول الافتراضي وتعبئته، استخدِم طلب البحث الذي يوفّره SearchView للبحث في البيانات. أضِف الطرق التالية إلى فئة DatabaseTable لإنشاء عبارة SQL تبحث عن الاستعلام:

Kotlin

fun getWordMatches(query: String, columns: Array<String>?): Cursor? {
    val selection = "$COL_WORD MATCH ?"
    val selectionArgs = arrayOf("$query*")

    return query(selection, selectionArgs, columns)
}

private fun query(
        selection: String,
        selectionArgs: Array<String>,
        columns: Array<String>?
): Cursor? {
    val cursor: Cursor? = SQLiteQueryBuilder().run {
        tables = FTS_VIRTUAL_TABLE
        query(databaseOpenHelper.readableDatabase,
                columns, selection, selectionArgs, null, null, null)
    }

    return cursor?.run {
        if (!moveToFirst()) {
            close()
            null
        } else {
            this
        }
    } ?: null
}

Java

public Cursor getWordMatches(String query, String[] columns) {
    String selection = COL_WORD + " MATCH ?";
    String[] selectionArgs = new String[] {query+"*"};

    return query(selection, selectionArgs, columns);
}

private Cursor query(String selection, String[] selectionArgs, String[] columns) {
    SQLiteQueryBuilder builder = new SQLiteQueryBuilder();
    builder.setTables(FTS_VIRTUAL_TABLE);

    Cursor cursor = builder.query(databaseOpenHelper.getReadableDatabase(),
            columns, selection, selectionArgs, null, null, null);

    if (cursor == null) {
        return null;
    } else if (!cursor.moveToFirst()) {
        cursor.close();
        return null;
    }
    return cursor;
}

ابحث عن طلب بحث عن طريق الاتصال بالرقم getWordMatches(). يتم عرض أي نتائج مطابقة في Cursor يمكنك تصفّحها أو استخدامها لإنشاء ListView. يستدعي هذا المثال getWordMatches() في طريقة handleIntent() للنشاط القابل للبحث. تذكَّر أنّ النشاط القابل للبحث يتلقّى طلب البحث داخل نية ACTION_SEARCH كإضافة، وذلك بسبب فلتر النيّة الذي سبق أن أنشأته:

Kotlin

private val db = DatabaseTable(this)

...

private fun handleIntent(intent: Intent) {

    if (Intent.ACTION_SEARCH == intent.action) {
        val query = intent.getStringExtra(SearchManager.QUERY)
        val c = db.getWordMatches(query, null)
        // process Cursor and display results
    }
}

Java

DatabaseTable db = new DatabaseTable(this);

...

private void handleIntent(Intent intent) {

    if (Intent.ACTION_SEARCH.equals(intent.getAction())) {
        String query = intent.getStringExtra(SearchManager.QUERY);
        Cursor c = db.getWordMatches(query, null);
        // process Cursor and display results
    }
}