1. Ход работы

Понятие веб-сервисов. Архитектурный стиль REST

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

Веб-служба или веб-сервис (web-service) – сетевая технология, обеспечивающая межпрограммное взаимодействие на основе веб-стандартов. W3C определяет веб-службу как «программную систему, разработанную для поддержки интероперабельного межкомпьютерного (machine-to-machine) взаимодействия через сеть».

К моменту появления веб-служб уже существовали технологии, позволяющие приложениям взаимодействовать на расстоянии, где одна программа могла вызвать какой-нибудь другой метод в другой программе, которая при этом могла быть запущена на компьютере, расположенном в другом городе или даже стране. Это сокращенно называется RPC (Remote Procedure Calling – удаленный вызов процедур). В качестве примеров можно привести технологии CORBA, а для Java – RMI (Remote Method Invoking – удаленный вызов методов).

Идея веб-службы заключалась в создании такого RPC, который будет упаковываться в HTTP пакеты. Такой подход стал очень популярным, т.к. HTTP был хорошо известен, прост, понятен и обеспечивал лучшее «прохождение» через различные firewall`ы. Именно с появлением веб-сервисов развилась идея SOA – сервис-ориентированной архитектуры веб-приложений (Service Oriented Architecture).

Протоколы веб-сервисов

На сегодняшний день наибольшее распространение получили следующие протоколы реализации веб-служб:

  • SOAP (Simple Object Access Protocol) – тройка стандартов SOAP/WSDL/UDDI. Сообщения упаковываются в виде структуры, которая называется конверт (envelope), которая включает идентификатор сообщения, заголовок и тело сообщения;

  • REST (Representational State Transfer) – архитектурный стиль, который использует концепцию ресурсов и определяет операции через методы HTTP-протокола;

  • XML-RPC (XML Remote Procedure Call) – вызов удаленных процедур, использующий XML для кодирования своих сообщений и HTTP в качестве транспортного механизма.

Архитектурный стиль REST

Передача состояния представления (Representational State Transfer (REST)) является архитектурным стилем, в котором веб-службы рассматриваются, как ресурсы и могут быть идентифицированы Унифицированными идентификаторами ресурсов (Uniform Resource Identifiers (URI)).

Веб-службы, разработанные в стиле REST и с учетом ограничений REST, известны как RESTful веб-службы.

Каждая единица информации в REST называется ресурсом и имеет однозначный URI, который является ее, своего рода, первичным ключом. То есть, например, третья книга с книжной полки будет иметь URI /book/3, а 35-ая страница в этой книге – /book/3/page/35/. Отсюда и получается строго заданный формат. Причем совершенно не имеет значения, в каком формате находятся данные по адресу /book/3/page/35/ – это может быть и HTML, и отсканированная копия книги в виде jpeg-файла и документ Microsoft Word.

Над ресурсами выполняется ряд простых четко определенных операций. В качестве протокола передачи данных используется stateless-протокол, обычно HTTP.

При использовании протокола HTTP действия над данными выполняются с помощью HTTP-методов: GET (получить), PUT (добавить, заменить), POST (добавить, изменить, удалить), DELETE (удалить). Таким образом, действия CRUD (Create-Read-Update-Delete) могут выполняться как со всеми 4-мя методами, так и только с помощью GET и POST. Примеры запросов:

  • GET /book/ – получить список всех книг;

  • GET /book/3 – получить книгу номер 3;

  • PUT /book/ – добавить книгу (данные в теле запроса);

  • POST /book/3 – изменить книгу (данные в теле запроса);

  • DELETE /book/3 – удалить книгу.

Как правило, необязательно поддерживать все методы, но, как правило, веб-служба должна поддерживать:

  • GET – используется для получения существующих ресурсов;

  • POST – используется для создания/обновления нового ресурса;

  • PUT – используется для обновления/замены ресурса;

  • DELETE – используется для удаления ресурса.

  • кроме этого, служба может поддерживать такие методы как PATCH (обновление части ресурса), HEAD (возвращение заголовка ресурса, то есть метаданных) и так далее.

Библиотека Retrofit

Retrofit — это известная среди Android-разработчиков библиотека для сетевого взаимодействия, некоторые даже считают её в каком-то роде стандартом. Она является незаменимым инструментом для работы с API в клиент-серверных приложениях.

Откроем учебное приложения для создания и редактирования задач. Прежде всего необходимо добавить зависимости в build.gradle. Нам необходимо добавить библиотеку Retrofit, а также библиотеку Gson для преобразования данных в формате json в объектный вид и обратно.

build.gradle
// Retrofit, GSON
    implementation 'com.squareup.retrofit2:retrofit:2.5.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.5.0'

Для работы с удаленным сервером необходимо воспользоваться интернетом. Для работы с интернетом необходимо добавить разрешение в манифест приложения

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="ua.opu.pnit.todolist">

    <uses-permission android:name="android.permission.INTERNET"/>

    ...
    
</manifest>

Прежде чем перейти к настройке Retrofit, немного изменим сущность Task в нашем проекте. Изменим тип и название поля, которое хранит дату создания задачи (Date date --> long timestamp). Также измени сигнатуры конструктор и геттер\сеттер для поля даты.

Task.java
@Entity(tableName = "tasks")
public class Task {

    ...
    
    private long timestamp;

    ...
}

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

Перейдем в класс AppRepository и в методах работы с данными изменим тела методов, вместо вызова DAO поставим "заглушки".

AppRepository.java
public class AppRepository {

    private static AppRepository ourInstance;

    public LiveData<List<Task>> taskList;
    private Executor executor = Executors.newSingleThreadExecutor();

    public static AppRepository getInstance(Context context) {
        if (ourInstance == null) {
            ourInstance = new AppRepository(context);
        }
        return ourInstance;
    }

    private AppRepository(Context context) {
        taskList = new MutableLiveData<>();
    }

    private LiveData<List<Task>> getAllTasks() {
        // TODO: получение списка всех задач
        return null;
    }

    public void addSampleData() {
        //TODO: добавление списка тестовых задач на сервер
    }

    public void deleteAllTasks() {
        // TODO: удаление всех задач
    }

    public void deleteTask(Task task) {
        // TODO: удаление определенной задачи
    }

    public Task getTaskById(long id) {
        // TODO: получение задачи по id
        return null;
    }

    public void insertTask(Task t) {
        // TODO: добавление новой задачи
    }

Далее займемся настройкой Retrofit.

Для начала разберемся с REST API на сервере.

Method

URI

Описание конечной точки

GET

/task/all

Получить список всех задач

GET

/task?id=XXX

Получить задачу с указанным id

POST

/task

Добавить новую задачу

PUT

/task

Изменить существующую задачу

DELETE

/task?id=XXX

Удалить задачу с указанным id

DELETE

/task/all

Удалить все задачи

Создадим интерфейс TaskClient. Методы этого интерфейса соответствует определенному запросу на сервер. С помощью аннотаций и входных аргументов указываем различные параметры запроса.

Для начала реализуем запросы на получение списка всех задач и добавление новой задачи. Создадим пакет retrofit, в котором создадим интерфейс.

TaskClient.java
public interface TaskClient {

    @GET("/task/all")
    Call<List<Task>> getAllTasks();

    @POST("/task")
    Call<Void> addTask(@Body Task task);
}

Давайте разберемся в написанном коде. С помощью аннотации GET и указания пути в параметре аннотации мы указываем метод и uri ресурса. Список задач List<Task> необходимо "обернуть" в объект обобщенного типа Call. Во втором методе указываем входной аргумент - ссылка на объект задачи, которую необходимо добавить на сервер. Так как в методе POST данные передаются в теле метода, то указываем аннотацию @Body.

Теперь вернемся в класс AppRepository. Нам необходимо настроить Retrofit для отправки данных. Добавим поле типа TaskClient, в конструкторе класса создаем объект Retrofit, после чего используем его для генерации класса, реализующего интерфейс TaskClient.

AppRepository.java
public class AppRepository {

    private TaskClient client;
    
    ...

    private AppRepository(Context context) {
        taskList = new MutableLiveData<>();

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("172.168.88.135:8081")
                .addConverterFactory(GsonConverterFactory.create())
                .build();

        client = retrofit.create(TaskClient.class);
    }
}

Давайте рассмотрим подробнее создание объекта Retrofit. Создание объекта происходит с помощью паттерна Builder. В билдере мы указываем URL сервера, указываем фабрику для конвертации данных из объектного вида в json и обратно.

Объект TaskClient получен, теперь реализуем метод получения списка всех задач.

public class AppRepository {

    private TaskClient client;
    public MutableLiveData<List<Task>> taskList;

    public static AppRepository getInstance(Context context) {
        if (ourInstance == null) {
            ourInstance = new AppRepository(context);
        }
        return ourInstance;
    }

    private AppRepository(Context context) {
        taskList = new MutableLiveData<>();

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl("172.168.88.135:8081")
                .addConverterFactory(GsonConverterFactory.create())
                .build();

        client = retrofit.create(TaskClient.class);
    }

    private void getAllTasks() {

        Call<List<Task>> listCall = client.getAllTasks();
        listCall.enqueue(new Callback<List<Task>>() {
            @Override
            public void onResponse(Call<List<Task>> call, Response<List<Task>> response) {
                if (response.body() != null) {
                    taskList.postValue(response.body());
                }
            }

            @Override
            public void onFailure(Call<List<Task>> call, Throwable t) {
                Log.d("RETROFIT","Connection to server failed!");
            }
        });
    }

    public void addSampleData() {
        //TODO: добавление списка тестовых задач на сервер
    }

    public void deleteAllTasks() {
        // TODO: удаление всех задач
    }

    public void deleteTask(Task task) {
        // TODO: удаление определенной задачи
    }

    public Task getTaskById(long id) {
        // TODO: получение задачи по id
        return null;
    }

    public void insertTask(Task t) {
        // TODO: добавление новой задачи
    }
}

AppRepository.java
public class AppRepository {

    private static AppRepository ourInstance;

    public MutableLiveData<List<Task>> taskList;

    private Retrofit retrofit;
    private TaskClient client;

    private Context context;

    public static AppRepository getInstance(Context context) {
        if (ourInstance == null) {
            ourInstance = new AppRepository(context);
        }
        return ourInstance;
    }

    private AppRepository(Context context) {
        this.context = context;
        retrofit = configureBuilder().build();
        client = retrofit.create(TaskClient.class);

        taskList = new MutableLiveData<>();
    }

    private Retrofit.Builder configureBuilder() {

        Retrofit.Builder builder = new Retrofit.Builder()
                .baseUrl("http://46.250.10.29:8081")
                .addConverterFactory(GsonConverterFactory.create());

        return builder;
    }

    public void getAllTasks() {

        List<Task> list = new ArrayList<>();

        Call<List<Task>> c = client.getAllTasks();
        c.enqueue(new Callback<List<Task>>() {
            @Override
            public void onResponse(Call<List<Task>> call, Response<List<Task>> response) {

                if (response.body() != null) {
                    list.addAll(response.body());
                    taskList.postValue(response.body());
                }
            }

            @Override
            public void onFailure(Call<List<Task>> call, Throwable t) {
                Toast.makeText(context, "Failed to get all tasks!", Toast.LENGTH_LONG).show();

                Log.d("REST", call.toString());
                Log.d("REST", t.getMessage());

            }
        });

        taskList.postValue(list);
    }

    public void addSampleData() {
    }

    public void deleteAllTasks() {

        Call<Void> c = client.deleteAllTasks();
        c.enqueue(new Callback<Void>() {
            @Override
            public void onResponse(Call<Void> call, Response<Void> response) {
                Toast.makeText(context, "delete successful", Toast.LENGTH_LONG).show();
                getAllTasks();
            }

            @Override
            public void onFailure(Call<Void> call, Throwable t) {
                Toast.makeText(context, "Failed to delete all tasks!", Toast.LENGTH_LONG).show();

                Log.d("REST", call.toString());
                Log.d("REST", t.getMessage());
            }
        });
    }

    public void deleteTask(Task task) {

        Call<Void> c = client.deleteTaskById(task.getId());
        c.enqueue(new Callback<Void>() {
            @Override
            public void onResponse(Call<Void> call, Response<Void> response) {
                Toast.makeText(context, "delete successful", Toast.LENGTH_LONG).show();
                getAllTasks();
            }

            @Override
            public void onFailure(Call<Void> call, Throwable t) {
                Toast.makeText(context, "Failed to delete task!", Toast.LENGTH_LONG).show();

                Log.d("REST", call.toString());
                Log.d("REST", t.getMessage());
            }
        });
    }

    public void getTaskById(long id, MutableLiveData<Task> liveData) {

        Call<Task> c = client.getTaskById(id);
        c.enqueue(new Callback<Task>() {
            @Override
            public void onResponse(Call<Task> call, Response<Task> response) {

                Log.d("REST", call.toString());
                Log.d("REST", response.toString());

                liveData.postValue(response.body());
            }

            @Override
            public void onFailure(Call<Task> call, Throwable t) {
                Toast.makeText(context, "Failed to get task!", Toast.LENGTH_LONG).show();

                Log.d("REST", call.toString());
                Log.d("REST", t.getMessage());
            }
        });
    }

    public void insertTask(Task t) {

        Call<Void> c = client.addTask(t);

        c.enqueue(new Callback<Void>() {
            @Override
            public void onResponse(Call<Void> call, Response<Void> response) {
                AppRepository.this.getAllTasks();
            }

            @Override
            public void onFailure(Call<Void> call, Throwable t) {
                Toast.makeText(context, "Failed to insert task", Toast.LENGTH_LONG).show();

                Log.d("REST", call.toString());
                Log.d("REST", t.getMessage());
            }
        });
    }

    public void updateTask(Task t) {
        Call<Void> c = client.updateTask(t);
        c.enqueue(new Callback<Void>() {
            @Override
            public void onResponse(Call<Void> call, Response<Void> response) {
                AppRepository.this.getAllTasks();
            }

            @Override
            public void onFailure(Call<Void> call, Throwable t) {

            }
        });
    }
}

MainViewModel.java
public class MainViewModel extends AndroidViewModel {

    public LiveData<List<Task>> taskList;
    private AppRepository repository;

    public MainViewModel(@NonNull Application application) {
        super(application);

        repository = AppRepository.getInstance(application.getApplicationContext());
        taskList = repository.taskList;
        repository.getAllTasks();
    }

    public void addSampleData() {
        repository.addSampleData();
    }

    public void deleteAllTasks() {
        repository.deleteAllTasks();
    }

    public void deleteTask(Task task) {
        repository.deleteTask(task);
    }
}
MainActivity.java
public class MainActivity extends AppCompatActivity {

    private RecyclerView rv;
    private TaskAdapter adapter;
    private List<Task> mTaskList = new ArrayList<>();

    private MainViewModel mViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        rv = findViewById(R.id.recycler);
        rv.setHasFixedSize(true);
        LinearLayoutManager manager = new LinearLayoutManager(this, RecyclerView.VERTICAL, false);
        rv.setLayoutManager(manager);

        initViewModel();

        FloatingActionButton fab = findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent = new Intent(MainActivity.this, TaskActivity.class);
                startActivity(intent);
            }
        });
    }

    private void initViewModel() {

        final Observer<List<Task>> tasksObserver = new Observer<List<Task>>() {
            @Override
            public void onChanged(List<Task> tasks) {
                mTaskList.clear();
                mTaskList.addAll(tasks);

                if (adapter == null) {
                    adapter = new TaskAdapter(mTaskList, MainActivity.this);
                    rv.setAdapter(adapter);
                } else {
                    adapter.notifyDataSetChanged();
                }
            }
        };

        mViewModel = ViewModelProviders.of(this).get(MainViewModel.class);
        mViewModel.taskList.observe(this, tasksObserver);
    }

    // region Меню

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        int id = item.getItemId();
        if (id == R.id.action_delete_all) {
            deleteAllTasks();
            return true;
        } else if (id == R.id.action_sample_data) {
            addSampleData();
            return true;
        }
        return super.onOptionsItemSelected(item);
    }

    private void deleteAllTasks() {
        mViewModel.deleteAllTasks();
    }

    private void addSampleData() {
        mViewModel.addSampleData();
    }

    public void deleteTask(Task task) {
        mViewModel.deleteTask(task);
    }

    // endregion
}

Last updated