Skip to content

MixKage/DataBasePreferences

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 

Repository files navigation

Для хранения большого количества информации в Android используется DataBase (SQLite, FireBase), а для простого хранения настроек SharedPreferences. Вопрос, можно ли использовать SharedPreferences как своего рода Базу Данных и как это сделать?

Зачем всё это? Зачем?

Наверное, в вашей голове возник вопрос: “В каких ситуациях может понадобится использование SheredPreferences, если абсолютно всё можно хранить в Базе Данных?” Отвечаю: и правда, абсолютно всю информацию можно хранить в таком виде. Иметь таблицу с настройками приложения (например, тёмная тема: вкл/выкл), а в другой таблице хранить ту самую информацию, которую отображает само приложение. Но эффективно ли это? Если информации много и SQL запросы помогают вам, то да, но не стоит забывать, что Базы Данных в большинстве своих случаев, являются сложным механизмом, из-за чего производительность вашего приложения может быть занижена. Если информации не так много, то гораздо выгоднее использовать SharedPreferences.

Краткая теория как использовать SharedPreferences

Чтобы получить экземпляр класса SharedPreferences в коде приложения используются три метода:

  • getPreferences() — внутри активности, чтобы обратиться к определённому для активности предпочтению
  • getSharedPreferences() — внутри активности, чтобы обратиться к предпочтению на уровне приложения
  • getDefaultSharedPreferences() — из объекта PreferencesManager, чтобы получить общедоступную настройку, предоставляемую Android. (Является устаревшим методом)

Все эти методы возвращают экземпляр класса SharedPreferences, из которого можно получить информацию с помощью ряда методов:

  • getBoolean(String key, boolean defValue)
  • getFloat(String key, float defValue)
  • getInt(String key, int defValue)
  • getLong(String key, long defValue)
  • getString(String key, String defValue)

По умолчанию используется MODE_PRIVATE - только приложение имеет доступ к настройкам. Также существуют другие режимы, с которыми практически не встречался.

  • MODE_APPEND - присоединяет новые настройки к существующим
  • MODE_ENABLE_WRITE_AHEAD_LOGGING
  • MODE_MULTI_PROCESS
  • MODE_WORLD_READABLE - позволяет другим приложениям читать настройки
  • MODE_WORLD_WRITEABLE - позволяет другим приложениям записывать новые настройки

Эти файлы настроек хранятся в каталоге /data/data/имя_пакета/shared_prefs/имя.xml.

Начинаем эксперимент

Представим такой проект: на вход поступает json файл, в нём хранится информация о валютах (имя, стоимость и т.д.). Нам необходимо сохранить эти данные и при перезапуске приложения показать их, не читая json (все данные взять из нашей БД). Возможные взаимодействие с карточкой валюты: изменение любого поля. Все исходники, вы сможете найти на моём GitHub в конце статьи. Создадим класс и передадим ему в аргументы context для SharedPreferences. class DataBasePreferences(var context: Context)

Мы создадим 2 пространства. 1 – будет хранить информацию о нашей DB и будет статична по своей размерности. В ней будет храниться количество наших карточек с валютами. 2 поле – должно хранить те самые карточки, а значит – уметь динамически изменять свой размер. Как оно будет работать? SharedPreferences хранит данные, как грубо говоря Map (ключ->значение) и вся информация находится в “таблице данных”. Благодаря имени данной таблицы, мы и будем передвигаться по карточкам. Новое имя таблицы = новая карточка. Таблицы мы будем называть цифрами, это будет выполнять роль их id. Так будет намного проще передвигаться по ним. Создадим и проинициализируем глобальные переменные класса

private var prefs: SharedPreferences
private val prefsSetting = context.getSharedPreferences("databaseInfo", Context.MODE_PRIVATE)
private var editor: SharedPreferences.Editor
private val editorSettings = prefsSetting.edit()
private var indexNow = 0

init {
    prefs = context.getSharedPreferences(getSizeInt().toString(), Context.MODE_PRIVATE)
    editor = prefs.edit()
}

