состав и структура данных мобильного приложения
Архитектура Android-приложений. Часть III — основные части приложения
Итак, мы уже говорили о происхождении архитектуры ОС Android и о шаблонах, реализованных в этой архитектуре. Теперь настала пора поговорить о том, из чего состоит Android-приложение.
В этой статье будут представлены основные «персонажи» архитектуры Android-приложения.
В общем случае, Android-приложение состоит из:
Java классы
На следующей диаграмме представлена иерархия основных классов из Android SDK, с которыми предстоит иметь дело разработчику:
На самом деле классов намного больше, но это основные. Выделенные жёлтым — те, с которыми разработчик работает непосредственно (в частности, наследуются от них). Остальные так же важны, но они реже используются напрямую.
View — базовый класс для всех виджетов пользовательского интерфейса (GUI widgets). Интерфейс Android-приложения представляет собой дерево экземпляров наследников этого класса. Можно создать это дерево программно, но это неправильно. Пользовательский интерфейс определяется с помощью XML (файлы слоёв, layout files), а во время исполнения автоматически превращается (inflate, термин Android) в дерево соответствующих объектов.
Класс Activity и его подклассы содержат логику, лежащую за пользовательским интерфейсом. При ближайшем рассмотрении этот класс соответствует ViewModel в архитектурном шаблоне Model-View-ViewModel (MVVM). Отношение между подклассом Activity и пользовательским интерфейсом — это отношение один к одному; обычно каждый подкласс Activity имеет только один связанный с ним слой пользовательского интерфейса, и наоборот. Activity имеет жизненный цикл.
В течении жизненного цикла Activity может находиться в одном из трёх состояний:
Класс ContentProvider и его подклассы представляют model в архитектуре MVVM. В большинстве практических случаев это обёртка над базой данных SQLite с немного причудливым способом доступа на основе URI. Теоретически, никто не мешает разработчику создать ContentProvider на основе чего-то ещё, кроме базы данных. Тем не менее, существующий метод query() контент-провайдера возвращает объект Cursor, который крайне похож на JDBC ResultSet интерфейсом и тем, как он работает. Поэтому вряд ли кто-то усомнится, что настоящее назначение контент-провайдеров — инкапсулировать базу данных.
Я не знаю, как комманда Android пришла к такому дизайну, но, по-моему, здесь соединены две хороших, но не слишком совместимых идеи.
И вот почему я так считаю. Основная идея контент-провайдеров, похоже, базируется на архитектуре AJAX приложений. AJAX приложения обычно используют архитектуру MVVM, где модель представлена как URI на стороне сервера (тем не менее, это изменилось с поялвинем HTML5, который позволяет хранить данные локально). В самом деле, тот факт, что контент-провайдеры запрашиваются с помощью URI и создают расширение с помощью типов MIME указывает на то, что в основе лежит AJAX. Напомню, ребята из Google создали большое количество AJAX приложений, таких как Gmail, Google Docs и т.п., поэтому вполне естественно, что идеи заимствовались из архитектуры AJAX.
Возможно, кто-то ещё пришёл с ещё одной отличной идеей: как было бы здорово иметь полноценную реляционную базу на мобильном устройстве! (замечу, это было примерно в 2005 году, когда мобильные телефоны были намного слабее, чем сейчас). И, как результат, они соединили две хороших идеи в один класс ContentProvider. Как это обычно и случается в разработке ПО, соединение двух хороших идей не всегда даёт в результате хорошую идею; в случае Android мы имеем несколько обескураживающий дизайн контент-провайдеров.
Класс Service и его подклассы я затрудняюсь как-то классифицировать. Я думаю, ребята из Google испытывают те же трудности (прочтите, пожалуйста, их документацию). Их классификация, в основном, говорит, чем этот класс не является. Я лично думаю, что сервис — это разновидность Model, обслуживающая несколько иные варианты использования, нежели ContentProvider.
По-моему, архитектурный дизайн Android Service навеян сервисами OSGI.
Думаю, сервисы были созданы ребятами из Google как решение логической проблемы, возникшей из-за модели потоков Android.
Подумайте над этим: Activity активно и выполняется только когда его пользовательский интерфейс находится на переднем плане. Как только интерфейс другого Activity закрывает собой текущее, последнее останавливается, даже если оно что-то делало. А что, если вам нужно выполнять некую операцию, даже если процесс, которые её выполняет, не на переднем плане? С помощью Activity вы не сможете этого сделать. Вы не сможете это сделать и с помощью ContentProvider, поскольку у них нет собственного жизненного цикла, и они могут выполняться только пока Activity, использующее его, активно.
И тут на помощь приходят сервисы. Они могут выполняться даже когда процесс, в котором они работают, не на переднем плане. Так, если вы разрабатываете активити, выполняющее растянутую во времени операцию, которая должна завершиться даже работая в фоне, вы должны создать Service, реализующий эту операцию, и запустить его из Activity.
Service так же имеет жизненный цикл. Это означает, что он может быть инстанцирован и запущен Android-приложением по некому условию (мы обсудим это позже).
Как я уже упоминал, Service, как model, приследует более общие цели, нежели ContentProvier. Он может использовать базу данных, но его API не связано с БД, как в случае ContentProvider. В большинстве случаев сервисы используются для связи с внешними серверами.
Класс BroadcastReceiver и его подклассы представляют собой «подписчика» в механизме взаимодейтсвия издатель/подписчик, реализованном в архитектуре Android.
Мы уже говорили о механизмах взаимодействия в предыдущей статье.
Конечно, разработчик под Android не ограничен одним только расширением классов из Android SDK. Он может писать собственные классы так, как захочет. Но все они будут только хелперами («helper classes») для классов из Andoird SDK.
Манифест Android
Манифест Android — ещё одна важная часть Android-приложения. Идея была навеяна манифестами плагинов для Eclipse.
Манифест Android представляет собой XML файл и выполняет несколько функций. Вот как их описывает Google:
Обратите внимание на второй пункт. Имеется ввиду, что если некий класс расширяет Activity, ContentProvider, BroadcastReceiver или Service в вашем приложении, этот класс не может быть использован до тех пор, пока он не описан в манифесте.
Ресурсы
Каждое современное GUI приложение в той или иной форме использует ресурсы. Android-приложения — не исключение. Они используют следующие типы ресурсов:
Давайте рассмотрим следующий пример. Файл с именем mybutton.png содержит картинку для кнопки. Разработчик совершает ошибку и набирает mybuton.png, ссылаясь на ресурс из кода. Как итог, код пытается использовать несуществующий ресурс, но компиляция пройдёт успешно. Ошибка может быть обнаружена только в ходе тестирования (а может и не быть обнаружена вовсе).
Ребята из Google нашли элегантное решение этой проблемы. При сборке Android-приложения генерируется специальный Java-класс с именем R (всего лишь одна буква). Этот класс содержит несколько static final наборов данных. Каждый такой набор данных — ссылка на отдельный ресурс. Эти ссылки используются в коде приложения для связи с ресурсами. Теперь каждая ошибка в ссылке на ресурсы проявляет себя в процессе компиляции.
Файлы
Android-приложение использует несколько разных типов файлов:
API для работы с файлами реализован классом Context, от которого порождены классы Activity и Service. Этот класс уже обсуждался нами здесь.
На сегодня это всё. В следующей статье мы поговорим о том, как различные части Android-приложения взаимодействуют между собой.
Руководство по организации архитектуры Android приложения
Привет, Хабр! Представляю вашему вниманию вольный перевод «Руководство по архитектуре приложения (Guide to app architecture)» из JetPack. Все замечания по переводу прошу оставлять в комментариях, и они будут исправлены. Так же для всех будут полезны комментарии от тех кто использовал представленную архитектуру с рекомендациями её использования.
Это руководство охватывает лучшие практики и рекомендуемую архитектуру для создания надежных приложений. Эта страница предполагает базовое знакомство с Android Framework. Если вы новичок в разработке приложений для Android, ознакомьтесь с нашими руководствами для разработчиков, чтобы начать работу и узнать больше о концепциях, упомянутых в этом руководстве. Если вы интересуетесь архитектурой приложений и хотели бы ознакомиться с материалами этого руководства с точки зрения программирования на Kotlin, ознакомьтесь с курсом Udacity «Разработка приложений для Android с помощью Kotlin».
Опыт пользователя мобильного приложения
В большинстве случаев настольные приложения имеют единую точку входа с рабочего стола или программы запуска, а затем запускаются как единый монолитный процесс. Приложения на Android имеют гораздо более сложную структуру. Типичное приложение для Android содержит несколько компонентов приложения, включая Activities, Fragments, Services, ContentProviders и BroadcastReceivers.
Вы объявляете все или некоторые из этих компонентов приложения в манифесте приложения. Затем ОС Android использует этот файл, чтобы решить, как интегрировать ваше приложение в общий пользовательский интерфейс устройства. Учитывая, что правильно написанное приложение Android содержит несколько компонентов, и пользователи часто взаимодействуют с несколькими приложениями за короткий промежуток времени, приложения должны адаптироваться к различным типам рабочих процессов и задач, управляемых пользователями.
Например, рассмотрим, что происходит, когда вы делитесь фотографией в своем любимом приложении для социальных сетей:
Помните, что мобильные устройства также ограничены в ресурсах, поэтому в любой момент операционная система может уничтожить некоторые процессы приложения, чтобы освободить место для новых.
Учитывая условия этой среды, компоненты вашего приложения могут запускаться по отдельности и не по порядку, а операционная система или пользователь могут уничтожить их в любое время. Поскольку эти события не находятся под вашим контролем, вы не должны хранить какие-либо данные или состояния в ваших компонентах приложения, и ваши компоненты приложения не должны зависеть друг от друга.
Общие архитектурные принципы
Если вы не должны использовать компоненты приложения для хранения данных и состояния приложения, как вы должны разрабатывать свое приложение?
Разделение ответственности
Самый важный принцип, которому нужно следовать, — это разделение ответственности. Распространена ошибка, когда вы пишете весь свой код в Activity или Fragment. Это классы пользовательского интерфейса которые должны содержать только логику обрабатывающую взаимодействие пользовательского интерфейса и операционной системы. Как можно больше разделяя ответственность в этих классах (SRP), вы можете избежать многих проблем, связанных с жизненным циклом приложения.
Управление пользовательским интерфейсом из модели
Другой важный принцип заключается в том, что вы должны управлять своим пользовательским интерфейсом из модели, предпочтительнее из постоянной модели. Модели — это компоненты, которые отвечают за обработку данных для приложения. Они не зависят от объектов View и компонентов приложения, поэтому на них не влияют жизненный цикл приложения и связанные с ним проблемы.
Постоянная модель идеально подходит по следующим причинам:
Рекомендуемая архитектура приложения
Этот раздел демонстрирует как структурировать приложение, используя компоненты архитектуры, работая в сквозном сценарии использования.
Примечание. Невозможно иметь один способ написания приложений, который лучше всего подходит для каждого сценария. При этом рекомендованная архитектура является хорошей отправной точкой для большинства ситуаций и рабочих процессов. Если у вас уже есть хороший способ написания приложений для Android, соответствующий общим архитектурным принципам, менять его не стоит.
Представьте, что мы создаем пользовательский интерфейс, который показывает профиль пользователя. Мы используем приватный API и REST API для извлечения данных профиля.
Обзор
Для начала рассмотрим схему взаимодействия модулей архитектуры готового приложения:
Обратите внимание, что каждый компонент зависит только от компонента на один уровень ниже его. Например, Activity и Fragments зависят только от модели представления. Repository является единственным классом, который зависит от множества других классов; в этом примере хранилище зависит от постоянной модели данных и удаленного внутреннего источника данных.
Этот дизайн паттерн создает последовательный и приятный пользовательский опыт. Независимо от того, вернется пользователь к приложению через несколько минут после его закрытия или спустя несколько дней, он мгновенно увидит информацию пользователя о том, что приложение сохраняется локально. Если эти данные устарели, модуль хранилища приложения начинает обновлять данные в фоновом режиме.
Создаём пользовательский интерфейс
Для управления пользовательским интерфейсом наша модель данных должна содержать следующие элементы данных:
Объект ViewModel предоставляет данные для определенного компонента пользовательского интерфейса, таких как fragment или Activity, и содержит бизнес-логику обработки данных для взаимодействия с моделью. Например, ViewModel может вызывать другие компоненты для загрузки данных и может пересылать запросы пользователей на изменение данных. ViewModel не знает о компонентах пользовательского интерфейса, поэтому на него не влияют изменения конфигурации, такие как воссоздание Activity при повороте устройства.
Теперь мы определили следующие файлы:
Теперь, когда у нас есть эти модули кода, как мы их соединяем? После того как пользовательское поле установлено в классе UserProfileViewModel, нам нужен способ информировать пользовательский интерфейс.
Примечание. SavedStateHandle позволяет ViewModel получить доступ к сохраненному состоянию и аргументам связанного фрагмента или действия.
Теперь нам нужно сообщить нашему Фрагменту, когда получен пользовательский объект. Вот тут-то и появляется компонент архитектуры LiveData.
LiveData — это наблюдаемый держатель данных. Другие компоненты в вашем приложении могут отслеживать изменения объектов, используя этот держатель, не создавая явных и жестких путей зависимости между ними. Компонент LiveData также учитывает состояние жизненного цикла компонентов вашего приложения, таких как Activities, Fragments и Services, и включает логику очистки для предотвращения утечки объектов и чрезмерного потребления памяти.
Примечание. Если вы уже используете такие библиотеки, как RxJava или Agera, вы можете продолжать использовать их вместо LiveData. Однако при использовании библиотек и подобных подходов убедитесь, что вы правильно обрабатываете жизненный цикл своего приложения. В частности, убедитесь, что вы приостановили свои потоки данных, когда связанный LifecycleOwner остановлен, и уничтожили эти потоки, когда связанный LifecycleOwner был уничтожен. Вы также можете добавить артефакт android.arch.lifecycle: реактивные потоки, чтобы использовать LiveData с другой библиотекой реактивных потоков, такой как RxJava2.
Теперь модифицируем UserProfileFragment для наблюдения за данными во ViewModel и для обновления пользовательского интерфейса в соответствии с изменениями:
Каждый раз, когда данные профиля пользователя обновляются, вызывается обратный вызов onChanged(), и пользовательский интерфейс обновляется.
Получение данных
В этом примере мы предполагаем, что наш backend предоставляет REST API. Мы используем библиотеку Retrofit для доступа к нашему backend, хотя вы можете использовать другую библиотеку, которая служит той же цели.
Вместо этого наша ViewModel делегирует процесс извлечения данных новому модулю, хранилищу.
Модули репозитория обрабатывают операции с данными. Они предоставляют чистый API, так что остальная часть приложения может легко получить эти данные. Они знают, откуда взять данные и какие вызовы API следует выполнять при обновлении данных. Вы можете рассматривать репозитории как посредники между различными источниками данных, такими как постоянные модели, веб-службы и кэши.
Хотя модуль хранилища выглядит ненужным, он служит важной цели: он абстрагирует источники данных от остальной части приложения. Теперь наш UserProfileViewModel не знает, как извлекаются данные, поэтому мы можем предоставить модели представления данные, полученные из нескольких различных реализаций извлечения данных.
Примечание. Мы упустили случай сетевых ошибок для простоты. Для альтернативной реализации, которая выставляет ошибки и статус загрузки, см. Приложение: раскрытие статуса сети.
Управление зависимостями между компонентами
Для решения этой проблемы вы можете использовать следующие шаблоны проектирования:
Эти шаблоны позволяют вам масштабировать ваш код, потому что они предоставляют четкие шаблоны для управления зависимостями без дублирования или усложнения кода. Кроме того, эти шаблоны позволяют быстро переключаться между тестовыми и производственными реализациями выборки данных.
Подключите ViewModel и хранилище
Теперь мы модифицируем наш UserProfileViewModel для использования объекта UserRepository :
Кеширование
Эта конструкция является неоптимальной по следующим причинам:
Постоянные данные
Используя нашу текущую реализацию, если пользователь поворачивает устройство или уходит и немедленно возвращается в приложение, существующий пользовательский интерфейс становится видимым мгновенно, потому что хранилище извлекает данные из нашего кеша в памяти.
Тем не менее, что произойдет, если пользователь покинет приложение и вернется через несколько часов после того, как ОС Android завершит процесс? Полагаясь на нашу текущую реализацию в этой ситуации, нам нужно снова получить данные из сети. Этот процесс обновления не просто плохой пользовательский опыт; это также расточительно, потому что он потребляет ценные мобильные данные.
Вы можете решить эту проблему, кэшируя веб-запросы, но это создает ключевую новую проблему: что произойдет, если те же пользовательские данные будут отображены в запросе другого типа, например при получении списка друзей? Приложение будет отображать противоречивые данные, что в лучшем случае сбивает с толку. Например, наше приложение может отображать две разные версии данных одного и того же пользователя, если пользователь отправлял запрос списка друзей и однопользовательский запрос в разное время. Наше приложение должно было бы выяснить, как объединить эти противоречивые данные.
Правильный способ справиться с этой ситуацией — использовать постоянную модель. Нам на помощь приходит библиотека сохранения постоянных данных (БД) Room.
Room — это библиотека объектно-реляционного отображения (object-mapping), которая обеспечивает локальное сохранение данных с минимальным стандартным кодом. Во время компиляции он проверяет каждый запрос на соответствие вашей схеме данных, поэтому неработающие запросы SQL приводят к ошибкам во время компиляции, а не к сбоям во время выполнения. Room абстрагируется от некоторых базовых деталей реализации работы с необработанными таблицами SQL и запросами. Это также позволяет вам наблюдать за изменениями в данных БД, включая коллекции и запросы на соединение, выставляя такие изменения с помощью объектов LiveData. Он даже явно определяет ограничения выполнения, которые решают общие проблемы с потоками, такие как доступ к хранилищу в основном потоке.
Примечание. Если ваше приложение уже использует другое решение, такое как объектно-реляционное отображение SQLite (ORM), вам не нужно заменять существующее решение на Room. Однако, если вы пишете новое приложение или реорганизуете существующее приложение, мы рекомендуем использовать Room для сохранения данных вашего приложения. Таким образом, вы можете воспользоваться возможностями абстракции библиотеки и проверки запросов.
Чтобы использовать Room, нам нужно определить нашу локальную схему. Сначала мы добавляем аннотацию @Entity в наш класс модели данных User и аннотацию @PrimaryKey в поле id класса. Эти аннотации помечают User как таблицу в нашей базе данных, а id — как первичный ключ таблицы:
Затем мы создаем класс базы данных, реализуя RoomDatabase для нашего приложения:
Обратите внимание, что UserDatabase является абстрактной. Библиотека Room автоматически обеспечивает реализацию этого. Подробности смотрите в документации по Room.
Теперь нам нужен способ вставки пользовательских данных в базу данных. Для этой задачи мы создаем объект доступа к данным (DAO).
Обратите внимание, что метод load возвращает объект типа LiveData. Room знает, когда база данных изменена, и автоматически уведомляет всех активных наблюдателей об изменении данных. Поскольку Room использует LiveData, эта операция эффективна; он обновляет данные только при наличии хотя бы одного активного наблюдателя.
Примечание: Room проверяет недействительность на основе модификаций таблицы, что означает, что она может отправлять ложные положительные уведомления.
Если пользователи вернутся через несколько дней, то приложение использующее эту архитектуру, вполне вероятно, покажет устаревшую информацию, пока хранилище не получит обновленную информацию. В зависимости от вашего варианта использования вы можете не отображать устаревшую информацию. Вместо этого вы можете отобразить данные-заполнители (placeholder data), которые показывают фиктивные значения и указывают, что ваше приложение в настоящее время загружает и загружает актуальную информацию.
Единственный источник правды
Обычно разные конечные точки REST API возвращают одни и те же данные. Например, если у нашего бэкэнда есть другая конечная точка, которая возвращает список друзей, один и тот же пользовательский объект может исходить из двух разных конечных точек API, возможно, даже с использованием разных уровней детализации. Если бы UserRepository возвращал ответ от запроса Webservice как есть, без проверки согласованности, наши пользовательские интерфейсы могли бы показывать запутанную информацию, потому что версия и формат данных из хранилища зависели бы от последней вызванной конечной точки.
Показывать прогресс операции
Мы можем использовать одну из следующих стратегий для отображения согласованного статуса обновления данных в пользовательском интерфейсе независимо от того, откуда поступил запрос на обновление данных:
Протестируйте каждый компонент
В разделе о разделении интересов мы упомянули, что одним из ключевых преимуществ следования этому принципу является тестируемость.
В следующем списке показано, как протестировать каждый модуль кода из нашего расширенного примера:
Лучшие практики
Программирование — это творческое поле, и создание приложений для Android не является исключением. Существует много способов решения проблемы, будь то передача данных между несколькими действиями или фрагментами, извлечение удаленных данных и их локальное сохранение в автономном режиме, или любое количество других распространенных сценариев, с которыми сталкиваются нетривиальные приложения.
Хотя следующие рекомендации не являются обязательными, наш опыт показывает, что их выполнение делает вашу кодовую базу более надежной, тестируемой и поддерживаемой в долгосрочной перспективе:
Избегайте обозначения точек входа вашего приложения — таких как действия, службы и широковещательные приемники — в качестве источников данных.
Вместо этого они должны координировать свои действия только с другими компонентами, чтобы получить подмножество данных, относящихся к этой точке входа. Каждый компонент приложения довольно недолговечен, в зависимости от взаимодействия пользователя с его устройством и общего текущего состояния системы.
Создайте четкие границы ответственности между различными модулями вашего приложения.
Например, не распространяйте код, который загружает данные из сети, по нескольким классам или пакетам в вашей кодовой базе. Точно так же не определяйте множественные несвязанные обязанности — такие как кэширование данных и связывание данных — в одном классе.
Выставляйте как можно меньше от каждого модуля.
Не поддавайтесь соблазну создать ярлык «всего один», который раскрывает детали внутренней реализации из одного модуля. Вы можете выиграть немного времени в краткосрочной перспективе, но затем вы будете нести технический долг много раз по мере развития вашей кодовой базы.
Подумайте, как сделать каждый модуль тестируемым изолированно.
Например, наличие четко определенного API для извлечения данных из сети облегчает тестирование модуля, который сохраняет эти данные в локальной базе данных. Если вместо этого вы смешиваете логику этих двух модулей в одном месте или распределяете свой сетевой код по всей базе кода, тестирование становится намного сложнее — в некоторых случаях даже не невозможным.
Сосредоточьтесь на уникальном ядре вашего приложения, чтобы оно выделялось среди других приложений.
Не изобретайте велосипед заново, снова и снова записывая один и тот же шаблон. Вместо этого сосредоточьте свое время и энергию на том, что делает ваше приложение уникальным, и позвольте компонентам архитектуры Android и другим рекомендуемым библиотекам справиться с повторяющимся образцом.
Сохраняйте как можно больше актуальных и свежих данных.
Таким образом, пользователи могут наслаждаться функциональностью вашего приложения, даже если их устройство находится в автономном режиме. Помните, что не все ваши пользователи пользуются постоянным высокоскоростным подключением.
Назначьте один источник данных единственным источником истинны.
Всякий раз, когда вашему приложению требуется доступ к этому фрагменту данных, оно всегда должно происходить из этого единственного источника истины.
Дополнение: раскрытие статуса сети
В приведенном выше разделе рекомендуемой архитектуры приложения мы пропустили сетевые ошибки и состояния загрузки для упрощения фрагментов кода.
В этом разделе показано, как отобразить состояние сети с помощью класса Resource, который инкапсулирует как данные, так и их состояние.
Следующий фрагмент кода предоставляет пример реализации Resource:
На следующей диаграмме показано дерево решений для NetworkBoundResource :
Он начинается с наблюдения за базой данных для ресурса. Когда запись загружается из базы данных в первый раз, NetworkBoundResource проверяет, является ли результат достаточно хорошим, чтобы его можно было отправить, или его нужно получить из сети заново. Обратите внимание, что обе эти ситуации могут возникать одновременно, учитывая, что вы, вероятно, хотите показывать кэшированные данные при обновлении их из сети.
Если сетевой вызов завершается успешно, он сохраняет ответ в базе данных и повторно инициализирует поток. В случае сбоя сетевого запроса NetworkBoundResource отправляет сбой напрямую.
Примечание. После сохранения новых данных на диск мы повторно инициализируем поток из базы данных. Однако обычно нам не нужно этого делать, потому что сама база данных отправляет изменения.
Имейте в виду, что полагаться на базу данных для отправки изменений включает в себя использование связанных побочных эффектов, что не очень хорошо, потому что неопределенное поведение этих побочных эффектов может произойти, если база данных не отправит изменения, потому что данные не изменились.
Кроме того, не отправляйте результаты, полученные из сети, поскольку это нарушит принцип единого источника истины. В конце концов, возможно, база данных содержит триггеры, которые изменяют значения данных во время операции сохранения. Точно так же не отправляйте `SUCCESS` без новых данных, потому что тогда клиент получит неверную версию данных.
В следующем фрагменте кода показан открытый API, предоставленный классом NetworkBoundResource для его подклассов:
Обратите внимание на следующие важные детали определения класса:
После создания NetworkBoundResource мы можем использовать его для записи наших привязанных к диску и сети реализаций User в классе UserRepository :