RESTful API под Android: pattern B

Совсем недавно, на собеседовании в Яндексе, мне довелось обсуждать организацию Rest-взаимодействия в Android-приложениях. В ходе обсуждения всплыл вопрос – почему из трех паттернов, предложенных на Google IO 2010 Virgil Dobjanschi, первый используется существенно чаще двух других. Вопрос меня заинтересовал.

Поскольку тема обсуждения достаточно узкоспециализированная, я с позволения читателей пропущу слова о том, насколько правильная архитектура Rest-взаимодействия важна в Android-приложениях и как часто Android-разработчики сталкиваются с подобными задачами.

Краткое описание паттернов и обзор

(подробнее)
Pattern A Pattern B Pattern C
Используется Service API: Activity -> Service -> Content Provider. В данном варианте Activity работает с API Android Servcie. При необходимости послать REST-запрос Activity создает Service, Service асинхронно посылает запросы к REST-серверу и сохраняет результаты в Content Provider (sqlite). Activity получает уведомление о готовности данных и считывает результаты из Content Provider (sqlite). Используется ContentProvider API: Activity -> Content Provider -> Service. В этом случае Activity работает с API Content Provider, который выступает фасадом для сервиса. Данный подход основан на схожести Content Provider API и REST API: GET REST эквивалентен select-запросу к базе данных, POST REST эквивалентен insert, PUT REST ~ update, DELETE REST ~ delete. Результаты Activity так же загружает из sqlite. Используется Content Provider API + SyncAdapter: Activity -> Content Provider -> Sync Adapter. Вариация подхода "B", в котором вместо сервиса используется собственный Sync Adapter. Activity дает команду Content Provider, который переадресовывает ее в Sync Adapter. Sync Adapter вызывается из Sync Manager, но не сразу, а в "удобный" для системы момент. Т.е. возможны задержки в исполнении команд.
Беглый обзор подтвердил, что Pattern A используется действительно гораздо шире. Dv в своей замечательной статье «REST под Android. Часть 1: паттерны Virgil Dobjanschi» упоминает целый ряд библиотек и примеров реализации Pattern A (есть такой и на хабре), и всего один пример Pattern B – описание в книге «Programming Android, 2nd Edition» by Zigurd Mednieks, Laird Dornin, G. Blake Meike and Masumi Nakamura [1], в главе 13 «A Content Provider as a Facade for a RESTful Web Service», реализация есть на github.com. Других мне найти не удалось.
Чтение оригинала доклада Virgil Dobjanschi только добавило интриги.
Please note that in this particular pattern we broke the Content Provider contract a little bit. … Again, we’re not forcing you to adopt these particular design patterns.
В общем, не хотите – не используйте. Это воодушевляет.
Предлагаю кратко рассмотреть существующую реализацию Pattern B и попытаться понять, в чём же заключаются его особенности.

Приложение FinchVideo


Сразу отмечу, что этот код писался уважаемым G. Blake Meike в 2012 году, и с тех пор существенно не модифицировался, поэтому мы отнесёмся с пониманием к использованию всяких deprecated конструкций типа managedQuery, неиспользованию таких замечательных вещей как Loader, synchronized (HashMap) вместо ConcurrentHashMap и прочего – на архитектуру приложения они никак не влияют.

Итак, начнём с пользовательского интерфейса. В FinchVideoActivity всё вполне прозрачно – к ListView через SimpleCursorAdapter привязывается Cursor, в который и сливаются результаты запросов managedQuery к FinchVideoContentProvider.
Дальше – интереснее.

FinchVideoContentProvider
public class FinchVideoContentProvider extends RESTfulContentProvider {
    public static final String VIDEO = "video";
    public static final String DATABASE_NAME = VIDEO + ".db";
    static int DATABASE_VERSION = 2;

    public static final String VIDEOS_TABLE_NAME = "video";

    private static final String FINCH_VIDEO_FILE_CACHE = "finch_video_file_cache";

    private static final int VIDEOS = 1;
    private static final int VIDEO_ID = 2;
    private static final int THUMB_VIDEO_ID = 3;
    private static final int THUMB_ID = 4;

    private static UriMatcher sUriMatcher;

