Adfs microsoft android kotlin

База данных на SharedPreferences — Android Kotlin

Статья написана об учебном проекте, повторять такое на проде нельзя, как минимум это небезопасно. Намеренно используется не самая лучшая реализация тех или иных вещей, чтобы можно было подискутировать в комментариях.

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

Наверное, в вашей голове возник вопрос: «В каких ситуациях может понадобиться использование SharedPreferences, если абсолютно всё можно хранить в Базе Данных?»

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

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

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

  • getPreferences() — внутри активности, чтобы обратиться к определённому для активности предпочтению,
  • getSharedPreferences() — внутри активности, чтобы обратиться к предпочтению на уровне приложения.

Все эти методы возвращают экземпляр класса 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. (Хотел бы услышать ваше мнение по поводу классов. Какой класс вы бы предпочли использовать в данной ситуации?)

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

Создадим и проинициализируем глобальные переменные класса:

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

Если поле не создано, то возвращается 0.

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

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

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

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

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

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

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

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

Читайте также:  Super mario land 3 android

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

Подобные сеттеры нужно создать для каждого ключа JSON объекта.

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

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

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

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

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

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

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

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

Поздравляю, оно работает! (P.S. lateinit в Kotlin советуют не использовать)

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

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

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

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

Если у вас есть замечания по коду — пишите, обсудим в комментариях.

Источник

Более безопасный способ сбора потоков данных из пользовательских интерфейсов Android

В приложении для Android потоки Kotlin обычно собираются из пользовательского интерфейса для отображения обновлений данных на экране. Однако, собирая эти потоки (flows) данных, следует убедиться, что не приходится выполнять больше работы, чем необходимо, тратить ресурсы (как процессора, так и памяти) или допускать утечку данных, когда представление переходит в фоновый режим.

Читайте также:  Что использовать звонок android

В этой статье вы узнаете, как API Lifecycle.repeatOnLifecycle и Flow.flowWithLifecycle защищают вас от пустой траты ресурсов и почему их лучше использовать по умолчанию для сбора потоков данных из пользовательского интерфейса.

Неэффективное использование ресурсов

Рекомендуется предоставлять API Flow с нижних уровней иерархии вашего приложения, независимо от деталей имплементации производителя потока данных. При этом следует также безопасно собирать их.

Холодный поток, поддерживаемый каналом или использующий операторы с буферами, такие как buffer, conflate, flowOn или shareIn, небезопасно собирать с помощью некоторых из существующих API, таких как CoroutineScope.launch, Flow .launchIn или LifecycleCoroutineScope.launchWhenX, за исключением случаев, если вы вручную отменяете Job, запустивший корутину, когда активность переходит в фон. Эти API сохранят производителя стандартного потока активным, пока он будет эмитировать элементы в буфер в фоновом режиме, таким образом будут расходоваться ресурсы.

Примечание: Холодный поток — это тип потока, который по требованию выполняет блок кода производителя, когда необходимо собрать данные для нового подписчика.

Например, рассмотрим этот поток, который выдает обновления местоположения с помощью callbackFlow:

Примечание: Внутри callbackFlow использует канал, который концептуально очень похож на очередь блокировки и имеет емкость по умолчанию 64 элемента.

Сбор этого потока из пользовательского интерфейса с помощью любого из вышеупомянутых API обеспечивает передачу местоположений, даже если представление не отображает их в пользовательском интерфейсе! См. пример ниже:

lifecycleScope.launchWhenStarted приостанавливает выполнение корутины. Новые местоположения не обрабатываются, но производитель callbackFlow тем не менее продолжает отправлять местоположения. Использование API lifecycleScope.launch или launchIn еще более опасно, поскольку представление продолжает использовать местоположения, даже если оно находится в фоновом режиме! Что потенциально может привести к отказу вашего приложения.

Чтобы решить эту проблему с этими API, вам нужно будет вручную отменить сбор данных, когда представление перейдет в фоновый режим, чтобы отменить callbackFlow и избежать такого, когда провайдер местоположений будет эмитировать элементы и тратить ресурсы. Например, вы можете сделать что-то вроде следующего:

