Содержание статьи
В прошлой статье мы рассмотрели несколько интересных способов использования бездонных хранилищ наследия Павла Дурова. В сегодняшнем материале я предложу тебе развить эту тему и поделюсь навыками разработки клиента для нашей любимой социальной сети.
Опять теория
Для получения записей со стены используется метод wall.get.
Он вернет нам массив записей (JSON), начиная с последней (если нам нужно получать записи с начала списка, то придется использовать в запросе параметр offset
), а в поле соunt
попадет число всех записей.
Стоит отметить, что пока этот метод построен не очень гибко. Придется делать два запроса к API вместо одного, первый раз — чтобы получить количество записей (count), а второй раз — уже нужные записи, используя первое значение как смещение. Записи в массиве будут располагаться в порядке от ранних к поздним.
У каждого элемента массива записей со стены может быть до десяти приложений (документы, музыка, видео, фото). Вот пример ответа:
response:{
count:65,
items:[
{
id:92,
from_id:-50536551,
owner_id:-50536551,
date:1469733672,
marked_as_ads:0,
post_type:'post',
text:'Привет ][акер!',
can_edit:1,
created_by:3102253,
can_delete:1,
can_pin:1,
attachments:[
{
type:'photo',
photo:{
id:374032462,
album_id:-7,
owner_id:3102253,
photo_75:'https://pp.vk.me/...ad5/JN_ChKLiMZo.jpg',
photo_130:'https://pp.vk.me/...ad6/Z-84c1FuwVc.jpg',
photo_604:'https://pp.vk.me/...ad7/o79JN_hwnWs.jpg',
width:350,
height:163,
text:'Original: https://static38.cmtt.ru/comment-media/77/f0/00/0e8971de9c4360.jpg',
date:1435299352,
post_id:1003,
access_key:'1b927475e9be6cedd1'
}
},
{
type:'video',
video:{
id:171623222,
owner_id:-50536551,
title:'Напоминатель паролей',
duration:90,
description:'https://play.goog...delphi.wifipassword',
date:1452846402,
views:14,
comments:0,
photo_130:'https://pp.vk.me/...ideo/s_31105838.jpg',
photo_320:'https://pp.vk.me/...ideo/l_88896102.jpg',
photo_800:'https://pp.vk.me/...ideo/x_c377669a.jpg',
access_key:'71fedc69404803dcb7',
can_edit:1,
can_add:1
}
},
{
type:'audio',
audio:{
id:456239307,
owner_id:2000390511,
artist:'Научно-технический рэп',
title:'Тыж программист',
duration:172,
date:1469733672,
url:'https://cs1-32v4....3XvIToniOQ6tamk8E7A',
lyrics_id:196340205,
genre_id:3
}
},
{
type:'doc',
doc:{
id:422991754,
owner_id:3102253,
title:'Французские переменные.txt',
size:4017,
ext:'txt',
url:'https://vk.com/do...c46f98d38&api=1',
date:1443852692,
type:1,
access_key:'38855b19f953ffaa11'
}
}
],
post_source:{
type:'vk'
},
comments:{
count:0,
can_post:1
},
likes:{
count:0,
user_likes:0,
can_like:1,
can_publish:1
},
reposts:{
count:0,
user_reposted:0
}
}
]
}
Поле type
указывает на тип вложения, каждый из них нужно обрабатывать по-своему. Для новостного приложения обычно достаточно текста и картинок. Текст мы получим из поля text
, а описание картинки нужно брать из аттача с типом photo
, который обладает такими характеристиками, как ширина, высота, ссылки на картинку в разных размерах и собственно описание.
Для обработки видео нам нужно получить его id
и сделать дополнительный запрос к методу video.get
с параметром videos
. Значение для этого параметра должно состоять из owner_id + id (-50536551_171623222)
. Если владелец видео — группа, то owner_id
берется со знаком -
. В ответ придет описание видеообъекта.
response:{
count:1,
items:[
{
id:171623222,
owner_id:-50536551,
title:'Напоминатель паролей',
duration:90,
description:'https://play.goog...delphi.wifipassword',
date:1452846402,
views:14,
comments:0,
photo_130:'https://pp.vk.me/...ideo/s_31105838.jpg',
photo_320:'https://pp.vk.me/...ideo/l_88896102.jpg',
photo_800:'https://pp.vk.me/...ideo/x_c377669a.jpg',
files:{
external:'http://www.youtub...watch?v=vdacaryKf1A'
},
player:'https://www.youtu...ryKf1A?__ref=vk.api',
can_edit:1,
converting:0,
can_add:1
}
]
}
В поле files лягут прямые ссылки на видео в разном разрешении или ссылка на внешний источник. В нашем примере это YouTube.
Приложение с типом audio
(как и doc
) обладает еще и полем url со ссылкой на файл — MP3 или текстовый соответственно.
Получение данных
Для клиентского приложения важно получить данные из сети, сохранить их и отобразить пользователю. Способов работы с сетью предостаточно — это могут быть стандартные инструменты или сторонние библиотеки. Для работы с интернетом в SDK есть стандартный класс URLConnection. Ответ нужно разбирать вручную и как-то обрабатывать. Обычно для ответов собирают массив классов, где классы описывают объекты из API.
Сегодня для работы с сетью многие предпочитают использовать сторонние инструменты. Часто это вызвано необходимостью обеспечивать адекватную работоспособность при плохой связи (да-да, во многих местах нашей обширной родины качество мобильного интернета оставляет желать лучшего — операторы связи арендуют друг у друга оборудование, и в пиковые часы оно бывает тупо перегружено).
Сейчас в отрасли стандартом де-факто стало использование связки Retrofit + OkHttp. Хорошую статью по ее применению ты найдешь здесь. Удобна эта связка тем, что позволяет работать с сетью как синхронно, так и асинхронно, что очень важно, поскольку Андроид не дает работать с сетью в главном потоке.
Разбор ответа
Разбор ответов сервера делается автоматически, через механизм описания модели данных. Проиллюстрирую этот процесс кодом. Здесь мы в главном потоке запрашиваем список новых GIF-картинок для таблички с котами:
CatApiInterface client = ServiceGenerator.createService(CatApiInterface.class);
dataCall = client.getCats("-120909644", "10", String.valueOf(offset));
dataCall.enqueue(new Callback<Data>() {
@Override
public void onResponse(Call<Data> call, Response<Data> response) {
//android.os.Debug.waitForDebugger();
if (response.isSuccessful()) {
// request successful (status code 200, 201)
Data result = response.body();
LinkedHashSet<CatStore> newStore = new LinkedHashSet<>();
for (Data.Response cat : result.responses) {
newStore.add(new CatStore(cat.attachment.doc.url.split("\\?")[0], false,
Integer.parseInt(cat.text.substring(0, cat.text.indexOf("X"))),
Integer.parseInt(cat.text.substring(cat.text.indexOf("X") + 1)
)));
}
newStore.addAll(mCatsSet);
mCatsSet = newStore;
if (mCatsAdapter != null) {
mCatsAdapter.applyAndAnimateAdditions(new ArrayList<>(newStore));
} else {
mCatsAdapter = new CatsAdapter(mCatsSet, MainActivity.this, Glide.with(MainActivity.this));
setupRecyclerView(mRecyclerView);
}
onItemsLoadComplete();
} else {
Toast.makeText(getApplicationContext(), R.string.error_loading, Toast.LENGTH_SHORT).show();
onItemsLoadComplete();
}
}
@Override
public void onFailure(Call<Data> call, Throwable t) {
Toast.makeText(getApplicationContext(), R.string.error_loading, Toast.LENGTH_SHORT).show();
onItemsLoadComplete();
}
});
API
Работа с методами API описана в интерфейсе:
public interface CatApiInterface {
@GET("/method/wall.get")
Call<Data> getCats(@Query("owner_id") String owner_id, @Query("count") String count, @Query("offset") String offset);
@GET("method/wall.get?owner_id=-120909644&count=1")
Call<WallData> getCatsNumber();
}
Если API сервиса написан сложно, то приходится делать дополнения к парсингу ответов с помощью класса JsonDeserializer
. Иногда бывает трудно понять, что ушло на сервер, а что вернулось, в этой ситуации нам поможет HttpLoggingInterceptor
. Вот так выглядит рабочий сервис-генератор:
public class ServiceGenerator {
public static final String API_BASE_URL = "https://api.vk.com";
private static Retrofit.Builder builder = new Retrofit.Builder().baseUrl(API_BASE_URL);
public static <S> S createService(Class<S> serviceClass) {
// Крутой лог по запросам
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
// Свой парсер JSON
Type responseListType = new TypeToken<List<Data.Response>>() {
}.getType();
Gson gson = new GsonBuilder().registerTypeAdapter(responseListType, new JsonDeserializer<List<Data.Response>>() {
@Override
public List<Data.Response> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
List<Data.Response> responses = new ArrayList<>();
if (json.isJsonArray()) {
for (JsonElement jsonElement : ((JsonArray) json)) {
if (jsonElement.isJsonObject()) {
Data.Response resp = context.deserialize(jsonElement, Data.Response.class);
responses.add(resp);
}
}
return responses;
}
return context.deserialize(json, typeOfT);
}
}).create();
// Запуск
OkHttpClient client = new OkHttpClient.Builder().addInterceptor(logging).build();
builder.addConverterFactory(GsonConverterFactory.create(gson));
Retrofit retrofit = builder.client(client).build();
return retrofit.create(serviceClass);
}
}
Из описания записи со стены приложения нам нужно получить лишь текст и ссылку на документ. В тексте содержится разрешение картинки, а ссылка нужна для загрузки картинки.
public class Data {
@SerializedName("response")
@Expose
public List<Response> responses;
public class Response {
@SerializedName("text")
@Expose
public String text;
@SerializedName("attachment")
@Expose
public Attachment attachment;
public class Attachment {
@SerializedName("doc")
@Expose
public Doc doc;
}
public class Doc {
@SerializedName("url")
@Expose
public String url;
}
}
}
Загружать картинку в пользовательский интерфейс можно вручную с помощью AsyncTask. А можно немного полениться и воспользоваться библиотекой Fresco (от Facebook) или Glide (рекомендует Гугл). Они же позаботятся о кеше и сэкономят трафик.
Работа с Glide:
Glide.load(URL)
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
.into(imageView);
Тут все просто: говорим, откуда грузим картинку, куда вставляем и как работаем с кешем.
Хранение данных
Работа с видео, аудио и базами данных — темы обширные и достойные отдельных статей. И по этим направлениям андроид-кодеры имеют неплохой простор для творчества. В качестве БД можно использовать родную SQLite (кстати, создатели Telegram пересобрали ее и получили нехилый прирост производительности), кому-то нравится библиотека Realm — по некоторым тестам она в разы обгоняет SQLite.
Если структура данных не очень сложная, то можно наш массив классов сохранить как текст (gson) в SharedPreferences:
SharedPreferences sp = getPreferences(MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
Gson gson = new Gson();
String json = gson.toJson(mCatsSet);
editor.putString("cats", json);
editor.apply();
И восстанавливать его при каждом запуске:
SharedPreferences sp = getPreferences(MODE_PRIVATE);
Gson gson = new Gson();
String json = sp.getString("cats", null);
Type type = new TypeToken<Set<CatStore>>() {
}.getType();
if (json != null)
mCatsSet = gson.fromJson(json, type);
Не забудь подключить библиотеку gson в файле gradle:
compile 'com.google.code.gson:gson:2.6.2'
Вот и все
Этой статьей я заканчиваю наш мини-цикл по использованию возможностей VK.com в качестве бесплатного бэкенда для твоих приложений. Надеюсь, полученные знания помогут тебе написать настоящий шедевр и сэкономить при этом немного денежек. Удачи и до новых встреч на наших виртуальных страницах!