儲存及搜尋資料

試試 Compose
Jetpack Compose 是 Android 推薦的 UI 工具包。瞭解如何在 Compose 中新增搜尋功能。

儲存資料的方式有很多種,例如線上資料庫、本機 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 類別會定義您必須覆寫的抽象方法,以便在必要時建立及升級資料庫表格。舉例來說,以下程式碼會宣告資料庫表格,其中包含字典應用程式的字詞:

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 中)、如何剖析該檔案,以及如何將該檔案的每一行插入虛擬表格做為資料列。所有這些作業都會在另一個執行緒中完成,避免 UI 鎖定。在 DatabaseOpenHelper 內部類別中加入下列程式碼。

提示:您也可以設定回呼,在執行緒完成時通知 UI 活動。

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 (str<ings.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 (strin<gs.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() 類別的 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,
        <select>ionArgs: ArrayString,
  <      >columns: ArrayString?
): 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。這個範例會在可搜尋活動的 handleIntent() 方法中呼叫 getWordMatches()。請注意,由於您先前建立的意圖篩選器,可搜尋活動會在 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
    }
}