Столкнулся на проекте с проблемой доселе не виданной. Пришлось покурить документацию и в этой статье я расскажу как с помощью RxJava и Retrofit 2 — можно решить задачу по созданию клиента Odata для android приложения.

Спасибо огромное Jake Wharton за создание таких комфортных инструментов.

Добро пожаловать в мир магии


У нас есть приложение, которое по протоколу Odata должно выгребать с сервера данные, отображать их в списках, которые должны подгружаться по мере прокрутки и отправлять данные созданные пользователем на сервер. Тривиальная задача, но не тут то было, то что работает без проблемно на Java — не хочет так же работать с android.

А библиотеки и документация на Odata только от Apache — Olingo и Microsoft на C#.

В данной статье протокол Odata рассматривать я не буду, очень хорошая документация есть у Microsoft и ссылки я оставлю в конце статьи.

Вот вкратце определение с Wiki Open_Data_Protocol

Open Data Protocol (OData)
Open Data Protocol (OData) — это открытый веб-протокол для запроса и обновления данных. Протокол позволяет выполнять операции с ресурсами, используя в качестве запросов HTTP-команды, и получать ответы в форматах XML или JSON.

Начиная с версии 4.0, OData — открытый стандарт, одобренный OASIS.

И вот здесь и начинается самое интересное, Odata — это своеобразный SQL в REST API и для динамически создаваемых данных самое то.

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

Решение которой не может быть типовым и многократно описанным в сети.

Скажу даже больше, кроме этих ссылок на документацию в сети — по теме все плохо.

А теперь займемся магией:

Создаем службу для работы с сетью

Будем использовать Retrofit 2 и сопутствующие продукты компании Square

добавим зависимости в в файл build.gradle
// Network
    implementation 'com.squareup.retrofit2:retrofit:2.7.1' // Собственно сам Retrofit 2
    implementation 'com.squareup.retrofit2:converter-gson:2.7.1' // Конвертер для работы с JSON 
    implementation 'com.squareup.okhttp3:logging-interceptor:4.3.1' // Перехватчик запросов
    implementation 'com.squareup.retrofit2:adapter-rxjava2:2.7.1' // Адаптер для работы с RxJava
    implementation 'com.squareup.okhttp3:okhttp:4.3.1' // OkHttp - это HTTP-клиент
listing 1

Все эти зависимости и есть мощнейшая библиотека для работы с с сетью в Java.

Описывать работу с Retrofit 2 не вижу смысла, вот есть хороший мануал: Используем Retrofit 2 в Android-приложении.

Создаем класс NetworkService:
public class NetworkService {

    private static final String TAG = "NetworkService";

    private static final NetworkService
            mInstance = new NetworkService();

    private String mToken;
    private Retrofit mRetrofit;

    public static NetworkService getInstance() {
        return mInstance;
    }

    private NetworkService() {

        RxJava2CallAdapterFactory rxAdapter =
                RxJava2CallAdapterFactory
                        .createWithScheduler(Schedulers.io());

        HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
        interceptor.setLevel(HttpLoggingInterceptor.Level.BODY); 

        OkHttpClient.Builder okHttpClient =
                new OkHttpClient.Builder()
                        .addInterceptor(interceptor)
                        .addInterceptor(chain -> {
                            Request newRequest =
                                    chain.request().newBuilder()
                                           .addHeader("Accept",
                                                     "application/json,text/plain,*/*")
                                           .addHeader("Content-Type",
                                                    "application/json;odata.metadata=minimal")
                                           .addHeader("Authorization", mToken)
                                    .build();

                            return chain.proceed(newRequest);
                        });

        Gson gson = new GsonBuilder()
                .setDateFormat("yyyy-MM-dd'T'HH:mm:ssZ")
                .create();

        mRetrofit = new Retrofit.Builder()
                .baseUrl(Const.BASE_URL)
                .addConverterFactory(GsonConverterFactory.create(gson))
                .addCallAdapterFactory(rxAdapter)
                .client(okHttpClient.build())
                .build();
    }
    public ApiService getNetworkClient(){
        return mRetrofit.create(ApiService.class);
    }
}

listing 2

Создаем интерфейс API:
public interface ApiService {
// Делаем запрос без условий
@GET("odata/product")
Observable<List<ProductsDto>> getProducts();
}
listing 3

И создаем какой нибудь контроллер что бы дергать запросы:
public class ProductsController {

    private ApiService mApiService;

    private List<ProductsDto> listProducts;
    private Gson gson;