    // Statically construct a uri matcher that can detect URIs referencing
    // more than 1 video, a single video, or a single thumb nail image.
    static {
        sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        sUriMatcher.addURI(FinchVideo.AUTHORITY,
                FinchVideo.Videos.VIDEO, VIDEOS);
        // use of the hash character indicates matching of an id
        sUriMatcher.addURI(FinchVideo.AUTHORITY,
                FinchVideo.Videos.VIDEO + "/#",
                VIDEO_ID);
        sUriMatcher.addURI(FinchVideo.AUTHORITY,
                FinchVideo.Videos.THUMB + "/#",
                THUMB_VIDEO_ID);
        sUriMatcher.addURI(FinchVideo.AUTHORITY,
                FinchVideo.Videos.THUMB + "/*",
                THUMB_ID);
    }

    /** uri for querying video, expects appending keywords. */
    private static final String QUERY_URI =
            "http://gdata.youtube.com/feeds/api/videos?" +
                    "max-results=15&format=1&q=";

    private DatabaseHelper mOpenHelper;
    private SQLiteDatabase mDb;

    private static class DatabaseHelper extends SQLiteOpenHelper {
        private DatabaseHelper(Context context, String name,
                               SQLiteDatabase.CursorFactory factory)
        {
            super(context, name, factory, DATABASE_VERSION);
        }

        @Override
        public void onCreate(SQLiteDatabase sqLiteDatabase) {
            createTable(sqLiteDatabase);
        }

        private void createTable(SQLiteDatabase sqLiteDatabase) {
            String createvideoTable =
                    "CREATE TABLE " + VIDEOS_TABLE_NAME + " (" +
                            BaseColumns._ID +
                            " INTEGER PRIMARY KEY AUTOINCREMENT, " +
                            FinchVideo.Videos.TITLE + " TEXT, " +
                            FinchVideo.Videos.DESCRIPTION + " TEXT, " +
                            FinchVideo.Videos.THUMB_URI_NAME + " TEXT," +
                            FinchVideo.Videos.THUMB_WIDTH_NAME + " TEXT," +
                            FinchVideo.Videos.THUMB_HEIGHT_NAME + " TEXT," +
                            FinchVideo.Videos.TIMESTAMP + " TEXT, " +
                            FinchVideo.Videos.QUERY_TEXT_NAME + " TEXT, " +
                            FinchVideo.Videos.MEDIA_ID_NAME + " TEXT UNIQUE," +
                            FinchVideo.Videos.THUMB_CONTENT_URI_NAME +
                            " TEXT UNIQUE," +
                            FinchVideo.Videos._DATA + " TEXT UNIQUE" +
                            ");";
            sqLiteDatabase.execSQL(createvideoTable);
        }

