В данном репозитории расположены различные материалы для пошагового создания с нуля простого мобильного приложения City Map для платформы Android, на языке программирования Java.
City Map - мобильное приложения для просмотра базовой информации о различных городах.
Основные функции:
- Постраничная навигация;
- Загрузка данных из сети;
- Оффлайн доступ к данным;
- Отображение городов на мировой карте.
Если вас когда-либо интересовала нативная разработка для мобильных устройств на платформе Android и Вы хотели бы попробовать себя в этом, то вы можете попробовать начать изучение используя материалы из данного репозитория.
Для того, чтобы начать изучать нативную мобильную разработку под Android, Вам понадобится:
- Среда разработки Android Studio (или Eclipse) и SDK;
- Базовые знания английского языка (для чтения материалов);
- Базовые навыки работы с Git;
- Базовые знания языка программирования Java.
Мы предлагаем Вам познакомиться с нативной разработкой под платформу Android, путем последовательного выполнения заданий по созданию приложения CityMap.
Основные понятия:
Android Studio, SDK, AndroidManifest, Gradle Plugin, base structure of android project.
Задание: В рамках данного задания требуется создать новый проект с одним экраном и xml-файлом разметкой для него.
Результат, который должен получиться: GitHub
Основные понятия:
Application, Activity, Fragment, Intent, Resources, XML, View and ViewGroup, base layouts (LinearLayout, FrameLayout, RelativeLayout), base views (TextView, ImageView), RecyclerView and Adapter for it, base Listeners for view.
Задание:
Данное задание требует создать список элементов (не более 10) с открытием экрана с детальным описанием после нажатия на конкретным элемент списка. Каждый элемент списка это объект класса с некоторым набором атрибутов и методов. Объекты класса как и сам список создаются программно в рамках приложения. Базовый объект класса должен содержать такие поля как id, name и description.
Дополнительно:
ConstraintLayout и FlexBox, ViewHolder pattern, ItemDecorator for RecyclerView, CustomView
Результат, который должен получиться: GitHub
Основные понятия: Threading, ThreadPool, AsyncTasks, Loaders, REST Api, Http Request, Json, Bitmap, Drawable.
Задание:
В рамках данного задания требуется провести модификацию приложения полученного в Часть 1 - заменить заданные вручную данные на полученные из сети в формате JSON. Реализовать загрузку и отображение картинок как для каждого элемента списка, так и в рамках экрана с детальным описанием. Приложение должно проверять наличие интернет соединения и сообщать о его отсутствии при попытке сделать запрос в сеть для загрузки данных. Для отображения картинок можно использовать одну из данных библиотек - Picasso/Glide.
JSON - https://api.myjson.com/bins/7ybe5
Дополнительно: OkHttp, Retrofit, Gson, Jackson, RxJava/RxAndroid, VectorDrawable
Результат, который должен получиться: GitHub
Основные понятия: SharedPreferences, Files, SQLite, ORM databases, NoSQL databases.
Задание: В рамках данного задания требуется организовать работу с базой данных. Полученные данные в рамках Часть 2 должны сохраняться в БД, а после, в ситуации с отсутствие интернет-соединения/ошибкой при загрузке данных из сети, доставаться из нее и отображаться пользователю.
Дополнительно: OrmLite, GreenDAO, Realm, ObjectBox.
Результат, который должен получиться: GitHub
Основные понятия: Base of material design, Support AppCompat libs, Google Play Services и Firebase services
Задание: Преобразование приложения в рамках Material design, добавление в приложения google map и отображение на ней по полученным из json координатам изображений.
Результат, который должен получиться: GitHub
Tip - небольшие советы по разработке.
WKM - информация для углубленного изучения.
- Создадим новый проект со стартовым экраном
EmptyActivity.
WKM: Основы создания приложений
-
Для создания списка, будем использовать
RecyclerView. ЭтаViewне входит в стандартный пакет, так что добавим необходимую зависимость вbuild.gradleфайл. -
Добавим
RecyclerViewв макет окна - файл-ресурсactivity_main.xml.
WKM: Подробнее о ресурсах приложения
Tip: Id будем задавать в формате <what_where_name>
- Создадим файл с внешним видом каждого элемента в
RecyclerView-adapter_list_item.xml.
Tip: Для названия элементов списка в RecyclerView используется шаблон adapter_<name>_item.
-
Хорошим тоном считается выносить отступы (а так же цвета, и др. ресурсы) в отдельный файл. Поэтому вынесем стандартные отступы 16 (отступ для экрана), и 8 (для контента) в
dimen.xml. -
Добавим необходимые виджеты в файл adapter_list_item.xml.
WKM: Подробнее о макетах
- Элементы списка должны выводить имя и описание, а также иметь номер для их идентификации. Для удобства работы создадим класс-модель
ListItemModel. Пропишем необходимые поля класса и гет-методы. В рамках этого задания все айтемы имеют одинаковые шаблоны для названия"Item <id>"и описание"This is item <id>". Вынесем эти строки-шаблоны в приватные поля класса, и используем в конструкторе для инициализации полейname,description.
Tip: Воспользуйтесь возможностями среды AndroidStudio. ПКМ -> Generate, и дальше сгенерируйте гет-методы.
- Для работы
RecyclerViewнеобходимы так же 2 сущности -Adapter, который будет контролировать создание и инициализацию внешнего вида каждого элемента в списке. ИLayoutManager- отвечает за компоновку элементов (списком, сеткой и т.п.). Сделаем свою реализацию адаптера, для начала создав пустой классListAdapter.
-
Создадим внутренний класс
ViewHolder, отнаследовав его отRecyclerView.ViewHolder. Этот класс нужен для реализации адаптера и будет содержать ссылки на вью элемента списка. -
Пропишем родителя для нашего адаптера - стандартную реализации -
RecyclerView.Adapter, в качестве параметра класса, укажем созданный на предыдущем шагеViewHolder. -
Создадим объект-список моделей элементов(
List<ListItemModel> items). -
После этого, среда попросит нас перегрузить 3 метода:
onCreateViewHolder- метод который вызовется при создании элемента в списке. В нем мы предоставляем визуальным представлением элемента (файлadapter_list_item.xml) объекту классаViewHolder.onBindViewHolder- вызывается при отрисовке каждого элемента на экране. Нужен для того что бы инициализировать вью элемента данными из модели.getItemCount- метод возвращающий количество элементов списка.
- Что бы удобно обновить содержимое списка, создадим метод update. В нем обновляем модель списка (
item), и уведомляем систему о том что содержимое списка было обновлено и необходимо перерисовать содержимоеRecyclerView. За последнее отвечает вызовnotifyDataSetChanged.
WKM: Подробнее о методах обновления списка и самом списке.
- Теперь у нас есть все необходимое для показа списка элементов. Создаем метод
initList. Для работыRecyclerViewсоздадим менеджер лэйаута -LinearLayoutManagerс вертикальной ориентацией. И объект адаптераListAdapter.
WKM: Подробнее о LinearLayoutManager.
-
Теперь необходимо передать адаптеру данные элементов списка для отображения. Воспользуемся созданным ранее методом
update. На вход дадим результат работы методаinitListItemModels. -
Добавим декоратор для элементов списка. Воспользуемся стандартной реализацией - классом
DividerItemDecoration. Декоратор будет добавлять разделяющию линию между элементами списка.
WKM: Подробнее о DividerItemDecoration.
- Согласно заданию, по нажатию на элемент списка у нас должно открываться активити с детальной информацией.
RecyclerViewне предоставляет удобный способ понять на какой элемент списка нажали. Нам необходимо реализовать эту функциональность самостоятельно. Реализуем слушатель нажатияView.OnClickListenerв классеViewHolderи перегрузим методonClick.
Мы могли бы попытать запускать открытие следующего экрана уже здесь, но это нарушило бы абстракцию. Адаптер не должен ничего знать о других экранах приложения, и тем более брать на себя работу Activity. Мы получим возможность использовать этот адаптер при необходимости и в других местах, когда по клику должно быть совершено другое действие.
Мы можем передавать адаптеру ссылку на активити, и в методе onClick ViewHolder'а вызывать у активити метод стартующий другой экран. Решение уже лучше, но нужно помнить о сокрытии данных. Получая ссылку на активити, в адаптере нам становятся доступны все ее методы.
Решением будет использовать новый тип, под видом которого мы будем передавать ссылку на аквити в адаптер. Создадим интерфейс ItemClickListener, с одним методом - onItemClick. И реализуем этот интерфейс в классе MainActivity.
-
В теле метода
onItemClickнеобходимо будет стартоватьDetailedActivity, но поскольку его сейчас нет, для проверки создадим всплывающее сообщение - Toast. WKM: Подробнее о Toast -
В адаптере создадим методы
setItemClickListener,getItemByPosition. -
В методе
onClickViewHolder'а, вызовемonItemClick. -
Возвращаемся в
MainActivityи вызываем методsetItemClickListenerу адаптера, передавthis- ссылку на активити. -
Создаем новую активити
DetailedActivity, при этом не забывая упомянуть о новом экране в манифест файле. -
Создадим разметку экрана в файле
activity_detailed.xml. -
Вынесем размеры и начертания текста в отдельные стили, в файл
styles.xml -
Вернемся в метод
onItemClick, который выполнится по нажатию на элемент списка. СоздадимIntentстартующийDetailedActivity. А так же с помощьюIntent'а мы передадим в другой экран данные элемента -nameиdescription. WKM: Подробнее об Intent. -
Перейдем в
DetailedActivity. В методеonCreateполучаем доп. данные, которые содержалIntent, извлекая их по присвоенным именным константам. И инициализируем этими данными вьшки (nameTextView,descriptionTextView). -
Заверщаюшим шагом будет добавление кнопки "Назад" в тулбаре у
DetailedActivity. Сначала в методеonCreateотобразим ее. А дальше - навесим логику закрытия активити, переопределив методonSupportNavigateUp.
- Для удобной работы с сервером, используем библиотеку
GSONдля парсингаjson'а иOkHTTPдля отправки и получения запроса. Добавим необходимые зависимости в build.gradle. И сразу добавим в манифест файл пермишен на доступ к интернету.
WKM: Permissions
WKM: GSON
WKM: OkHTTP
- Воспользуемся классом
AsyncTaskдля асинхронной работы с сервером. Создадим его наследника - классLoadCitiesTaskс параметрами класса -<Void, Void, List<ListItemModel>>
WKM: AsyncTask
-
Инициализируем объекты, в билдере реквеста укажем
URLc которого будем получать данные городов. -
В теле метода
doInBackground, который будет выполняться в отдельном потоке, создадим и выполним запрос к серверу. Далее узнаем успешно ли завершился наш запрос. Если да - то объектresponseбудет содержать данные, извлечем их и приведем к строке. -
Полученная строка - это
JSON, содержащий данные городов. Если бы мы не использовали библиотекуGSON, нам бы пришлось извлекать город из общего списка, а затем получать каждое свойство в отдельности. И заносить в модель города.GSON'у достаточно показать класс модели, и если он составлен правильно - преобразоватьJSONв объект этого класса. Потребуется 2 класса -ListItemModelsResponse, общий класс ответа, который содержит список изListItemModel(городов). Достаточно что бы имена в классе модели иJSON'е совпадали.
Tip: Если вам необходимо что бы названия полей отличались, при этом сохранить возможность преобразование объекта в JSON и обратно - воспользуйтесь аннотацией @SerializedName.
-
Модель готова и можно десереализовать данные используя метод
gson.fromJSON(). В параметрах передадим ссылку на класс, в который преобразовываем строку-JSON и саму строку. -
После того как мы получили список городов необходимо передать его назад в
MainActivity. Используем механизм обратного вызова, так же как мы поступили с передачей данных о кликнутом городе из адаптера. Создадим интерфесIMainActivityViewс методомshowCitiesи заимплементим его вMainActivity. Пока не будем отображать данные городов на экран, а выведем их на консоль для проверки. В конструктореLoadCitiesTaskполучим объектIMainActivityи сохраним его в полях класса. В методеonPostExecute, который выполнится один раз, после того как doInBackground будет успешно завершен, вызовем у объектаIMainActivityметодshowCities. -
Осталось только оптимизировать и почистить класс
MainActivity- в связи с тем, что список-айтемов превратился списком городов, информация о которых не создается в приложении, а получается с сервера. -
Для отображения картинок воспользуемся библиотекой
Glide. Она упрощает работу с изображениями, и предоставляет ряд полезных возможностей, например, кэширование. Добавим зависимость вbuild.gradle.
WKM: GLide
-
Будем выводить картинки сеткой, для этого изменим
LayoutManagerнаGridLayoutManager. Вместо вывода на консоль вshowCities, обновим данные у адаптера. -
Элемент списка теперь должен отображать картинку города - перейдем в
adapter_city_itemи изменим макет.TextViewс описанием нам уже не понадобится, а вот для отображения картинки города необходим виджетImageView(а еще лучше - его версия изsupportпакета). Что бы картинки хорошо смотрелись на любых экранах - будем использовать не фиксированные значения высоты и ширины, а зададим их через пропорции. В этом нам поможетConstraintLayout, для виджетов находящихся в этом контейнере можно указать параметр соотношенияlayout_constraintDimensionRatio.
WKM: Support Library
WKM: ConstraintLayout
-
Перейдем в сам адаптер. В методе по отрисовке айтема -
onBindViewHolder, вызовемGlide.with, передавая методу контекст приложения, задаем урл изображения, иImageViewдля отображения. -
Теперь изменим макет экрана с деталями -
activity_city_details- необходимо добавить виджет для вывода изображения. И немного отрефакторить файл, удалив ненужныйFrameLayout. Note: Изначально я так же пробовал изменить макет черезConstraintLayout, но решил не усложнять. Вот только не убрал и закомитил лишние атрибуты app:... иGuideLine) Прошу не обращать на них внимание, в случае отстутствияConstraintLayout, они попросту игнорируются. -
В
Intentна стартCityDetailedActivity, теперь передадим еще один доп. параметр -urlизображения. Аналогично адаптеру, настроим глайд на загрузку изображения вCityDetailedActivity.
WKM: После того как изображение загрузилось на главном экране, оно поместиться в кэш. Это полезно, потому что глайду не потребуется грузить картинку второй раз: для отображения он просто достанет старую версию из кэша.
- Осталось только выводить сообщение при отсутствии интернета. Для этого добавим в
AndroidManifestзапрос на разрешение -ACCESS_NETWORK_STATE. И воспользуемся службойCONNECTIVITY_SERVICEдля определения состояния интернета, в методеisNetworkAvailable.
- Для построения офлайн хранилища воспользуемся файлами настроек -
SharedPreferences. Создадим классDatabase, в конструктор передадим контекст приложения. Файлу настроек необходим уникальный идентификатор - создадим константуCITIES_DB_NAME.
WKM: SharedPreferences
-
Файлы настроек хранят информацию в формате Ключ-Значение, для сохранения
JSONстроки с данными полученными от сервера, создадим ключCITIES_RESPONSE_KEY- создадим его как поле класса. -
В методе
saveCitiesResponseполучим объект для редактирования файла -SharedPreferences.Editor, вызовом методаedit. Под созданным ранее ключом занесем строку c ответом. И сохраним изменения вызвав методapply. -
Создадим метод
loadCitiesResponse, в котором достанем строку по ключу. -
Настало время позаботиться о внутренней архитектуре и прийти к чему я уже давно шел - паттерну
MVP. Слой отвечающий за данные у нас уже есть, а вотViewиPresenterпока содержались в одном классеActivity.
Создадим отдельный класс MainPresenter и вынесем в него всю логику работы с данными. А именно:
- парсинг данных в метод
parseCitiesResponse. - проверка статуса интернет соединения в метод
isNetworkAvailable. - метод
loadCities, в котором в зависимости от наличия интернета данные будут браться либо из сети, либо из локального хранилища. Он будет вызываться изMainActivity.
Данные из БД, или с сервера теперь должны проходить через Presenter. Слой View служит лишь для отображения.
-
Создадим интерфейс
IDataSubscriber, который будет реализовыватьMainPresenter.onDataLoadedбудет вызываться в случае если данные были успешно получены, сохранять респонс в БД, парсить и передавать вMainAcitivity. В случае ошибки -onLoadError, вызывающий уMainActivityметодshowServerRequestFailed. -
Передадим в конструкторе для
LoadCitiesReponseTaskссылку наMainPresenterкак типIDataLoader.
- Студия предоставляет удобный темплейт для создания экрана с картами. Благодаря этому почти все задание можно свести к командам: ПКМ (на папке в окне структуры проекта) -> New -> Google -> Google Maps Activity. Дальше для использования карты необходимо будет получить
API KEY. Следуйте инструкциям из файлаgoogle_maps_api.
Tip: Или воспользуйтесь этим, более длинным руководством
-
У
MapActivityесть методonMapReady, который вызовется когда карта будет готова. В цикле, приходясь по списку городов, создадим маркеры (красные указатели), и добавим их на карту. Указателю необходимо передать координаты, с помощью объектаLatLng, и имя. -
Осталось создать кнопку по которой будет происходить старт
MapActivity. Сделаем ее с помощью меню опций. Создадимmain_menu.xmlв каталогеres\menu, и добавим один айтем с названием и id. -
Переопределим метод
onCreateOptionsMenuи добавим свое меню -main_menu.xml. -
Обработаем нажатие на айтем меню в методе
onOptionsItemSelected- с помощью объектаIntentстартанемMapActivity. И передадим в дополнительных параметрах список городов.