    public ProductsController() {

        mApiService = App.getNetworkService().getNetworkClient();

        listProducts = new ArrayList<>();

        gson = new Gson();
    }

    public void productsBtnClick() {

        mApiService.getProducts()
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new DisposableObserver<List<ProductsDto>>() {
                    @Override
                    public void onNext(List<ProductsDto> products) {

                        listProducts.addAll(listProducts);
                    }

                    @Override
                    public void onError(Throwable e) {

                    }

                    @Override
                    public void onComplete() {
                    }
                });
    }
listing 4

Итак, на сервере храниться модель данных Poduct, которая имеет какие то параметры и атрибуты. Все данные идут в формате JSON и для работы нам необходим POJO класс.

Рекомендую в HttpLoggingInterceptor настроить уровень подробности перехвата — Level.BODY.

Делаем запрос listing 4, чтобы максимально вытянуть структуру данных и ответ будет приблизительно в таком формате:

// Есть некий список в котором перечислены сущности высшего уровня
// в формате JSON
{
  "@odata.context":"https://example.xxx/api/odata/$metadata","value":[
    {
      "name":"product","kind":"EntitySet","url":"product" // продукт 
    },{
      "name":"blogs","kind":"EntitySet","url":"blogs"  // блоги
    },{
      "name":"posts","kind":"EntitySet","url":"posts"  // посты
    },{
      "name":"comments","kind":"EntitySet","url":"comments" // комментарии
    },{
      "name":"rates","kind":"EntitySet","url":"rates" // рейтинги
    }
  ]
}

listing 5

И уже на основании этих данных можно формировать запрос для дальнейших манипуляций с данными.

То есть это полноценный объект с каким то поведением и атрибутами, чтобы получить эту информацию при создании запроса необходимо добавить условия, на основании которых мы и получим наш DataSource не выдумывая новый велосипед и не прикручивая к нему костыли.

Кульминация и щенячий восторг от комфорта инструмента


И вот он момент истинны, мощь, сила и простота библиотеки Retrofit 2. Теперь можно получить properties используя сервисный документ Odata:

// Можно получить properties сущности product
@GET("odata/product?$filter=Id eq 111&$expand=dateReading($orderby=Date desc")
Observable<List<ProductsDto>> getProducts();

// Или properties сущности blogs
@GET("odata/blogs?$orderby=Date desc")
Observable<List<BlogsDto>> getBlogs();

// И запрос уже можно формулировать исходя из потребностей
@GET("odata/product?$filter=((Id eq 19) and (Name eq 'Available')) and ((Status eq 'OPEN') or ((Status eq 'CLOSED') and (Date ge 2020-02-13T06:39:48Z)))&$orderby=Status asc,Date desc&$top=10&$expand=AuthorId,CategoryId($expand=weight)&$count=true")
Observable<List<ProductsDto>> getProducts();
// Этот запрос выдаст точную информацию отобранную по условиям,
// но он явно выходит за разумные рамки. 

// Исправляем:
@GET
Observable<List<ProductsDto>> getProducts(@Url String url);

listing 6

Вот она силушка богатырская библиотеки Retrofit 2. Динамический Url забирает на себя всю эту массу параметров, с которыми можно играться в коде на сколько хватит фантазии.

Выглядеть это будет как то так:
private void buttonGetProduct() {
// Здесь со строками можно все
        String one = "odata/product?$filter=Id eq ";
        String id = "777";
        String tree = "&$expand=dateReading($orderby=Date desc)";
        String url = one + id + tree;

        mApiService.getProduct(url)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new DisposableObserver<List<ProductDto>>() {
                    @Override
                    public void onNext(List<ProductDto> products) {

// А вот здесь мы и получаем искомое, правда данные в формате JSON,
// но есть масса конвертеров и это не проблема 
                        mProductsDto.addAll(countersDtos);
                    }

                    @Override
                    public void onError(Throwable e) {
                    }

                    @Override
                    public void onComplete() {
                    }
                });
    }
listing 7

Итоги


Это был полезный опыт, которым я и спешу поделится, мне в свое время эта статья реально бы убрала кучу проблем и вопросов.

В статье я не стал углубляться в ненужные подробности по Retrofit 2 и OData и указал ссылки на документацию если возникнет необходимость вникнуть глубже.

Полную версию кода предоставить не могу, за что приношу извинения, продукт коммерческий.

И, как обещал, ссылки:

Документация Microsoft: Open Data Protocol
Documentation OData 4.0 Java Library
Создание полнофункциональных интернет-приложений с применением Open Data Protocol