        @Override
        public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldv,
                              int newv)
        {
            sqLiteDatabase.execSQL("DROP TABLE IF EXISTS " +
                    VIDEOS_TABLE_NAME + ";");
            createTable(sqLiteDatabase);
        }
    }

    public FinchVideoContentProvider() {
    }

    public FinchVideoContentProvider(Context context) {
    }

    @Override
    public boolean onCreate() {
        FileHandlerFactory fileHandlerFactory =
                new FileHandlerFactory(new File(getContext().getFilesDir(),
                        FINCH_VIDEO_FILE_CACHE));
        setFileHandlerFactory(fileHandlerFactory);

        mOpenHelper = new DatabaseHelper(getContext(), DATABASE_NAME, null);
        mDb = mOpenHelper.getWritableDatabase();

        return true;
    }

    @Override
    public SQLiteDatabase getDatabase() {
        return mDb;
    }

    /**
     * Content provider query method that converts its parameters into a YouTube
     * RESTful search query.
     *
     * @param uri a reference to the query for videos, the query string can
     * contain, "q='key_words'".  The keywords are sent to the google YouTube
     * API where they are used to search the YouTube video database.
     * @param projection
     * @param where not used in this provider.
     * @param whereArgs not used in this provider.
     * @param sortOrder not used in this provider.
     * @return a cursor containing the results of a YouTube search query.
     */
    @Override
    public Cursor query(Uri uri, String[] projection, String where,
                        String[] whereArgs, String sortOrder)
    {
        Cursor queryCursor;

        int match = sUriMatcher.match(uri);
        switch (match) {
            case VIDEOS:
                // the query is passed out of band of other information passed
                // to this method -- its not an argument.
                String queryText = uri.
                        getQueryParameter(FinchVideo.Videos.QUERY_PARAM_NAME);

                if (queryText == null) {
                    // A null cursor is an acceptable argument to the method,
                    // CursorAdapter.changeCursor(Cursor c), which interprets
                    // the value by canceling all adapter state so that the
                    // component for which the cursor is adapting data will
                    // display no content.
                    return null;
                }

                String select = FinchVideo.Videos.QUERY_TEXT_NAME +
                        " = '" +  queryText + "'";

                // quickly return already matching data
                queryCursor =
                        mDb.query(VIDEOS_TABLE_NAME, projection,
                                select,
                                whereArgs,
                                null,
                                null, sortOrder);

                // make the cursor observe the requested query
                queryCursor.setNotificationUri(
                        getContext().getContentResolver(), uri);

                /**
                 * Always try to update results with the latest data from the
                 * network.
                 *
                 * Spawning an asynchronous load task thread, guarantees that
                 * the load has no chance to block any content provider method,
                 * and therefore no chance to block the UI thread.
                 *
                 * While the request loads, we return the cursor with existing
                 * data to the client.
                 *
                 * If the existing cursor is empty, the UI will render no
                 * content until it receives URI notification.
                 *
                 * Content updates that arrive when the asynchronous network
                 * request completes will appear in the already returned cursor,
                 * since that cursor query will match that of
                 * newly arrived items.
                 */
                if (!"".equals(queryText)) {
                    asyncQueryRequest(queryText, QUERY_URI + encode(queryText));
                }
                break;
            case VIDEO_ID:
            case THUMB_VIDEO_ID:
                long videoID = ContentUris.parseId(uri);
                queryCursor =
                        mDb.query(VIDEOS_TABLE_NAME, projection,
                                BaseColumns._ID + " = " + videoID,
                                whereArgs, null, null, null);
                queryCursor.setNotificationUri(
                        getContext().getContentResolver(), uri);
                break;
            case THUMB_ID:
                String uriString = uri.toString();
                int lastSlash = uriString.lastIndexOf("/");
                String mediaID = uriString.substring(lastSlash + 1);

                queryCursor =
                        mDb.query(VIDEOS_TABLE_NAME, projection,
                                FinchVideo.Videos.MEDIA_ID_NAME + " = " +
                                        mediaID,
                                whereArgs, null, null, null);
                queryCursor.setNotificationUri(
                        getContext().getContentResolver(), uri);
                break;

            default:
                throw new IllegalArgumentException("unsupported uri: " +
                        QUERY_URI);
        }

        return queryCursor;
    }

    /**
     * Provides a handler that can parse YouTube gData RSS content.
     *
     * @param requestTag unique tag identifying this request.
     * @return a YouTubeHandler object.
     */
    @Override
    protected ResponseHandler newResponseHandler(String requestTag) {
        return new YouTubeHandler(this, requestTag);
    }

    /**
     * Provides read only access to files that have been downloaded and stored
     * in the provider cache. Specifically, in this provider, clients can
     * access the files of downloaded thumbnail images.
     */
    @Override
    public ParcelFileDescriptor openFile(Uri uri, String mode)
            throws FileNotFoundException
    {
        // only support read only files
        if (!"r".equals(mode.toLowerCase())) {
            throw new FileNotFoundException("Unsupported mode, " + mode + ", for uri: " + uri);
        }

        return openFileHelper(uri, mode);
    }

    @Override
    public String getType(Uri uri) {
        switch (sUriMatcher.match(uri)) {
            case VIDEOS:
                return FinchVideo.Videos.CONTENT_TYPE;

            case VIDEO_ID:
                return FinchVideo.Videos.CONTENT_VIDEO_TYPE;

            case THUMB_ID:
                return FinchVideo.Videos.CONTENT_THUMB_TYPE;

            default:
                throw new IllegalArgumentException("Unknown video type: " +
                        uri);
        }
    }

    @Override
    public Uri insert(Uri uri, ContentValues initialValues) {
        // Validate the requested uri
        if (sUriMatcher.match(uri) != VIDEOS) {
            throw new IllegalArgumentException("Unknown URI " + uri);
        }

        ContentValues values;
        if (initialValues != null) {
            values = new ContentValues(initialValues);
        } else {
            values = new ContentValues();
        }

        SQLiteDatabase db = getDatabase();
        return insert(uri, initialValues, db);
    }

    private void verifyValues(ContentValues values)
    {
        if (!values.containsKey(FinchVideo.Videos.TITLE)) {
            Resources r = Resources.getSystem();
            values.put(FinchVideo.Videos.TITLE,
                    r.getString(android.R.string.untitled));
        }

        if (!values.containsKey(FinchVideo.Videos.DESCRIPTION)) {
            Resources r = Resources.getSystem();
            values.put(FinchVideo.Videos.DESCRIPTION,
                    r.getString(android.R.string.untitled));
        }

        if (!values.containsKey(FinchVideo.Videos.THUMB_URI_NAME)) {
            throw new IllegalArgumentException("Thumb uri not specified: " +
                    values);
        }

        if (!values.containsKey(FinchVideo.Videos.THUMB_WIDTH_NAME)) {
            throw new IllegalArgumentException("Thumb width not specified: " +
                    values);
        }

        if (!values.containsKey(FinchVideo.Videos.THUMB_HEIGHT_NAME)) {
            throw new IllegalArgumentException("Thumb height not specified: " +
                    values);
        }

        // Make sure that the fields are all set
        if (!values.containsKey(FinchVideo.Videos.TIMESTAMP)) {
            Long now = System.currentTimeMillis();
            values.put(FinchVideo.Videos.TIMESTAMP, now);
        }

        if (!values.containsKey(FinchVideo.Videos.QUERY_TEXT_NAME)) {
            throw new IllegalArgumentException("Query Text not specified: " +
                    values);
        }

        if (!values.containsKey(FinchVideo.Videos.MEDIA_ID_NAME)) {
            throw new IllegalArgumentException("Media ID not specified: " +
                    values);
        }
    }

    /**
     * The delegate insert method, which also takes a database parameter. Note
     * that this method is a direct implementation of a content provider method.
     */
    @Override
    public Uri insert(Uri uri, ContentValues values, SQLiteDatabase db) {
        verifyValues(values);

        // Validate the requested uri
        int m = sUriMatcher.match(uri);
        if (m != VIDEOS) {
            throw new IllegalArgumentException("Unknown URI " + uri);
        }

        // insert the values into a new database row
        String mediaID = (String) values.get(FinchVideo.Videos.MEDIA_ID_NAME);

        Long rowID = mediaExists(db, mediaID);
        if (rowID == null) {
            long time = System.currentTimeMillis();
            values.put(FinchVideo.Videos.TIMESTAMP, time);
            long rowId = db.insert(VIDEOS_TABLE_NAME,
                    FinchVideo.Videos.VIDEO, values);
            if (rowId >= 0) {
                Uri insertUri =
                        ContentUris.withAppendedId(
                                FinchVideo.Videos.CONTENT_URI, rowId);
                getContext().getContentResolver().notifyChange(insertUri, null);
                return insertUri;
            }

            throw new IllegalStateException("could not insert " +
                    "content values: " + values);
        }

        return ContentUris.withAppendedId(FinchVideo.Videos.CONTENT_URI, rowID);
    }

    private Long mediaExists(SQLiteDatabase db, String mediaID) {
        Cursor cursor = null;
        Long rowID = null;
        try {
            cursor = db.query(VIDEOS_TABLE_NAME, null,
                    FinchVideo.Videos.MEDIA_ID_NAME + " = '" + mediaID + "'",
                    null, null, null, null);
            if (cursor.moveToFirst()) {
                rowID = cursor.getLong(FinchVideo.ID_COLUMN);
            }
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
        return rowID;
    }

    @Override
    public int delete(Uri uri, String where, String[] whereArgs) {
        int match = sUriMatcher.match(uri);
        int affected;

        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        switch (match) {
            case VIDEOS:
                affected = db.delete(VIDEOS_TABLE_NAME,
                        (!TextUtils.isEmpty(where) ?
                                " AND (" + where + ')' : ""),
                        whereArgs);
                break;
            case VIDEO_ID:
                long videoId = ContentUris.parseId(uri);
                affected = db.delete(VIDEOS_TABLE_NAME,
                        BaseColumns._ID + "=" + videoId
                                + (!TextUtils.isEmpty(where) ?
                                " AND (" + where + ')' : ""),
                        whereArgs);
                getContext().getContentResolver().notifyChange(uri, null);

                break;
            default:
                throw new IllegalArgumentException("unknown video element: " +
                        uri);
        }

        return affected;
    }

    @Override
    public int update(Uri uri, ContentValues values, String where,
                      String[] whereArgs)
    {
        getContext().getContentResolver().notifyChange(uri, null);

        SQLiteDatabase db = mOpenHelper.getWritableDatabase();
        int count;
        switch (sUriMatcher.match(uri)) {
            case VIDEOS:
                count = db.update(VIDEOS_TABLE_NAME, values, where, whereArgs);
                break;

            case VIDEO_ID:
                String videoId = uri.getPathSegments().get(1);
                count = db.update(VIDEOS_TABLE_NAME, values,
                        BaseColumns._ID + "=" + videoId
                                + (!TextUtils.isEmpty(where) ?
                                " AND (" + where + ')' : ""),
                        whereArgs);
                break;

            default:
                throw new IllegalArgumentException("Unknown URI " + uri);
        }

        getContext().getContentResolver().notifyChange(uri, null);
        return count;
    }
}
FinchVideoContentProvider кроме реализации базовых для ContentProvider (query, insert и т.д.) операций к SQLiteDatabase наследует от RESTfulContentProvider механизм запуска http-запросов в отдельных потоках
asyncQueryRequest
public void asyncQueryRequest(String queryTag, String queryUri) {
synchronized (mRequestsInProgress) {
UriRequestTask requestTask = getRequestTask(queryTag);
if (requestTask == null) {
requestTask = newQueryTask(queryTag, queryUri);
Thread t = new Thread(requestTask);
// allows other requests to run in parallel.
t.start();
}
}
}
и HashMap<String, UriRequestTask> — mRequestsInProgress (соответственно, набор выполняемых запросов). Обрабатываются результаты запросов в YouTubeHandler implements ResponseHandler, который и передаётся в задачу UriRequestTask при её создании.