А вот и первая наша функция. Она позволяет узнать размер 2 поля (сколько таблиц оно содержит)

fun getSizeInt(): Int {
    return prefsSetting.getInt("size", 0)
}

Если поле не создано, то возвращается 0. Так геттер создали, теперь время сеттера. Для этого создадим две функции.

private fun sizeUp() {
    editorSettings.putInt("size", getSizeInt() + 1).apply()
}

private fun sizeDown() {
    editorSettings.putInt("size", getSizeInt() - 1).apply()
}

ВНИМАНИЕ: если в конце не написать .apply() изменения не сохранятся!

Вторая область

Теперь, перейдём к 2 области, нам необходимо уметь перемещаться по нашим “таблицам”, для этого реализуем такую функцию.

private fun updatePrefs(index: Int) {
    if (indexNow != index) {
        indexNow = index
        prefs = context.getSharedPreferences(index.toString(), Context.MODE_PRIVATE)
        editor = prefs.edit()
    }
}

Думаю, стоит немножко объяснить код. В аргументах мы принимаем название таблицы (её порядковый номер) после чего, можно заметить проверку, которая сравнивает текущий порядковый номер таблицы с полученным, в случае если они не равны, происходит переприсвоение текущего названия таблицы и её открытие. Доступ к чтению таблицы мы получаем благодаря глобальной переменной prefs, а редактирование – editor.

Добавление карточек к БД

Что же, думаю можно перейти к заполнению нашей таблицы.

fun addJSONObject(_input: JSONObject) {
    sizeUp()
    updatePrefs(getSizeInt())
    setId(_input.getString("ID"))
    setNumCode(_input.getString("NumCode"))
    setCharCode(_input.getString("CharCode"))
    setNominal(_input.getString("Nominal"))
    setName(_input.getString("Name"))
    setValue(_input.getString("Value"))
    setPrevious(_input.getString("Previous"))
}

На вход мы получаем JSON объект, после чего увеличиваем количество всех наших таблиц, открываем новую таблицу и используем сеттеры. Ниже представлен код одного из таких сеттеров.

fun setId(_input: String, _id: Int? = -1) {
    _id?.let {
        if (_id != -1)
            updatePrefs(_id)
        editor.putString("ID", _input).apply()
    }
}

Тут без объяснений не обойтись. Функция принимает 2 аргумента, причём последний аргумент является необязательным (если его не изменять, он по умолчанию будет равен -1), а также может хранить null. Дальше идёт конструкция _id?.let{}, она позволяет запустить фрагмент кода (в фигурных скобках), если переменная не равна null. После чего идёт проверка, стандартное ли значение 2 аргумента. Если на вход, мы получили номер таблицы -> открыть нужную таблицу. В конце присваиваем новое значение по ключу “ID” и не забываем применить все изменения с помощью .apply(). Для чего мы добавили возможность переменной _id хранить null, объясню чуть позже. Подобные сеттеры нужно создать для каждого ключа JSON объекта.

Чтение карточки

Сеттеры отдельных ключей есть, а это значит, что наша самодельная БД уже умеет сохранять информацию, осталось научить её считывать. Ниже приведён пример одного из геттеров.

fun getId(_id: Int? = -1): String? {
    _id?.let {
        if (_id != -1)
            updatePrefs(_id)
        return prefs.getString("ID", "").toString()
    }
    return null
}

Мы можем наблюдать, как функция может принимать пользовательское имя таблицы, так и использовать значение по умолчанию. Если пользователь не вводил имя таблицы, то информация будет взята из текущей.

Поиск карточки

Замечательно, наша БД на SharedPreferences умеет сохранять и выводить данные, правда осталась одна нерешённая проблема. Предположим, при работе с приложением пользователь захочет изменить определённую карточку нажав на неё, для этого у нас уже существуют Сеттеры, но откуда нам знать, какую именно таблицу нужно открыть для работы? Предположим, у нас есть возможность получить какую-нибудь информацию с карточки, например сокращённое имя валюты. Получается, нам необходимо пройтись по всем существующим таблицам, найти совпадение и вывести название данной таблицы. Это долгая операция, поэтому таких ситуаций лучше не создавать, например, пусть каждая карточка в интерфейсе будет хранить свой локальный номер, который будет совпадать с названием таблицы, но если такой возможности нет, то в бой идёт наша новая “тяжёлая” функция.

