В данном репозитории расположены различные материалы для пошагового создания с нуля простого мобильного приложения 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
. -
В методе
onClick
ViewHolder
'а, вызовем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
-
Инициализируем объекты, в билдере реквеста укажем
URL
c которого будем получать данные городов. -
В теле метода
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
. И передадим в дополнительных параметрах список городов.