Сопоставим существующие классы со схемой Pattern B.
С Activity и Content Provider всё достаточно ясно. Объект Service в явном виде в примере не используется, его функции и частично функции ServiceHelper по структурированию и запуску запросов выполняет FinchVideoContentProvider. Он же выполняет функции Processor, про Rest method написано выше. Такая вот упрощённая реализация.

Выводы


На основе анализа существующей реализации Pattern B и её описания, я сделал для себя следующие выводы
  1. Самый большой плюс Pattern B, как и описывает автор примера в разделе «Summary of Benefits» [1 — стр. 369] – увеличенная производительность запросов, поскольку они в первую очередь осуществляются к локальной БД (Content Provider);
  2. Обратная сторона этого плюса – рассогласование локальной и серверной БД и усложнённая логика получения данных.
    Неудивительно, что автор примера использовал только query (GET) запрос – это самый простой вариант. Не получили новые данные – возьмём старые из кэша. А если реализовывать insert (PUT)? Нужно будет сначала внести изменения в локальную БД, выставить им (изменениям) флаг «несинхронизировано», потом при неудачной попытке GET-запроса – повторять эту попытку, например с экспоненциально возрастающей паузой (как предлагает автор паттерна) … Всё это время пользователь будет видеть добавленные данные, которых нет на сервере. Более того, что их нет на сервере, он тоже узнать не сможет (см. пункт 3);
  3. И неприятный побочный эффект, связанный с ограниченностью взаимодействия Activity с REST (только через механизмы Content Provider) – в GUI мы не можем получить ничего, кроме данных.
    К примеру, мы никогда не узнаем о причинах отсутствия данных. Ошибка в парсинге? Сервер ничего не вернул? Вообще нет сети? Результат один – нет данных. В реализации Pattern A для этой цели мы могли передать из Activity в ServiceHelper RequestListener. C Content Provider этот номер не пройдёт.
    Конечно, мы можем получить данные, например через Broadcast Receiver, и в обход Content Provider, но к Pattern B это уже не будет иметь отношения.

