Selloby: как мы делали слонов под Android



    Selloby — это сервис частных объявлений в стиле Твиттера и наш самый первый проект под Android. Несмотря на сравнительно небольшой объём кода (чуть более 8 KLOC), в процессе реализации проекта Selloby мы опробовали ряд техник и методологий, которые пригодились в дальнейшей работе. Также Selloby дал нам возможность почувствовать, пускай и в миниатюре, как устроен Твиттер, что, помимо саморазвития, добавило интереса нашей работе.

    Основная идея проекта — все объявления в системе, подобно записям в Твиттере, однородны, а клиент при помощи набора фильтров получает из этого потока ту информацию, которая ему интересна.



    Фильтры


    Скорее всего, пользователю будут нужны только объявления, отобранные по определённым критериям. Например, из его города и из какой-то одной категории. Для этого реализована функция поиска по городам и категориям. Ленты объявлений на главном экране имеет три варианта отображения:

    • все объявления (если фильтры не настроены и не указан город)
    • объявления с учётом фильтров по хеш-тегам
    • объявления для конкретного города




    С хеш-тегами есть одна хитрость. Для цены указывается не значение, а максимальную и минимальную допустимые цены. Это дополнительно усложнило серверную логику, так как им пришлось делать выборку из довольно большого объёма данных не по точному совпадению, а по вхождению в интервал. Я верю, что когда-нибудь наши бек-енд разработчики напишут об этом отдельную статью.



    Текущий город может быть определён автоматически по GPS-координатам. Это одна из тех фишек, которая использовалась нами в дальнейших разработках. На сервере у нас хранится словарь населённых пунктов. С помощью простого веб-сервиса мы посылаем туда наши текущие координаты и получаем название и айдишник города. Ну или деревни :)

    Создание объявлений и хеш-теги


    Само собой, кроме просмотра объявлений пользователь должен иметь возможность их создавать. Как раз в создании объявления, а именно в использовании хеш-тегов, заключается основное сходство Selloby с Тwitter. Хеш-теги могут быть следующие:
    • ! — действие (например, "! продам", "! куплю", "! сниму" и т.п.)
    • # — объект ("#телефон", "#автомобиль", "#комната")
    • @ — метсоположение ("@Москва", "@Уфа")
    • $ или символ рубля — стоимость товара, услуги или предложения




    О последнем теге, стоимости, стоит поговорить отдельно. Он выбирается в зависимости от текущей локали. Если системный язык русский, то ставится знак рубля. Иначе ставится знак доллара. Для каждого тега есть автоподстановка из наиболее часто вводимых значений.

    К объявлению может быть добавлено до трёх фотографий. Фотографии можно загрузить как из галлереи телефона, так и получить непосредственно с камеры. Тут внезапно обнаружилось, что в андроиде есть баг, и разные телефоны по-разному работают с камерой. Одни преедают сфотографированную картинку в интенте, другие сохраняют её во временный каталог. Проблему решил несложный воркэраунд.



    Помимо хеш-тегов и местоположения, каждое объявление принадлежит одной из нескольких десятков категорий, которые объеденные по группам (товары, работа, недвижимость, услуги, туриз и т.п.).

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

    Авторизация


    Кроме собственного механизма входа в систему в Selloby реализована авторизация через Фейсбук и Твиттер.

    Сложнее всего нам далась авторизация через Твиттер, так как на тот момент там не было собственного sdk. Для этого был у нас поднят специальный веб-сервис, который выполнял авторизацию. Мобильное приложение конектилось к этому сервису, а не к серверу Твиттера. Кстати, авторизация в системе не обязательна — просматривать объявления можно и без ввода логина и пароля. Для работы с Facebook мы использовали Facebook Android SDK.

    Библиотеки


    В этом приложении для работы с базой данных мы использовали библиотеку OrmLite. Это повлекло за собой некоторые трудности в составлении сложных запросов и модификации базы, поэтому в следующих проектах мы отказались от неё. А вот другая библиотека, RoboGuice — IoC-контейнер для Android-приложений, показала с себя с наилучшей стороны, и ее использование стало обязательным для всех Android-приложений, разрабатываемых в Parcsis. Для отображения списка в духе Твиттера использовалась библиотека PullToRefresh.

    Заключение


    Первая версия Selloby для Android была опубликована 15 июля 2011 года. В настоящий момент приложение находится в стадии доработки, и в будущем его ждут значительные изменения. Следите за нашими новостями!
    Фестиваль 404
    80,25
    Компания
    Поделиться публикацией

    Комментарии 36

      +3
      Samsung GT-P7510
      This item is not compatible with your device.

      Asus Transformer Prime TF201
      This item is not compatible with your device.

      Samsung YP-G70
      This item is not compatible with your device.
        –1
        да, это так. скорее всего, дело в разрешении <uses-permission android:name="android.permission.CALL_PHONE"/> в программе есть возможность звонить по номеру телефона, указанному в объявлении. предвосхищая ваш следующий вопрос, скажу, что когда мы её разрабатывали, мы о планшетах даже и не задумывались. в следующих версия ваше замечание обязательно учтём
          +4
          <uses-feature android:name=«android.hardware.telephony» android:required=«false»/> решит вопрос.
        0
        >чуть более 8 KLOC
        Если уж очень хочется использовать мало кому известные единицы измерения, то потрудитесь хотя бы использовать единицы измерения, несущие хоть какую-то полезную нагрузку.
          +5
          1 KLOC = 1,000 строк кода

          KLOC (тысячи строк кода) является мерой того, насколько большая компьютерная программа или как долго и сколько людей потребуется, чтобы написать эту программу. Как правило измеряется исходный код.
            +2
            боюсь вас расстроить, но эта единица измерения известна всем программистам уровня чуть выше джуниора. LOC — lines of code, то есть строк кода.
              +11
              Я программирую не первый год. Услышал об аббревиатуре «KLOC» впервые.
              Впрочем это не отменяет второй и основной части моего «наезда»:)
                0
                пожалуйста
                зная количество строк кода и особенности платформы, можно оценить трудоёмкость проекта в мифических человекочасах. а зная человекочасы, можно оценить бюджет. разумеется, с некоторой погрешностью, но порядок будет правильный, я гарантирую это
                это достаточно полезная нагрузка?
                  +1
                  Тю, а почему прямым тектом не написать, что N человек трудились X времени?
                  А то вы предлагаете оценить пройденное расстояние по износу подошвы:)
                    0
                    во-первых, не уверен, что договор, подписанный мною с Парксис, позволяет мне это делать.
                    а во-вторых, количество строк я узнаю с помощью Metrics в пару кликов, а для того, чтобы узнать количество человек и времени, мне придётся поднимать кучу записей в Джире, делать какие-то выборки и тп. не стоит оно того
                  +2
                  Впервые услышав о такой аббревиатуре имеет смысл изучить ее историю и весь тот обширный спор который касается ее значимости как метрики. С 80х маститые дядьки вроде Брукса обсуждали этот вопрос годами. И в крупных компаниях пишущих более менее однородный код, она используется как коммерчески значимая. Я думаю большинство программистов которые знали что такое KLOC получили из числа 8 KLOC достаточно информации, начиная с того что проект небольшой (в среднем проекте такого масштаба новичок разберется довольно быстро), автор хочет показать что он достаточно легковесный и/или расширяемый. Из самого того факта что автор упоминает метрику, можно заключить что как он сам воспринимает проект.
                    0
                    Я думаю никто не против этой метрики. Просто достаточно указать в скобках, например, «количество строк», и все будет понятно. За всеми этими аббревиатурами, к тому же редкими, не угонишься.
                  +1
                  Меня вы тоже расстроили — пишу уже не первый и даже не третий год, но про эту аббревиатуру слышу впервые.
                    +1
                    Я программирую лет 15 уже… Конечно, я мерил код в количестве строк, ради фана, но никогда даже не думал мерить их в клоках.
                  0
                  > авторизация через Твиттер, так как на тот момент там не было собственного sdk
                  А сейчас есть? Я только неофициальные нашёл.
                    0
                    сейчас специально поискал — похоже что всё ещё нет
                      +1
                      какое ещё специально-официальное сдк? твиттер дает апи — юзайте его!
                      habrahabr.ru/post/114544/
                    +2
                    1. Как раз с твиттером очень легко интегрироваться через oauth, просто подписываете callback в манифесте:
                    data android:scheme=«x-oauthflow-twitter» android:host=«callback» и ловите ответ в свем активити. Аналогично интегрируется фэйсбук. Плюс в том, что юзеру не нужно будет вводить в вашем приложении логин пароль от фэйсбука, твиттера, так как авторизовывать будет либо сайт либо соответствующее приложения а не вы.
                    2. Насколько я вижу воркэраунд с временным файлом — он кривой. Потестируйте например на самсунг галэкси таб 10.9. Значительный пул девайсов от самсунг не сохраняет фото в файл. Вот как обрабытывал интент от самсунгов я:
                    Override public void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (resultCode==0) return;//TODO test samsung Log.d(TAG, "onactres "+resultCode+requestCode+mCurrentPhotoPath); if (requestCode==CHOOSE_PHOTO) { if(data != null){ Uri selectedImage = data.getData(); String[] filePathColumn = {MediaStore.Images.Media.DATA}; Cursor cursor = activity.getContentResolver().query(selectedImage, filePathColumn, null, null, null); cursor.moveToFirst(); int columnIndex = cursor.getColumnIndex(filePathColumn[0]); mCurrentPhotoPath = cursor.getString(columnIndex); cursor.close(); //жмакаем (если есть что) if (!Utils.compressBmp(mCurrentPhotoPath)) mCurrentPhotoPath = ""; } } if(requestCode==TAKE_PHOTO && !mCurrentPhotoPath.equals("")){ if (data!=null) { Log.d(TAG, data.toString()); } //TODO можно и отскалить конечно хотя с другой стороны фотографы будут злы а с другой стороны аут оф мемори не словить бы if (!Utils.compressBmp(mCurrentPhotoPath)) { //похуй на фотографов, на другой чаше весов канал, бабло и аутофмемори. фотографы на айфоне пусть залипают //Samsung devices have a bug with no picture result in intent //Nice блеать! Пидары, превед! final ContentResolver cr = activity.getContentResolver(); final String[] p1 = new String[] { MediaStore.Images.ImageColumns._ID, MediaStore.Images.ImageColumns.DATE_TAKEN }; Cursor c1 = cr.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, p1, null, null, p1[1] + " DESC"); if ( c1.moveToFirst() ) { String uristringpic = "content://media/external/images/media/" +c1.getInt(0); Uri newuri = Uri.parse(uristringpic); //нехуевый обходной суворовский маневр mCurrentPhotoPath =getRealPathFromURI(newuri); //жмакаем if (!Utils.compressBmp(mCurrentPhotoPath)) mCurrentPhotoPath = ""; } c1.close(); } } if(!mCurrentPhotoPath.equals("")){ String code = "var messages = API.photos.getProfileUploadServer({});" + "return {server: messages};"; Bundle params = new Bundle(); params.putString("code", code); restRequest(params,5,5,""); } mCurrentPhotoPath = ""; } private String getRealPathFromURI(Uri contentUri) { String[] proj = { MediaStore.Images.Media.DATA }; CursorLoader loader = new CursorLoader(activity, contentUri, proj, null, null, null); Cursor cursor = loader.loadInBackground(); int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA); cursor.moveToFirst(); return cursor.getString(column_index); }
                      +5
                      Не пойму что с тегом code тут
                          @Override
                      	public void onActivityResult(int requestCode, int resultCode, Intent data) {
                      		super.onActivityResult(requestCode, resultCode, data);
                      		if (resultCode==0) return;//TODO test samsung
                      		Log.d(TAG, "onactres "+resultCode+requestCode+mCurrentPhotoPath);
                      		if (requestCode==CHOOSE_PHOTO) {
                      			if(data != null){  
                      	            Uri selectedImage = data.getData();
                      	            String[] filePathColumn = {MediaStore.Images.Media.DATA};
                      
                      	            Cursor cursor = activity.getContentResolver().query(selectedImage, filePathColumn, null, null, null);
                      	            cursor.moveToFirst();
                      
                      	            int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
                      	            mCurrentPhotoPath = cursor.getString(columnIndex);
                      	            cursor.close();
                      	            //жмакаем (если есть что)
                      	            if (!Utils.compressBmp(mCurrentPhotoPath))
                      	            	mCurrentPhotoPath = "";
                      	        }
                      		}
                      		if(requestCode==TAKE_PHOTO && !mCurrentPhotoPath.equals("")){
                      			if (data!=null) {
                      				Log.d(TAG, data.toString());
                      			}
                      			//TODO можно и отскалить конечно хотя с другой стороны фотографы будут злы а с другой стороны аут оф мемори не словить бы
                      			if (!Utils.compressBmp(mCurrentPhotoPath)) {
                      				//похуй на фотографов, на другой чаше весов канал, бабло и аутофмемори. фотографы на айфоне пусть залипают
                      
                      				//Samsung devices have a bug with no picture result in intent
                      				//Nice блеать! Пидары, превед!
                      				final ContentResolver cr = activity.getContentResolver();    
                      	            final String[] p1 = new String[] {
                      	                    MediaStore.Images.ImageColumns._ID,
                      	                    MediaStore.Images.ImageColumns.DATE_TAKEN
                      	            };                   
                      	            Cursor c1 = cr.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, p1, null, null, p1[1] + " DESC");     
                      	            if ( c1.moveToFirst() ) {
                      	               String uristringpic = "content://media/external/images/media/" +c1.getInt(0);
                      	               Uri newuri = Uri.parse(uristringpic);
                      	               //нехуевый обходной суворовский маневр
                      	               mCurrentPhotoPath =getRealPathFromURI(newuri);
                      	               //жмакаем
                      	               if (!Utils.compressBmp(mCurrentPhotoPath))
                      		            	mCurrentPhotoPath = "";
                      	            }
                      	            c1.close();
                      			}
                      		}
                      		if(!mCurrentPhotoPath.equals("")){
                      			String code = "var messages = API.photos.getProfileUploadServer({});" +
                      	        		"return {server: messages};";
                      			Bundle params = new Bundle();
                      		    params.putString("code", code);
                      			restRequest(params,5,5,"");
                      		}
                      		mCurrentPhotoPath = "";
                          }
                      
                          private String getRealPathFromURI(Uri contentUri) {
                              String[] proj = { MediaStore.Images.Media.DATA };
                              CursorLoader loader = new CursorLoader(activity, contentUri, proj, null, null, null);
                              Cursor cursor = loader.loadInBackground();
                              int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
                              cursor.moveToFirst();
                              return cursor.getString(column_index);
                          }
                      
                        0
                        спасибо!
                        +2
                        Вот вы пишете
                        Также Selloby дал нам возможность почувствовать, пускай и в миниатюре, как устроен Твиттер

                        а где подробности про внутренности вашего проекта то?
                          0
                          я могу только про мобильный клиент под андроид рассказать. собственно всё, что мне показалось достойным внимания, я написал. если вас ещё что-то интересует — спрашивайте.
                          про серверную часть ничего говорить не возьмусь, но если сильно попросить, то кто-нибудь из наших бек-енд разработчиков раскажет :)
                          +1
                          Программа вроде для андроида, а интерфейс какой-то айфоно-подобный. Вот зачем так делать?

                          А идея очень нравится!
                            0
                            спасибо)
                            да, есть такая проблема, я о ней уже писал в предыдущем посте. у наших дизайнеров тогда ещё не было опыта рисования макетов под андроид, поэтому тупо копировали айфон с минимальными модификациями. позже мы осознали ошибку и примерно нашли пути решения, но, ввиду предстоящих изменений, переделывать дизайн Селлобая смысла не увидели.
                            да и гайдлайны в начале 2011 ещё не были опубликованы
                              –2
                              а по-моему — интерфейс прекрасен, тенюшечки, скругления, отступы — все по сеточке — смотрю на скрины и наслаждаюсь.
                              Я отлично представляю себе какая это колосальная работа — нарисовать классные виджеты под андроид. Респект дизайнерам и тому человеку, который все это кропотливо найнпатчил
                                –2
                                эти люди — Артём in_balance Московских и Алексей Синиченко (его, насколько я знаю, нет на хабре)
                                  +1
                                  Он красив, но непривычен андроид юзерам. Меню снизу сильно сбивает с толку. Отсутствие action bar ведет к большим проблемам у телефонов без кнопки menu. Тенюшечки спорны, в оригинальном интерфейсе holo их нет.
                                    +1
                                    Меню снизу прекрасно знакомо как пользователям android 2.x, так и пользователям 4.x (им оно известно как split action bar).

                                    Тени-градиенты-иконки да, не родные, но по расположению пользователя с толку не собъет. По крайней мере не больше, чем переход 2.х-4.х (Гугль с интерфейсами вообще непоследователен, гайдлайны меняются с каждой версий андроида, иногда даже с минорными).

                                    Мне holo очень нравится, это я так, справедливости ради.
                                      0
                                      Сорри, не меню, а переключение между табами. Оно всегда было наверху, начиная с 1.x
                                        +1
                                        и до него всегда было неудобно тянуться большим пальцем правой руки…
                                        Иногда мне кажется что некоторые вещи в андроиде сделаны не удобства для, а исключительно из духа противоречия
                                0
                                Типичная проблема многих программ, на хабре об этом недавно было.
                                +4
                                Долго пытался волос с монитора убрать…
                                  +1
                                  :-) эмитация ломографии же
                                  0
                                  Текущий город может быть определён автоматически по GPS-координатам. Это одна из тех фишек, которая использовалась нами в дальнейших разработках. На сервере у нас хранится словарь населённых пунктов. С помощью простого веб-сервиса мы посылаем туда наши текущие координаты и получаем название и айдишник города. Ну или деревни :)

                                  А почему вы не стали использовать обратный геокодинг, которые предоставлять Google Maps API?
                                    0
                                    Извиняюсь за некрасиво оформленную цитату.
                                      0
                                      нам же нужно знать не только название, но и внутренний айдишник населённого пункта

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                  Самое читаемое