Это хорошее решение, но это шаблонный код, друзья! И если существует всеобщая истина о разработчиках Android, то она такова, что мы совершенно не любим писать шаблонный код. Одно из самых больших преимуществ отказа от написания шаблонного кода заключается в том, что при небольшом количестве кода меньше шансов совершить ошибку!

Lifecycle.repeatOnLifecycle

Теперь, когда мы пришли к единому мнению и знаем, где кроется проблема, настало время придумать решение. Решение должно быть 1) простым, 2) дружественным или легким для запоминания/понимания, и, что более важно, 3) безопасным! Оно должно работать для всех случаев использования, независимо от деталей имплементации потока.

Без лишних слов, API, который вы должны использовать, это Lifecycle.repeatOnLifecycle , доступный в библиотеке lifecycle-runtime-ktx.

Примечание: Эти API доступны в библиотеке lifecycle:lifecycle-runtime-ktx:2.4.0-alpha01 или более поздней версии.

Взгляните на следующий код:

repeatOnLifecycle — это функция приостановки, принимающая Lifecycle.State в качестве параметра, который используется для автоматического создания и запуска новой корутины с переданным ей блоком, когда жизненный цикл достигает этого state , и отмены текущей корутины, выполняющей этот блок, когда жизненный цикл падает ниже state .

Это позволяет обойтись без использования шаблонного кода, поскольку код, для отмены корутины, когда она больше не нужна, автоматически выполняется функцией repeatOnLifecycle . Как вы могли догадаться, рекомендуется вызывать этот API в методах onCreate активности или onViewCreated фрагмента, чтобы избежать неожиданного поведения. Смотрите пример ниже с использованием фрагментов:

Важно: Фрагменты всегда должны использовать viewLifecycleOwner для запуска обновлений пользовательского интерфейса. Однако это не относится к DialogFragments , у которых иногда может не быть View. Для DialogFragments можно использовать lifecycleOwner .

Примечание: Эти API доступны в библиотеке lifecycle:lifecycle-runtime-ktx:2.4.0-alpha01 или более поздней версии.

Копнем глубже!

repeatOnLifecycle приостанавливает вызывающую корутину, повторно запускает блок, когда жизненный цикл переходит в таргет state и из него в новую корутину, и возобновляет вызывающую корутину, когда Lifecycle уничтожается. Последний пункт очень важен: вызывающая программа, которая вызывает repeatOnLifecycle , не возобновит выполнение до тех пор, пока жизненный цикл не будет уничтожен.

Визуальная диаграмма

Возвращаясь к началу, сбор locationFlow непосредственно из корутины, запущенной с помощью lifecycleScope.launch , был опасен, поскольку он продолжался, даже когда представление находилось в фоновом режиме.

repeatOnLifecycle предотвращает трату ресурсов и сбои приложения, поскольку останавливает и перезапускает сбор потока, когда жизненный цикл переходит в таргет-состояние и обратно.

Разница между использованием и неиспользованием API repeatOnLifecycle

Flow.flowWithLifecycle

Вы также можете использовать оператор Flow.flowWithLifecycle , когда у вас есть только один поток для сбора. Этот API использует repeatOnLifecycle , эмитирует элементы и отменяет стандартного производителя, когда Lifecycle переходит в таргет-состояние и выходит из него.

Примечание: Это API использует оператор Flow.flowOn(CoroutineContext) в качестве прецедента, поскольку Flow.flowWithLifecycle изменяет CoroutineContext , используемый для сбора восходящего потока, оставляя при этом нисходящий поток незатронутым. Также, подобно flowOn , Flow.flowWithLifecycle добавляет буфер на случай, если потребитель не успевает за производителем. Это связано с тем, что его имплементация использует callbackFlow .

Настройка производителя стандартного потока

Если вы используете эти API, остерегайтесь горячих потоков, которые могут тратить ресурсы, хотя данные потоки никто не собирает! Имейте ввиду, что для них есть несколько подходящих случаев использования, и если это необходимо, документируйте. Наличие активного производителя стандартного потока в фоновом режиме, даже если он тратит ресурсы, может быть полезно для некоторых сценариев использования: вы мгновенно получаете свежие данные, а не наверстываете упущенное и временно показываете несвежие данные. В зависимости от сценария использования, решите, должен ли производитель быть всегда активным или нет.

