Социальная сеть на Android за несколько выходных — часть I (клиент)
Введение Несмотря на обилие социальных сетей, за последние несколько лет появился целый ряд новых, оригинальных и необычных социальных приложений, таких как just yo, snapchat, secret, и пр. Успех приложения just yo, ограниченного единственной функцией — отправкой сообщения фиксированного содержания, меня заинтересовал и мы с друзьями тоже решили попробовать написать очередную социальную сеть на Android. Нашей целью было очертить круг задач общий для большинства подобных приложений, предложить их решения и подготовить скелет, из которого каждый сможет сделать что-то своё и оригинальное, не тратя время на решение рутинных вопросов. С результатами работы можно сразу ознакомиться на github — android клиент и сервер на ruby on rails.Содержание Концепция и функционалИнтерфейсУскорение загрузки фотографийГраф друзейСетевые запросыАвторизацияSQLite для БД контактов, изображенийПродолжение следуетКонцепция и функционал Для начала мы определились с концепцией нового сервиса и выбрали модель — гибрид instagram и whatsapp. От инстаграма мы взяли главный сценарий использования — загрузку фотографий в ленту друзей с комментариями и лайками. А от вотсапа принцип организации графа друзей — через записную книжку по телефонным номерам друзей в автоматическом режиме.Особенности выбранной модели Выбранная модель графа подразумевает среднюю скорость роста сети, т.к. каждый новый пользователь может вовлечь в приложение максимум несколько десятков/сотен человек из записной книжки. С другой стороны, такой подходит исключает необходимость модерации и фильтрации контента на начальном этапе развития, т.к. доступность контента определяется личными контактами пользователя и предотвращает массовые спам-рассылки и прочие пагубные явления. В итоге имеем простой сервис, готовый для использования в кругу друзей и не требующий больших затрат на операционную поддержку с технической и организационной точки зрения.
Далее мы очертили функциональный минимум нашего приложения: Загрузка фотографий и их просмотр в галерее Управление списком друзей на основе записной книжки Лента фотографий, загруженных друзьями Интерфейс Очертив круг базовых функций, мы перешли к дизайну интерфейса. Каждая функция хорошо ложится на отдельный фрагмент и равновероятность их использования подсказывает использование горизонтального swipe-перехода с помощью ViewPager (Туториалы по swipe и ViewPager туториал 1 туториал 2). На первом этапе у нас получилась следующая диаграмма переходов: Рис. 1. Диаграмма переходов Рассмотрим каждый из фрагментов более детально.Контакт лист Рис. 2. Контакт лист — wireframe и скриншот Контакт лист отображает список друзей по записной книжке, зарегистрированных в сервисе, позволяет подписаться, а также открывает подробный профиль пользователя при нажатии. Он реализован обычным ListView (туториал).Галерея Рис. 3. Галерея — wireframe и скриншот Галерея отображает локальные и загруженные на сервер фотографии пользователя с кратким описанием в заголовке.Т.к. размер фотографий с учётом заголовка и соотношение сторон может различаться, мы решили использовать асимметричный GridView от Etsy AndroidStaggeredGrid. Алгоритм позиционирования отображений в таком случае требует особого подхода, в частности в AndroidStaggeredGrid вместо ImageView используется DynamicHeightImageView с заранее предопределяемым соотношением. В результате получается довольно красивая и плавно прокручиваемая плиточная галерея.Лента Лента отображает фотографии, загруженные друзьями, на которых подписан пользователь. Здесь мы также применили обычный ListView, т.к. каждая фотография может занимать большую часть экрана для удобства просмотра и масштабирование может проходить по ширине экрана. При нажатии на изображение открывается подробное описание фотографии с комментариями.Рис. 4. Лента — wireframe и скриншот Подробное описание фотографии Подробное описание фотографии содержит метаданные выбранного изображения (имя автора, описание, кол-во лайков и т.д.), а также список комментариев.Рис. 5. Описание фотографии — wireframe и скриншот Профиль пользователя Профиль пользователя содержит метаданные пользователя и галерею загруженных фотографий пользователя.Рис. 6. Профиль пользователя — wireframe и скриншот Ускорение загрузки фотографий L1/L2 кэш Подгрузка фотографий является ключевым и достаточно ресурсоёмким процессом. Фотографии загружаются как из локальной галереи на устройстве, так и с удаленного хранилища. Скорость подгрузки влияет на плавность прокрутки галереи и общее удобство интерфейса, поэтому мы решили использовать двухуровневый кэш — L1 cache в оперативной памяти и L2 cache на дисковом носителе устройства.В качестве дискового L2 кэша мы выбрали популярный плагин от Jake Wharton, он поддерживает журналирование и предоставляет удобную обёртку над стандартным DiskLruCache из Андроид SDK. L1 cache реализован стандартным андроидовским LruCache (см. com.freecoders.photobook.utils.DiskLruBitmapCache и com.freecoders.photobook.utils.MemoryLruCache).Упреждающее масштабирование В случае с лентой новостей возможен вариант, когда лента уже загружена, а фотографии продолжают загружаться с удаленного хранилища. Тогда при прокрутке возможен скачкообразный эффект при завершении подгрузки, если пользователь уже прокрутил список вниз. Чтобы его избежать мы применили упреждающее масштабирование отображений в ленте — т.е. размеры рамки для фотографии высчитываются на основе соотношения сторон и задаются еще до того, как фотография загрузилась с сервера. Таким образом позиция элементов в списке ListView не изменяется после подгрузки новых фотографий.Код 1. Пример упреждающего масштабирования в DynamicHeightImageView class FeedAdapter public View getView (int position, View convertView, ViewGroup parent) { … holder.imgView.setHeightRatio (feedEntry.image.ratio); holder.imgView.setTag (pos); mImageLoader.get (feedEntry.image.url_medium, new ImageListener (pos, holder.imgView, null)); … } Даунсэмплинг Кроме того, в Android существуют ограничения на максимальный размер и разрешение фотографии, отображаемой на экране. Это предусмотрено для предотвращения переполнения памяти. Поэтому перед загрузкой bitmap необходимо произвести downsampling.Код 2. Пример даунсэмплинга изображений class ImageUtils public static int calculateInSampleSize (BitmapFactory.Options options, int reqWidth, int reqHeight) { final int height = options.outHeight; final int width = options.outWidth; int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2; final int halfWidth = width / 2;
while (((halfHeight / inSampleSize) > reqHeight) || ((halfWidth / inSampleSize) > reqWidth)) { inSampleSize *= 2; } } return inSampleSize; }
public static Bitmap decodeSampledBitmap (String imgPath, int reqWidth, int reqHeight) { final BitmapFactory.Options options = new BitmapFactory.Options (); options.inJustDecodeBounds = true; BitmapFactory.decodeFile (imgPath, options);
options.inSampleSize = calculateInSampleSize (options, reqWidth, reqHeight);
options.inJustDecodeBounds = false; return BitmapFactory.decodeFile (imgPath, options); } Миниатюры Также если media scanner успел обработать все фотографии, то для них уже могут быть созданы миниатюры в памяти устройства. Это правило выполняется не всегда, но если миниатюра есть, то это значительно ускоряет процесс загрузки и позволяет избежать downsampling.Код 3. Пример загрузки миниатюр из MediaStore class ImagesDataSource public String getThumbURI (String strMediaStoreID) { ContentResolver cr = mContext.getContentResolver (); String strThumbUri = »; Cursor cursorThumb = cr.query (MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI, new String[]{MediaStore.Images.Thumbnails.DATA}, MediaStore.Images.Thumbnails.IMAGE_ID + »= ?», new String[]{strMediaStoreID}, null); if (cursorThumb!= null && cursorThumb.getCount () > 0) { cursorThumb.moveToFirst (); strThumbUri = cursorThumb.getString ( cursorThumb.getColumnIndex (MediaStore.Images.Thumbnails.DATA)); } cursorThumb.close (); return strThumbUri; } Граф друзей Следующим шагом, определяющим интерфейс и логику сервиса была детализация графа друзей. Мы рассматривали два подхода — принцип друзей (как вконтакте) и принцип подписчиков (как в твиттере). Принцип друзей подразумевает взаимное согласие на доступ к галерее и личному профилю, а также требует подтверждения знакомства с противоположной стороны. В случае с подписчиками не требуется запрашивать подтверждение с противоположной стороны и позволяет каждой из сторон независимо определять источники наполнения своей фотоленты.Данный выбор подразумевает направленный граф, который будет реализован в виде строк (ID подписчика, ID автора) реляционной БД на сервере.Сетевые запросы Все сетевые запросы выполняются асинхронно, а результат операции с помощью callback-интерфейса передаются в запрашивающий модуль. В 2013 году Google представила собственный плагин Volley как замену Apache HTTPClient. Её преимуществами является поддержка очереди запросов, приоритизация, стандартные обёртки для string и json запросов, keepalive, повторная отправка при неудаче, и пр. Мы решили использовать её в качестве основы для большинства сетевых запросов.Что не понравилось в Volley Забегая вперед скажу, что Volley действительно упрощает разработку сетевых интерфейсов по сравнению с HTTPClient, но на момент разработки стандартные обёртки для String и Json запросов от Volley еще были довольно сырыми, например не позволяли настроить ContentType или HttpHeaders, отсутствовала поддержка MultiPart запросов, поэтому нам пришлось их немного переписать (см. com.freecoders.photobook.network.MultiPartRequest и com.freecoders.photobook.network.StringRequest)
Код 4. Пример сетевого запроса (Запрос профиля пользователя)
class ServerInterface
public static final void getUserProfileRequest (Context context, String[] userIds,
final Response.Listener
database.insert (dbHelper.TABLE_FRIENDS, null, cv); return null; }
public ArrayList
selection = selection + » AND » + SQLiteHelper.COLUMN_TYPE + » = » + FriendEntry.INT_TYPE_PERSON;
String orderBy = SQLiteHelper.COLUMN_NAME + » ASC»;
Cursor cursor = database.query (dbHelper.TABLE_FRIENDS, null, selection, values, null, null, orderBy);
ArrayList
if (cursor == null) { return listFriends; } else if (! cursor.moveToFirst ()) { cursor.close (); return listFriends; }
do{ listFriends.add (cursorToFriendEntry (cursor)); }while (cursor.moveToNext ());
cursor.close (); return listFriends; }
private FriendEntry cursorToFriendEntry (Cursor cursor) {
FriendEntry friend = new FriendEntry (); friend.setId (cursor.getInt (idColIndex)); friend.setName (cursor.getString (nameColIndex)); friend.setUserId (cursor.getString (userIdColIndex)); friend.setAvatar (cursor.getString (avatarColIndex)); friend.setStatus (cursor.getInt (statusColIndex)); friend.setType (cursor.getInt (typeColIndex)); friend.setContactKey (cursor.getString (ContactKeyColIndex));
return friend; } … } Продолжение следует В этой части статьи мы попытались рассмотреть только основные вопросы и проблемы, с которыми мы столкнулись при разработке Android-клиента. Проект разрабатывался в стиле хакатона выходного дня и без каких-либо коммерческих целей, поэтому мы не претендуем на оригинальность подходов, не можем похвастаться целостной стилистикой кода. Если у вас есть другие советы, решения или идеи по разработке мобильных социальных приложений, то будем рады услышать их в комментариях. Также если вам понравилось или пригодилось наше пособие, то можете свободно использовать его в своих проектах, улучшать или даже присылать pull-request’ы, за что будем особенно благодарны.Во второй части мы рассмотрим более подробно серверную часть сервиса, особенности загрузки изображений на облачное хранилище AWS S3, постобработки изображений, доставки Push-уведомлений, и пр.Всем хороших выходных и до новых встреч!