fun searchIdCardByNumCode(_input: String): Int? {
    for (index in 1..getSizeInt()) {
        updatePrefs(index)
        if (_input == getNumCode()) {
            return index
        }
    }
    return null
}

В случае, если совпадений не найдено, будет возвращено null. Т.к. данная функция, скорее всего, будет использована в комбинации с сетерами и гетерами, мы и добавили в них поддержку null. Данная функция ищет совпадения по NumCode значению и нам ничего не мешает сделать аналогичные функции для оставшихся ключей. Отлично, простейшая БД на SharedPreferences готова, давайте посмотрим на неё в действии.

DataBase на SharedPreferences в действии

Перейдём в наш MainActivity и создадим экземпляр нашего класса, именовав его db. После чего получим JSONobject из памяти Android и в функции “createDB”, запишем всю интересующую нас информацию в нашу db, затем прочитаем некоторую информацию карточки в функции readInfoDB, выпишем её, следом изменим внутри лежащую информацию с помощью функции editInfoDB и снова распечатаем результаты.

class MainActivity : AppCompatActivity() {
    private lateinit var db: DataBasePreferences

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        db = DataBasePreferences(this)
        createDB()
        val idTable = db.searchIdCardById("R01035")
        readInfoDB(idTable)
        editInfoDB(idTable)
        readInfoDB(idTable)
    }

    private fun createDB() {
        val jsonRoot = JSONObject(resources.openRawResource(R.raw.daily_json)
            .bufferedReader().use { it.readText() })
        val currency = jsonRoot.getJSONObject("Valute")
        val keys = currency.keys()
        for (index in 0 until currency.length())
            db.addJSONObject(currency.getJSONObject(keys.next()))
    }

    private fun editInfoDB(_id: Int?) {
        _id?.let {
            db.setCharCode("LP", _id)
            db.setName("@lonely_programmer")//Заметьте, я могу не указывать id явно, т.к. работаю с одной карточкой
            db.setNumCode("1234", _id)
        }
    }

    private fun readInfoDB(_id: Int?) {
        _id?.let {
            Log.d("checkDBCHARCODE: ", db.getCharCode(_id).toString())
            Log.d("checkDBNAME: ",db.getName(_id).toString())
            Log.d("checkDBNUMCODE: ",db.getNumCode(_id).toString())
        }
    }
}

Поздравляю, оно работает!

Если вдруг Logcat не работает, попробуйте его перезагрузить. (обычно такое происходит из-за нехватки мощности компьютера)

Итог

Какие могут возникнуть трудности в будущем?

  • Удаление карточки-таблицы, т.к. это не связной список, при удалении все элементы будут сдвигаться, а это в свою очередь долгий процесс.
  • Добавление карточки не в конец списка приведёт к сдвигу всех элементов.
  • Нельзя наглядно увидеть, что хранится в Базе Данных.
  • При стирании кэша приложения, всё сотрётся.
  • При замене всех параметров таблицы, есть вероятность её потерять, т.к. неизвестно, по каким параметрам искать таблицу.

Это был весёлый и интересный эксперимент, который и правда сработал. Можно ли это использовать на проде? Конечно же нет, но надеюсь данная статья показала вам, что при должном желании, возможно абсолютно всё. Если эта тема кажется вам интересной, дайте знать, сделаю 2 часть, где мы добавим много действительно крутых функций от старших братьев и прокачаем нашу DataBase. Например, было бы неплохо реализовать её через список, чтобы не тратить так много времени на добавление и удаление карточки. Или же добавить возможность добавления любой информации (использовать любые ключи), в таком случае, это и правда сможет практически заменить SQLite.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published