Читайте также:  Как найти скачанный файл андроид

API MutableStateFlow и MutableSharedFlow предоставляют поле subscriptionCount , которое можно использовать для остановки производителя стандартного потока, когда subscriptionCount равно нулю. По умолчанию они будут поддерживать производителя активным до тех пор, пока объект, содержащий экземпляр потока, находится в памяти. Однако для этого есть несколько подходящих случаев использования, например, UiState , передаваемый из ViewModel в UI с помощью StateFlow . Это нормально! Этот случай использования требует, чтобы ViewModel всегда предоставляла последнее состояние пользовательского интерфейса представлению.

Точно так же операторы Flow.stateIn и Flow.shareIn могут быть сконфигурированы для этого с правилами запуска общего доступа. WhileSubscribed() остановит производителя стандартного потока, если нет активных наблюдателей! Напротив, Eagerly или Lazily будут поддерживать базового производителя активным до тех пор, пока активен CoroutineScope, который они используют.

Примечание: API, показанные в этой статье, являются хорошим вариантом по умолчанию для сбора потоков из пользовательского интерфейса и должны использоваться независимо от деталей реализации потока. Эти API делают то, что должны: прекращают сбор, если пользовательский интерфейс не виден на экране. Это зависит от имплементации потока, должен ли он быть всегда активным или нет.

Безопасный сбор Flow в Jetpack Compose

Функция Flow.collectAsState используется в Compose для сбора потоков из компонуемых объектов и представления значений в виде State для обновления пользовательского интерфейса (UI) Compose . Даже если Compose не перекомпоновывает UI, когда активность хоста или фрагмента находится в фоновом режиме, производитель потоков все еще активен и может тратить ресурсы. Compose может испытывать аналогичную проблему, что и система представления (View).

При сборе потоков в Compose используйте оператор Flow.flowWithLifecycle следующим образом:

Обратите внимание, что вам требуется поток remember, который знает о жизненном цикле, с locationFlow и lifecycleOwner в качестве ключей, чтобы всегда использовать один и тот же поток, если только один из ключей не изменится.

В Compose побочные эффекты должны выполняться в контролируемой среде. Для этого используйте LaunchedEffect, чтобы создать корутину, которая следует за жизненным циклом составного компонента. В ее блоке вы можете вызвать приостановку Lifecycle.repeatOnLifecycle , если вам нужно, чтобы она повторно запустила блок кода, когда жизненный цикл хоста находится в определенном State .

Сравнение с LiveData

Вы могли заметить, что этот API ведет себя аналогично LiveData, и это действительно так! LiveData знает о Lifecycle , и возможность перезапуска делает его идеальным для наблюдения за потоками данных из пользовательского интерфейса. Это также справедливо для API Lifecycle.repeatOnLifecycle и Flow.flowWithLifecycle !

Сбор потоков с помощью этих API является естественной заменой LiveData в приложениях, работающих только на Kotlin. Если вы используете эти API для сбора потоков, LiveData не имеет никаких преимуществ перед корутинами и потоками. Более того, потоки более гибкие, так как их можно собирать из любого Dispatcher , и они могут работать со всеми его операторами. В отличие от LiveData , который имеет ограниченное количество доступных операторов и значения которых всегда наблюдаются из потока UI.

Поддержка StateFlow в связывании данных

С другой стороны, одна из причин, по которой вы, возможно, используете LiveData, заключается в том, что он поддерживается в привязке данных. Так вот, StateFlow также имеет такую поддержку! Для получения дополнительной информации о поддержке StateFlow в привязке данных ознакомьтесь с официальной документацией.

Используйте API Lifecycle.repeatOnLifecycle или Flow.flowWithLifecycle для безопасного сбора потоков данных из пользовательского интерфейса в Android.

Перевод материала подготовлен в преддверии старта курса «Android Developer. Basic».

Источник

Оцените статью