Напишем приложение сложение двух чисел, где сложение двух чисел происходит на стороне сервера.
Эта статья из цикла статей Клиент-серверные приложения.
Статья из цикла «Сложение двух чисел». Для меня минимальное освоение любой системы программирования начинается с возможности создания такой программы. Если можно написать приложение, в которой пользователь может ввести два числа, считать их, провести с ними какие-то действия, а потом вывести результат, то, значит, базовое владение имеется. И много задач именно из области программирования, алгоритмики можно будет решать, зная, как в конкретной системе программирования запрограммировать такую программу.
В статье приведен вариант, сервер выдает одно число и всё. Чаще всего же сервера в виде текстовых файлов отдают либо HTML, либо XML, либо JSON, то есть структурированную информацию. В таких случаях часто используют библиотеку Retrofit.
Содержание
- Постановка задачи
- Серверная часть
- Создание Android проекта
- XML разметка
- Добавляем разрешения
- Про аннотации
- Подключение зависимостей
- Шаблон запроса
- Java код
Постановка задачи
На сервер от Android приложения поступает HTTP запрос с двумя переменными a и b. Переменные a и b передаются через POST параметры.
Сервер возвращает JSON файл, который содержит как слагаемые, так и сумму чисел, которая отображается в Android приложении.
Если мы передадим через POST параметры a=2, b=3, то сервер выдаст ответ.
1 |
{"a":2,"b":3,"c":5} |
Серверная часть
У вас должен быть сервер, доступный из интернета, к которому можно обращаться.
В статье приведен пример серверной части на PHP.
В статье буду использовать PHP скрипт, который я расположил по адресу http://demo.harrix.org/demo0013/ (если перейти по ссылке без параметров, то должно выдаваться error).
Создание Android проекта
Надеюсь, что вы сможете сами создать болванку приложения, так что закинул под сплойер.
XML разметка
Пусть разметка файла activity_main.xml будет такой.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingLeft="16dp" android:paddingRight="16dp" android:orientation="vertical" > <EditText android:id="@+id/editText" android:layout_width="match_parent" android:layout_height="wrap_content" android:ems="10" android:inputType="textPersonName" /> <EditText android:id="@+id/editText2" android:layout_width="match_parent" android:layout_height="wrap_content" android:ems="10" android:inputType="textPersonName" /> <Button android:id="@+id/button" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Button" /> <TextView android:id="@+id/textView" android:layout_width="match_parent" android:layout_height="wrap_content" /> </LinearLayout> |
Есть два поля ввода чисел, кнопка и поле для вывода суммы чисел. Все элементы имеют свои идентификаторы android:id.
Добавляем разрешения
Приложение у нас будет обращаться в интернет. Поэтому нужно ей дать на это разрешение в AndroidMainfest.xml.
1 |
<uses-permission android:name="android.permission.INTERNET"/> |
Про аннотации
Мы закончили со стандартной частью и теперь переходим к программированию с использованием библиотеки Retrofit. Она основана на работе с аннотациями. Многие, кто изучает Java, с ними напрямую не сталкивались и не понимают, что это такое. Так что настоятельно рекомендую изучить эту тему, прежде чем идти дальше. Вот пример одной из аннотаций, чтобы понимали, как они выглядят. Видите, знаки @. Вот ими и обозначаются аннотации.
1 2 3 4 5 |
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface NeedMethod { int type() default 1; } |
А тут показан пример использования аннотаций для решения одной из задач, по которой, надеюсь, аннотации станут понятнее.
Как вызвать метод класса из другого класса при недостатке информации через аннотации в Java
Подключение зависимостей
Надеюсь, что вы только что поломали себе мозг над аннотациями, так что переходим к Retrofit.
Нам нужна будет сама библиотека Retrofit.
От сервера мы получим JSON файл. Библиотекой gson будет его парсить.
И нужен конвектор данных от Retrofit в gson Gson Converter.
Подключим эти библиотеки. Переходим в файл build.gradle (Module.app).
В разделе dependencies нужно подключить вышеупомянутые три библиотеки…
Хотя нет. А давайте в этот раз пропишем зависимости в «автоматизированном» виде.
Вводим retrofit и жмем кнопку поиска.
Вводим retrofit:converter-gson.
Вводим gson.
Теперь в build.gradle появились нужные строчки. Можно было их прописать вручную.
1 2 3 |
compile 'com.squareup.retrofit2:retrofit:2.2.0' compile 'com.squareup.retrofit2:converter-gson:2.2.0' compile 'com.google.code.gson:gson:2.8.0' |
Возможно, что, когда вы будете читать эту статью, версии библиотек обновятся, так что способом, что показано выше, лучше воспользоваться.
Шаблон запроса
Создадим шаблон запроса к серверу. Он будет основан на интерфейсе с использованием аннотаций (тех самых, которыми я выше пугал) из библиотеки Retrofit.
Допустим наш интерфейс будет называться Request.
Шаблон для наполнения я скопировал со страницы из раздела FORM ENCODED AND MULTIPART.
Пока получилось так.
1 2 3 4 5 6 7 |
package com.example.clientadding2numbers; public interface Request { @FormUrlEncoded @POST("user/edit") Call<User> updateUser(@Field("first_name") String first, @Field("last_name") String last); } |
Нужно поменять строчку с Call. Здесь мы прописываем шаблон метода, который будем отправлять данные на сервер и получать ответ от него.
Как и в предыдущих статьях (эта и эта) набор отправляемых параметров будем передавать в виде HashMap.
Также по аналогии с прошлыми статьями метод, который отправляет запрос и получает ответ, назовем performPostCall.
А вот с возвращаемым объектом не будем заморачиваться: пусть будет возвращаться обычный Object.
Получилась строчка у меня такая.
1 |
Call<Object> performPostCall(@FieldMap HashMap<String, String> postDataParams); |
И поменяем путь к скрипту на сервере, который считает сумму наших чисел. Это прописано в аннотации @POST.
Выше я говорил, что PHP скрипт я расположил по адресу http://demo.harrix.org/demo0013/. Получается, что сервер находится по адресу http://demo.harrix.org, а скрипт на этом сервере находится по строчке /demo0013/. Вот этот путь и записываем в @POST. Адрес сервера не указываем!
В итоге я получил вот такой интерфейс.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package com.example.clientadding2numbers; import java.util.HashMap; import retrofit2.Call; import retrofit2.http.FieldMap; import retrofit2.http.FormUrlEncoded; import retrofit2.http.POST; public interface Request { @FormUrlEncoded @POST("/demo0013/") Call<Object> performPostCall(@FieldMap HashMap<String, String> postDataParams); } |
Java код
Был такой код файла MainActivity.java.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
package com.example.clientadding2numbers; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } } |
Вначале объявим переменные кнопки, полей ввода и поля для вывода результата и соединим их с элементами из XML файла. Также назначим слушателя для клика кнопки.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
package com.example.clientadding2numbers; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; public class MainActivity extends AppCompatActivity { private TextView textView; private EditText editText; private EditText editText2; private Button button; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = (TextView)findViewById(R.id.textView); editText = (EditText)findViewById(R.id.editText); editText2 = (EditText)findViewById(R.id.editText2); button = (Button)findViewById(R.id.button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { } }); } } |
В классе активности объявим переменные.
1 |
private String a, b, answerHTTP; |
А также определим переменную server, которая будет указывать на сервер, к которому мы будем осуществлять запрос. Если вы используйте свой сервер, то поменяйте значение. Обратите внимание, что слэша в конце адреса сервера нет!
1 |
private final String server = "http://demo.harrix.org"; |
Создадим экземпляр парсера Gson файла JSON.
1 |
private Gson gson = new GsonBuilder().create(); |
И, наконец, создадим экземпляр нашего главного класса Retrofit, где укажем экземпляр gson и адрес сервер server.
1 2 3 4 |
private Retrofit retrofit = new Retrofit.Builder() .addConverterFactory(GsonConverterFactory.create(gson)) .baseUrl(server) .build(); |
Привяжем шаблон запроса к объекту retrofit.
1 |
private Request req = retrofit.create(Request.class); |
Подготовительная работа сделана. Теперь переходим к программированию клика кнопки.
В клике кнопки считаем значения переменных a и b из полей ввода.
1 2 |
a = editText.getText().toString(); b = editText2.getText().toString(); |
Там же создадим HashMap для наших параметров и закинем туда наши считанные числа.
1 2 3 |
HashMap<String, String> postDataParams = new HashMap<String, String>(); postDataParams.put("a", a); postDataParams.put("b", b); |
Создаем объект запроса (пока не запуская отправки запроса) по нашему шаблону.
1 |
Call<Object> call = req.performPostCall(postDataParams); |
В Retrofit есть два метода для запуска запроса: execute() и enqueue().
Можно пользоваться любым, но первый выполняется в том потоке, в котором его вызвали. И если его вызвать в главном потоке, то вызовется ошибка NetworkOnMainThreadException, так как обращения к сети нельзя делать в главном потоке. С чем лично я намучился, не понимая, что делаю не так.
А вот метод enqueue() сам создает еще один поток, в котором вызывает запрос. Его и используем.
1 2 3 4 5 6 7 8 9 |
call.enqueue(new Callback<Object>() { @Override public void onResponse(Call<Object> call, Response<Object> response) { } @Override public void onFailure(Call<Object> call, Throwable t) { } }); |
onResponse() вызывается, если запрос прошел успешно.
onFailure() вызывается, если что-то пошло не так.
Обратите внимание, что в библиотеке есть готовый класс-дженерик Response<>, куда записывается ответ.
Теперь ответ, что содержится в объекте response из метода onResponse(), можем анализировать, как хотим.
В конкретно нашем случае, сервер генерирует простой линейный JSON файл c тремя параметрами a, b, c.
Поэтому нам будет удобно перевести ответ сервера в HashMap<String, Double> с тремя элементами.
1 |
HashMap<String, Double> map = gson.fromJson(response.body().toString(),HashMap.class); |
Из трех значений в данном отображении (карте) нужно только со значением ключа c. Вытаскиваем и выводим.
1 2 |
answerHTTP = Double.toString(map.get("c")); textView.setText(answerHTTP); |
А если пошло что-то не так, то выводим сообщение об ошибке.
1 2 3 4 |
@Override public void onFailure(Call<Object> call, Throwable t) { textView.setText("Request error"); } |
Полный код.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
package com.example.clientadding2numbers; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.TextView; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import java.util.HashMap; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; public class MainActivity extends AppCompatActivity { private TextView textView; private EditText editText; private EditText editText2; private Button button; private String a, b, answerHTTP; private final String server = "http://demo.harrix.org"; private Gson gson = new GsonBuilder().create(); private Retrofit retrofit = new Retrofit.Builder() .addConverterFactory(GsonConverterFactory.create(gson)) .baseUrl(server) .build(); private Request req = retrofit.create(Request.class); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); textView = (TextView) findViewById(R.id.textView); editText = (EditText) findViewById(R.id.editText); editText2 = (EditText) findViewById(R.id.editText2); button = (Button) findViewById(R.id.button); button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { a = editText.getText().toString(); b = editText2.getText().toString(); HashMap<String, String> postDataParams = new HashMap<String, String>(); postDataParams.put("a", a); postDataParams.put("b", b); Call<Object> call = req.performPostCall(postDataParams); call.enqueue(new Callback<Object>() { @Override public void onResponse(Call<Object> call, Response<Object> response) { HashMap<String, Double> map = gson.fromJson(response.body().toString(), HashMap.class); answerHTTP = Double.toString(map.get("c")); textView.setText(answerHTTP); } @Override public void onFailure(Call<Object> call, Throwable t) { textView.setText("Request error"); } }); } }); } } |
Всё. Можете запускать приложение и проверять работу.