Таким образом, при использовании Pattern B необходимо учитывать вышеуказанные моменты.

Может быть, кто-нибудь использовал этот паттерн в рабочих проектах или знает более удачные примеры реализации? Есть ли вообще смысл реализовать его более качественно (была такая идея), если за 4 года этим никто не озаботился? Буду рад видеть ответы в комментариях.
Поделиться публикацией

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 21

    0
    Был на Google IO 2014, спросил вопрос о том, как же делать callback из сервиса в фрагменты или активити. Т.е. идея в том, чтобы Activity -> Service. Тут в принципе понятно, тот же IntentService. А вот как обратно? Спрашивал нужно ли broadcast messaging или может сразу singleton manager к которому можно подписываться и слушать результаты. Сказали что они делают manager повсеместно внутри фреймворка, разумеется если ситуация в одном процессе. Так, что пока держусь этого курса. Если данные не особо критичны, не должны отображать 100% инфу с сервера, то пускать как описано в статье через DB кэш. Линк Activity -> Loader -> DB. Как только DB получает данные, отправить локальный броадкаст, чтобы Loader перезапустился.

    Как то так и живем.
      0
      В случае IntentService задачу решает ResultReceiver.
        0
        Так ведь в этом случае, Activity получит onDestroy и всеравно потом получит onReceiveResult. Если так, то не решает проблему. Поправьте меня если не прав, я не использую ResultReceiver.
          0
          Не очень понял тогда в чём у вас проблема?
            0
            Проблем нет. Просто думал будет полезно поделиться тем, что разработчики из андроид команды сами использует синглтоны для коммуникации между сервисами и активити.
        +3
        Внутри одного процесса для колбэков очень много кода может сэкономить EventBus. Он как «облегченная» версия бродкастов.
          0
          Одна из проблем, которую я заметил используя другой OttoBus, то, что не явно кто отсылает и кто получает сообщения. Хотя, наверное это дело предпочтения больше. Я посмотрю на EventBus, может он другой подход использует. Спасибо.
          • НЛО прилетело и опубликовало эту надпись здесь
              0
              Выкладывайте, не задумываясь)
              • НЛО прилетело и опубликовало эту надпись здесь
              0
              Действительно, очень удобная вещь.

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

              А так как это класс — то туда легко и удобно добавить полученный результат запроса. При этом активити просто просит сервис что-то сделать — и ждёт event (Event)соответствующего запросу типа. Получив этот event, вызывает у него, к примеру, getData() — и в путь.
              Ну и, более того, ещё удобнее в этот же Event добавить информацию о том, успешно ли выполнился запрос. В этом случае активити вначале спросит — эй, event.hasError()?

              Кроме того часто бывает возможным такой сценарий: однообразные запросы формируются при выборе пользователем каких-то элементов из списка — ну, например, загрузка выписки по банковской карте.
              Вполне может быть так, что пользователь кликнул по одной, ждать ему надоело, он кликнул по другой. А сразу после этого данные по первой карте загрузились и в активити прилетел соответствующий event.
              Таким образом видим, что надо как-то отслеживать контекст запроса — и игнорировать ответ, если этот ответ устаревший.
              Наиболее простой способ — добавить в event ещё одно поле — requestId.
              Активити при вызове сервиса формирует (логично — что UUID) значение requestId и вместе с другими параметрами передаёт его на/в сервис. Сервис по окончании обработки создаёт event, устанавливает ему результат, признак ошибки, бла-бла-бла — и этот самый requestId.
              А активити теперь получает event и сравнивает — requestId у этого event такой же, как только что сделанный? Если нет — значит игнорируем event этот, не нужен он больше…
              0
              Для этого достаточно объявить два компонента, описанных с помощью AIDL. Первый — тип передаваемых данных (по факту — простая линковка на .java класс, который будет передаваться, обязательно должен расширять Parcelable), второй — описание коллбэка. Далее будет сгенерирован класс, соответствующий описанному посредством AIDL коллбэку, с которым уже просто взаимодействовать. Много сложнее вопрос, как мне кажется, с реализацией аналогичного общения с оповещением из сервиса конкретных рецепиентов, в силу сложности контролирования их жц
              0
              С точки зрения Virgil Dobjanschi более удачный пример реализации паттерна B — паттерн С.
              В презентации кстати сказано: «The last pattern is simply a variant of the previous one. We’re still going to use the Content Provider API, but we’re going to use the help of a Sync Adapter.The Sync Adapter is a concept that you should learn as soon as you get home and you start developing for applications....All our applications use the concept of the Sync Adapter to refresh content. Gmail, e-mail, all these apps use that particular concept. Please use it in your apps.»
              Так что думаю реализовать паттерн B, если за 4 года этим никто не озаботился, смысла нет. Приятной наградой за труды в случае реализации паттерна С будет попадание приложения в раздел синхронизация в настройках.
              Хотя на самом деле интересно провести голосование. Думаю большинство даже паттерн A не используют в том виде в котором как он представлен на схеме, и думаю они правы :)
              0
              Сейчас работаю над проектом по паттерну B. В основном выбор на него пал как раз из-за того, что Content Provider — это фассад, сильно упрощающий логику активити и фрагментов. В GUI самое «сложное» — это отреагировать на признак «строка не синхронизирована». «Сложное» в кавычках, т.к. сложность заключается только в том, что в отдельных случаях нужно применять другие View, и все. Данные обновляются => идет оповещение слушателям => Loader'ы их перечитывают и GUI вы перерисовываете. Удобнее сложно придумать. Опять же нет проблем с выходом из активти и повторным входом в нее — у вас все результаты в кеше, вы все перечитаете и продолжите работу без потери каких-либо данных. Но самое главное — это фассад. В моем приложении данные хранятся не только в БД, а еще и в файловой системе, там довольно специфично все. И этот фасад дает шикарное разделения слоя GUI и DAO. Причем Content Provider у меня ничего не делает, кроме того, что следит за синхроноостью данных в базе, в файловой системе и вовремя вызывает сервис для синхронизации с сервером. Какая-либо логика работы с этими разношерстными данными вынесена в Content Helper'ы — промежуточный слой между Activity и Content Provider'ами. Эти хелперы отвечают за построения правильных запрсов к провайдерам и при необходимости вызывают и переиспользуют друг друга.
                0
                очень интересно, спасибо :-)
                а Вы используете в реализации паттерна сторонние библиотеки или всё сами пишете?
                и я правильно понял, что данные Вы сохраняете по инициализации PUT-запроса, а оповещение слушателям и обновление Loader'ов происходит после ответа сервера об их успешной обработке? или Вы на сервер ничего не пишете, а только читаете?
                  +1
                  Пишем сами. Мое личное мнение — андроид — не та система, где стоит создавать библиотеки над библиотеками. Родные компоненты работают достаточно неплохо, а сторонние надстройки добавляют тормозов и багов. У нас полноценный REST-протокол с GET, POST, PUT и DELETE. В случае POST, PUT, DELETE первым шагом делаются изменения в локальной БД. POST приводит к появлению новой записи с заполнением реквизитов, пришедших из Activity. Запись помечается как status = «INSERTING». PUT — меняет существующую и помечает ее как «UPDATING». Delete только помечает запись как «DELETING». Этим занимается ContentProvider. Затем ContentProvider дергает сервис, чтобы сервис в фоновом потоке вызвал API. Сам ContentProvider при этом отправляет слушателям URI этой таблички уведомление о том, что данные изменились (статусы ведь поменялись) и завершает работу, не дожидаясь ответа сервиса. При этом если в GUI реально есть слушатели этой таблички, то они ее перечитают и покажут юзеру, что с таким-то объектом сейчас идет какая-то работа. Этот элемент будет недоступен для изменений, кликов, будет отмечен серым цветом и может даже крутящееся колесико будет в версии 1.2 :) Сервис же неспеша дернет API, получит ответ, положит его в таблицу путем вызова того же Content Provider'а уже из сервиса. POST, PUT обновят уже существующую запись, DELETE окончательно ее удалит. В случае POST, PUT Content Provider также меняет статус записи на «READY». После этих изменений Content Provider снова отправляет слушателям URI этой таблички уведомление о том, что данные изменились. Элементы GUI ее перечитают и покажут юзеру, что данные опять изменились (объект готов к работе, можно убирать колесико и разрешать кликать на него). На каждом из этих этапов данные персистенты и отвязаны от жизненного цикла GUI. И дает возможность строить GUI логику максимально гибко, т.к. нигде в этом цикле мы не блокировали GUI целиком, а только отдельные элементы, связанные с непосредственно обрабатываемыми данными. Это позволяет запускать одновременно любое количество операций в фоне, если конечно логика приложения это допускает.
                  0
                  Интересно. Но я так и не использую Content Provider, т.к. зачем если он только и делает, что привносит сложность в проект. Если есть SQLite, то зачем ее крыть Content Provider'ом. Из документации:
                  Decide if you need a content provider. You need to build a content provider if you want to provide one or more of the following features:
                  You want to offer complex data or files to other applications.
                  You want to allow users to copy complex data from your app into other apps.
                  You want to provide custom search suggestions using the search framework.
                  You don't need a provider to use an SQLite database if the use is entirely within your own application.

                  Или вы делитесь информацией из вашего приложения с другими приложениями?
                    0
                    Пока не делимся. Но это ж не значит, что потом не захотим :) Если сразу будет Content Provider, писать придется меньше в итоге. Но это не основаня причина, почему я люблю Content Provider'ы в связке с CursorLoader'ами. Они упрощают жизненный цикл GUI. Вам не нужно самостоятельно заботиться о том, чтобы правильно и вовремя закрыть курсоры. При изменении конфигурации и пересоздании Activity Loader не разрушается. Новый экземпляр Activity присоединится к существующему экземпляру Loader'а и получит либо уже готовый курсор, либо дождется окончания запроса, отправленного еще предыдущим экземпляром Activity. Они естественным образом добавляют асинхронность в Ваш проект. Т.е. вы создали CursorLoader, получили курсор из ContentProvider'а, отрисовали ListView к примеру. Дальше где-то в другом фрагменте данные изменились (или вообще пришли обновления от API через сервис). Тот фрагмент/сервис независимо ни от кого обратился к ContentProvider'у, поменял данные в таблице. ContentProvider отправил оповещения Вашему CursorLoader'у, что данные изменились. Ваш Loader сам перечитал данные, обратившись к ContentProvider'у, получил новый курсор, заменил курсор у ListView — и Вы получили согласованность данных в разных фрагментах при том, что фрагменты между собой никак не взаимодействовали. В планшетном приложении таких фрагментов на экране может быть десяток, и здесь их независимость, асинхронность и при этом согласованность реально спасает. Это очень хорошо масштабируемая схема. И еще один плюс, о котором я писал в прошлом комментарии — это согласованность данных на уровне хранилища. У меня в приложении 3 источника данных — SQLite, файлы и REST API. ContentProvider служит фассадом для них всех, и GUI слой понятия не имеет, где данные реально хранятся. Нужно будет добавить/убрать/изменить хранилище — я смогу это сделать без каких-либо изменений GUI, т.к. у меня есть промежуточная абстракция данных, на которой изменения будут локализованы.
                  0
                  Здравствуйте!
                  Хоть и прошло уже достаточно много времени но до сих пор не находится ничего более подробно описывающее RESTful шаблоны на Android с поддержкой синхронизации, чем их описание от Virgil Dobjanschi на Google IO 2010. А точнее его предложения по реализации Pattern A/B/C. Или все пути так или иначе ведут к ним.
                  Но никак не могу найти работающие примеры кода, реализующие основные компоненты.
                  Pattern A — хоть и написано что это наиболее всречающийся, не нашёл.
                  Описываемый в статье Pattern B FinchVideo не работает, что-то не так с парсером YouTube, видимо с 2012 г. когда вышла книга с данным примером что-то в API YouTube изменилось.
                  Про загадочный Pattern С ещё меньше информации до сих пор.

                  Есть ли работающие примеры любого из шаблонов желательно с минимальным посторонним функционалом?
                    0
                    Могу лишь предложить Вам посмотреть не так давно появившийся доклад Yigit Boyar-а о чистой архитектуре в андроиде с современными фреймворками. Линки на запись доклада и на исходники проекта. Это конечно не совсем то, что Вы просили, но, думаю, что в плане прикладном должно Вас удовлетворить.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое