| Android TV | Содержание | |
| Критерий | Баллы |
|---|---|
| Создан репозиторий для проекта | 0.25 |
| Создана ветка для мобильного приложения | 0.35 |
| Проект корректно сохранен в ветку для мобильного приложения и не требует дополнительного разархивирования | 0.45 |
| Итого | 1.05 |
По веткам:
Я бы поменял знак тире на подчёркивание. Не известно как тире в командной строке сработает.
Во-первых, перед выполнением модуля сразу создайте соответствующий репозиторий (в этом задании названия репозиториев не оговорено): Module_1, Module_2, Module_3
Во-вторых, создайте в репозитории ветку: git checkout -b Module_1
В-третьих, при работе желательно в основной ветке (Module_Х) держать только РАБОЧИЙ код, чтобы было что оценивать не зависимо от того, на каком месте вы остановились. Для этого создайте ещё одну ветку, например work (git checkout -b work) и работайте в ней. После отладки какой-то фичи (например, сделали авторизацию) фиксируете изменения, переключаетесь на основную ветку (Module_1), сливаете ветки и заливаете основную ветку в репозиторий. Затем возвращаетесь в ветку work:
фиксируем изменения в ветке work
git add .
git commit -m "сделал авторизацию"
переключаемся на ветку Module_1
git checkout Module_1
сливаем изменения из ветки work (в текущую) и, при желании, пушим на удаленный сервер
git merge work
git push origin Module_1
возвращаемся в рабочую ветку
git checkout work
итоговая структура репозиториев должна получиться примерно такая:
Необходимо корректно обрабатывать запросы к серверу. В случае получения ошибки от сервера или отсутствия соединения с сетью Интернет необходимо отобразить соответствующий текст ошибки с помощью диалогового окна.
Тут вам придумывать ничего не надо - в классе Http уже есть пример, как перехватывать исключения
Необходимо строго следовать предложенному дизайну. Макеты приложения доступны по ссылке: https://www.figma.com/file/tD64TlCMQEqlr8OTv6bW2o/KOD1.4-Variant3?node-id=0%3A1
Описание протокола API доступно по ссылке:
https://app.swaggerhub.com/apis-docs/WorldSkills-MAD/WorldCinema/1.0.0(так как АПИ на момент написания этих лекций не доступно, то я нарисовал своё АПИ и Swagger для него лежит тут)Проект приложения должен быть структурирован по экранам, то есть исходные файлы конкретного экрана должны быть в соответствующей папке. Общие для нескольких экранов классы необходимо поместить в папку common.
В андроиде нужно делать не папки, а пакеты. Например, создаёте пакет LaunchScreen и в него переносите MainActivity. Для остальных окон сначала делаете пакет, например SignUpScreen, затем в этом пакете создаёте SignUpActivity.
| Критерий | Баллы |
|---|---|
| Проект приложения для смартфона имеет корректную структуру (все файлы распределены по папкам, соответствующим экранам приложения) | 0.5 |
Необходимо реализовать следующий функционал:
- Создайте проект. Настройте иконку приложения согласно макету. Следует учесть разницу в отображении иконок на различных версиях операционной системы.
| Критерий | Баллы |
|---|---|
| Создан проект мобильного приложения | 0.1 |
| Проект мобильного приложения успешно собирается | 0.1 |
| Иконка приложения соответствует Заданию | 0.2 |
Все ресурсы нужно получать из "фигмы"
Как пользоваться "фигмой" для получения ресурсов:
+, чтобы открыть настройки экспортаБраузер сохранит файл с тем же названием, что и у элемента вёрстки, например Icon-android 512.png). Переименуйте его, убрав пробелы, чёрточки и любую другую фигню, которая может помешать воспринимать ресурс по имени (оставьте только латинские буквы и цифры). Ещё, как выяснилось на практике, ресурсы не могут быть в CamelCase, при именовании не используйте большие буквы.
Про создание иконок смотри первичную настройку приложения
В этом задании используется тёмная тема. Для переключения в ночной режим в файле разметки в режиме "дизайна" установите соответствующий параметр.
или в коде принудительно переключитесь в ночной режим:
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
- Реализуйте экран Launch Screen согласно макету. Текст должен быть отдельным элементом. Логотип приложения должен быть расположен по центру экрана.
| Критерий | Баллы |
|---|---|
| Текст на Launch Screen является отдельным элементом | 0.1 |
| Логотип на Launch Screen отцентрирован | 0.1 |
Экспортируйте необходимые файлы из "фигмы".
Перенесите (drag-n-drop) полученные файлы в ресурс drawable проекта
в шаблон окна добавьте элемент ImageView и отцентрируйте его горизонтально и вертикально (на этом окне можно не извращаться и не добавлять LinearLayout)
Аналогично экспортируйте и внедрите в ресурсы картинку с текстом WorldCinema.
Нигде в задании не сказано, как переходить на окно авторизации - добавьте таймер на несколько секунд, чтобы экперты успели рассмотреть вёрстку, и переходите на следующее окно
- Реализуйте экран SignUp Screen согласно макету:
- При нажатии на кнопку "Зарегистрироваться" необходимо проверять поля для ввода на пустоту, а также email на корректность (требования к email описаны в документации к API). При некорректном заполнении необходимо отобразить ошибку с помощью диалогового окна. Так же необходимо проверять равенство пароля и его повтора.
- При корректном заполнении формы необходимо отправлять запрос регистрации на сервер. При получении ошибки от сервера ее необходимо отобразить с помощью диалогового окна. При успешной регистрации нужно автоматически осуществить авторизацию и перейти на Main Screen.
- При нажатии на кнопку "У меня уже есть аккаунт" необходимо осуществлять переход на SignIn Screen.
- При первом запуске приложения после Launch Screen должен отображаться SignUp Screen. При последующих - SignIn Screen.
| Критерий | Баллы |
|---|---|
| Реализован запрос регистрации. Запрос фиксируется сервером | 0.7 |
| Экран соответствует макету (оценивается верстка). Корректно реализованы 9 элементов: логотип, название, 5 текстовых полей, 2 кнопки (минус 0,1 за каждый отсутствующий или некорректный элемент) | 0.9 |
| Поля для ввода валидируются на пустоту (минус 0,1 за каждое поле без валидации) | 0.5 |
| Пароль и его повтор проверяются на равенство | 0.1 |
| При получении ошибки от сервера она отображается с помощью диалогового окна | 0.3 |
| Email проверяется на удовлетворение шаблону из Задания | 0.2 |
| При первом запуске приложения первым отображается SignUp Screen, при последующих - SignIn | 0.1 |
| Итого | 2.8 |
Тут уже можно завернуть всё в LinearLayout
В панели свойств (правая панель) фигмы указаны параметры вёрстки выбранного элемента. В частности там указаны цвет элемента (нужен для кнопки), внутренние отступы (padding) и радиус границы.
Можно было бы использовать специальный компонент с маской ввода, но в задании сказано что валидация делается вручную при клике на кнопку "Зарегистрироваться". Пример ниже:
// обратите внимание на наименование переменных: самоочевидное название + тип элемента
lateinit var nameEditText: EditText
lateinit var signUpButton: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// не забываем инициализировать ссылки на визуальные элементы
nameEditText = findViewById(R.id.nameEditText)
signUpButton = findViewById(R.id.signUpButton)
signUpButton.setOnClickListener {
try {
if (nameEditText.text.isEmpty())
throw Exception("Не заполнено имя пользователя")
тут остальные проверки
тут отправка запроса регистрации
} catch (e: Exception) {
// любую ошибку показываем на экране
AlertDialog.Builder(this)
.setTitle("Ошибка")
.setMessage(e.message)
.setPositiveButton("OK", null)
.create()
.show()
}
}
}
Swagger - это фреймворк для спецификации RESTful API. Его прелесть заключается в том, что он дает возможность не только интерактивно просматривать спецификацию, но и отправлять запросы.
Открываем ссылку на описание АПИ и смотрим что там есть:
В начале идёт общая информация. Нам тут пока интереснен только так называемый базовый URL. Здесь он находится в блоке Servers (http://cinema.areas.su), но может быть написан и просто текстом (как базовые урлы для картинок и видео).
ВАЖНО!!! figma и swagger на момент написания этой лекции работают, а вот сервер АПИ - нет, видимо его запускали только на время работы демо-экзамена. Я написал простенький симулятор этого сервера, он доступен по адресу http://cinema.kolei.ru.
Дальше идут описания методов АПИ. Рассмотрим подробно метод Регистрация:
В заголовке указано какой метод и "путь" используются для запроса. К пути надо добавить "базовый урл" и получим полный адрес запроса: http://cinema.areas.su/auth/register
В параметрах (Parameters) указываются параметры GET-запросов, передаваемые в строке запроса. У нас тут пусто.
Тело запроса (Request body). Тут указано что тело запроса обязательно должно присутсвовать (required) и формат application/json
В теле запроса должна быть JSON-строка. Пример её можно посмотреть на вкладке Example value, но нам интереснее вкладка schema - на ней описаны типы данных (string), описание поля (что это такое вообще) и, возможно, обязательность использования поля. Например, для поля email расписан шаблон, которому оно должно соответсвовать.
Коды ответов (Responses)
Тут надо быть внимательным, коды могут отличаться.
Ну и самое приятное в Swagger - можно прямо в нём проверить результат работы. Кликаем кнопку "Try it out", вводим в открывшемся окне тело запроса и нажимаем выполнить (Execute). Таким образом нам не нужны ни Postman ни VSCode с плагинами
Пример отправки запроса я не привожу - вы уже должны написать его самостоятельно.
Такая проверка делается с помощью регулярных выражений. Например:
^[a-z0-9]+@[a-z0-9]+\.[a-z]{1,3}$
В задании сказано, что имя и домен второго уровня должны содержать только маленькие буквы и цифры.
^ означает "начало строки"+ означает 1 и более)@ пишем как есть$ означает "конец строки"Пример проверки электронной почты:
val re = Regex("""^[a-z0-9]+@[a-z0-9]+\.[a-z]{1,3}$""")
val res = re.find(emailEditText.text.ToString())
// если регулярное выражение ничего не найдет, то вернёт null
if(res == null)
throw Exception("Электронная почта не соответствует шаблону")
Для того чтобы узнать первый запуск или нет, нужно сохранить этот признак в постоянное хранилище. Пример работы с хранилищем есть в шпаргалке. Попробуйте разобраться самостоятельно.
При последовательном кодировании у вас получается слишком тяжёлая реализация (много вложенных вызовов - "callback hell")
Чтобы избежать многократной вложенности, вспомним, что функцию обратного вызова мы можем объявить отдельно:
private fun showAlert(message: String) {
runOnUiThread {
AlertDialog.Builder(this)
.setTitle("Ошибка")
.setMessage(message)
.setPositiveButton("OK", null)
.create()
.show()
}
}
private val registrationCallback: (response: Response?, error: Exception?)->Unit = {
response, error ->
try {
// тут проверка результата запроса и при необходимости запуск авторизации
} catch (e: Exception) {
// любую ошибку показываем на экране
showAlert(e.message)
}
}
private val authorisationCallback: (response: Response?, error: Exception?)->Unit = {
response, error ->
try {
// тут проверка результата запроса и при необходимости переход на главное окно
} catch (e: Exception) {
// любую ошибку показываем на экране
showAlert(e.message)
}
}
// и обработчик кнопки "Регистрация" выглядит уже легче
registrationButton.setOnItemClickListener { parent, view, position, id ->
try {
// тут проверки полей
Http.call(
Http.buildRequest(
"http://s4a.kolei.ru/login",
json.toString()
),
registrationCallback
)
} catch (e: Exception) {
// любую ошибку показываем на экране
showAlert(e.message)
}
}
В итоге общая логика выглядит более структурированной:
- Реализуйте экран SignIn Screen согласно макету:
- При нажатии на кнопку "Войти" необходимо проверять поля для ввода на пустоту, а также email на корректность (требования к email описаны в документации к API). При некорректном заполнении необходимо отобразить ошибку с помощью диалогового окна. При корректном заполнении формы необходимо отправить на сервер соответствующий запрос.
- При нажатии на кнопку "Регистрация" необходимо осуществлять переход на SignUp Screen.
- При успешной авторизации необходимо осуществлять переход на экран Main Screen. При получении ошибки от сервера необходимо отобразить ее с помощью диалогового окна.
| Критерий | Баллы |
|---|---|
| Реализован запрос авторизации. Запрос фиксируется сервером | 0.7 |
| При получении ошибки от сервера она отображается с помощью диалогового окна | 0.3 |
| Экран соответствует макету (оценивается верстка). Корректно реализованы 6 элементов: логотип, название, 2 текстовых поля, 2 кнопки (минус 0,1 за каждый отсутствующий или некорректный элемент) | 0.6 |
| Поля для ввода валидируются на пустоту (минус 0,1 за каждое поле без валидации) | 0.2 |
| Email проверяется на удовлетворение шаблону из Задания | 0.3 |
| Итого | 2.1 |
Тут просто копипастите предыдущий экран, удалив лишнее, ничего нового.
Запрос тоже практически идентичный предыдущему, расписывать не буду.
Не забудьте сохранить полученный токен в классе приложения (и в предыдущем окне тоже)
- Реализуйте экран Main Screen согласно макету:
- На экране необходимо отобразить обложки фильмов из подборки «new» (информацию о фильмах необходимо запрашивать с сервера). Обложки должны быть отображены в виде карусели, необходимо реализовать возможность пролистывания с помощью жеста swipe.
- При пролистывании обложек название фильма в верхней части экрана должно меняться.
- При нажатии на обложку необходимо переходить на Chat Screen для соответствующего фильма.
| Критерий | Баллы |
|---|---|
| Реализован запрос подборки фильмов. Запрос фиксируется сервером | 0.5 |
| Обложки пролистываются с помощью swipe | 0.5 |
| Обложка фильма отображается с закругленными углами | 0.3 |
| При нажатии на обложку осуществляется переход на экран чата соответствующего фильма | 0.1 |
| Итого | 1.4 |
В АПИ много лишних методов - ищете метод, который подходит по смыслу:
GET /movies Получить список фильмов
Так как метод запроса GET, то тела запроса (body) у него нет, но зато есть параметры запроса (Parameters), причём параметры эти обязательны (required).
Нам по заданию нужно отобразить новые (new), поэтому итоговый URL запроса будет такой:
http://cinema.kolei.ru/movies?filter=new
где:
http://cinema.kolei.ru - базовый URL/movies - путь (path) запроса? - разделитель полного URL запроса и списка параметровfilter=new - параметры запроса в формате имя=значение. Если необходимо передать несколько параметров, то они разделяются знаком &. Например: ?param1=one¶m2=twoФормат ответа - массив информации о фильмах. Нам, судя по вёрстке, нужны пока только название фильма (name) и обложка (poster). И, скорее всего, идентификатор (movieId), чтобы указать его в следующих запросах.
Обратите внимание на формат ответа, обязательным почему-то является только одно поле - movieId. Возможно это просто ошибка в формировании АПИ, но лучше всё-таки учесть такую вероятность и при отсутствии полей name или poster просто не добавлять такие элементы в список фильмов.
Для вывода списка фильмов используйте компонент RecyclerView (направление пролистывания не указано, так что делайте как вам больше нравится)
Есть вариант с программным изменением картинки, но он не учитывает последующую возможную обрезку картинки (понадобится при выводе аватарки).
Есть более простой и наглядный вариант: элемент ImageView заворачивается в контейнер androidx.cardview.widget.CardView, которому и задаётся радиус:
<androidx.cardview.widget.CardView
app:cardCornerRadius="100dp"
>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</androidx.cardview.widget.CardView>
Для перехода между экранами внизу главного экрана есть панель навигации. Для её реализации есть отдельный механизм, который мы пока не рассматривали. Вы можете сюда поместить горизонтальный LinearLayout.
- Реализуйте экран Profile Screen согласно макету:
- Данные о пользователе необходимо запрашивать с сервера.
- При нажатии на кнопку "Изменить" необходимо реализовать изменение аватара пользователя:
- Пользователь выбирает источник фотографии (камера или Галерея), выбор источника следует реализовать с помощью диалогового окна. При выборе Галереи необходимо открывать Галерею. При выборе камеры осуществлять переход на экран Камеры не нужно.
- Пользователь выбирает фотографию.
- Фотография отправляется на сервер.
- В случае успеха аватар пользователя заменяется на новый, в случае ошибки она отображается с помощью диалогового окна.
- При нажатии на кнопку "Выход" необходимо осуществлять переход на экран авторизации.
- При нажатии на кнопку «Обсуждения» необходимо переходить на соответствующий экран.
| Критерий | Баллы |
|---|---|
| Изображения отображаются без искажений | 0.1 |
| Радиус скругления углов кнопки "Выход" соответствует макету | 0.1 |
| Реализован запрос информации о пользователе. Запрос фиксируется сервером | 0.5 |
| Реализована отправка аватара. Запрос фиксируется сервером | 1 |
| При получении ошибки от сервера она отображается | 0.3 |
| Аватар пользователя обрезается | 0.2 |
| Аватар пользователя загружается с сервера | 0.2 |
| Кнопки "Обсуждения", "История", "Настройки" соответствуют макету оценивается верстка): для каждой кнопки иконка, заголовок, стрелка (минус 0,2 за каждый отсутствующий, неполный или некорректный элемент) | 0.6 |
| Реализован выбор источника фотографии | 0.2 |
| Итого | 3.2 |
Если предыдущие запросы были доступны всем, то этот запрос может делать только авторизованный пользователь.
Для того, чтобы запрос был авторизованным, нужно в заголовок запроса добавить параметр Authorization, как описано в начале документа.
Напоминаю синтаксис добавления заголовка в классе http:
Http.call(
Http.buildRequest(
"http://cinema.kolei.ru/user",
headers = mapOf("Authorization" to "Bearer ${app.token}")
),
userCallback
)
Успешный запрос вернёт массив объектов (с одним элементом, естественно), учитываёте при разборе JSON.
Чтобы протестировать этот запрос в swagger-e, нужно получить токен авторизации запросом /auth/login, затем кликнуть кнопку Authorize на окне выше и ввести токен в поле Value:
Иконка замка сменится на "закрыто". После этого можно выполнять запросы, требующие авторизации.
В ответ на запрос информации о пользователе придёт что-то подобное:
С диалоговыми окнами мы уже знакомы (мы их используем для вывода текста ошибок). У класса AlertDialog есть метод, позволяющий сделать выбор из массива:
// сначала объявляем массив строк для выбора
val choiceItems = arrayOf("Галерея","Камера")
// создаём и показываваем диалог
AlertDialog.Builder(this)
.setTitle("Выберите источник")
.setNegativeButton("Отмена", null)
.setSingleChoiceItems(choiceItems, -1){
dialog, index ->
Toast.makeText(this, "$index", Toast.LENGTH_LONG).show()
dialog.dismiss()
}
.create()
.show()
При создании диалога добавился вызов метода setSingleChoiceItems, который как раз и задаёт массив элементов для выбора. Первым параметром этого метода задаётся массив строк, вторым - активный по-умолчанию элемент (можно указать -1, если не нужно выбирать что-то по-умолчанию) и третьим параметром задаётся лямбда функция, которая вызывается при выборе элемента списка.
В лямбда функцию передаётся два параметра:
Я в примере выше просто вывожу на экран номер выбранного элемента и закрываю диалог (dialog.dismiss()). Вам нужно в зависимости от выбранного элемента либо показать Галерею, либо открыть приложение Камера (можно прямо тут, но можно и выделить в отдельную лямбда функцию).
Открытие внешних программ (активностей) осуществляется тем же вызовом класса Intent. Только в параметрах мы должны передавать не контекст (this), а тип нужной нам активности.
Для получения каких-то данных используется тип Intent.ACTION_PICK.
Для конкретизации типа данных указывается свойство type
Для открытия активности, которая может вернуть список картинок:
// константа для анализа результата объявляется на уровне класса
val GALLERY_REQUEST = 1
val photoPickerIntent = Intent(Intent.ACTION_PICK)
// фильтр
photoPickerIntent.type = "image/jpg"
//запускаем запрос, указав что ждём результат
startActivityForResult(photoPickerIntent, GALLERY_REQUEST)
Получение результата от активности мы уже разбирали, когда делали выбор города в проекте "погода". Обработчик результата всего один для класса активности. Поэтому при запуске активности мы и передаём уникальный номер запроса, чтобы при разборке ответа знать от кого он пришёл:
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
intent: Intent?)
{
// проверяем код запроса
if (requestCode == GALLERY_REQUEST) {
// убеждаемся что выполнено успешно (пользователь мог ничего и не выбрать в галерее)
if (resultCode == Activity.RESULT_OK && intent != null) {
// галерея возвращает URI картинки в свойстве data параметра intent
sendFile(intent.data!!)
}
}
}
Класс StreamHelper, используемый при получении файла, лежит в шпаргалках
private fun sendFile(fileUri: Uri) {
// получаем из Uri файла указатель на поток данных
val fileStream = contentResolver
.openInputStream(fileUri)
// получаем файл
val fileBody: RequestBody = StreamHelper
.create(
"image/jpg".toMediaType(),
fileStream!!)
val requestBody = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("token", app.token)
.addFormDataPart(
"file", // название части запроса
"filename.jpg", // имя файла
fileBody // тело файла
)
.build()
val request = Request.Builder()
.url("http://cinema.kolei.ru/user/avatar")
.post(requestBody)
.build()
Http.call(request) { response, error ->
try {
if (error != null) throw error
if (!response!!.isSuccessful)
throw Exception(response.message)
// всё ОК
runOnUiThread {
/* при успешной отправке меняем аватарку
(ImageView в вашей активности)*/
avatarImageView.setImageURI( fileUri )
}
} catch (e: Exception) {
showAlert(e.message!!)
}
}
}
При выборе файла из галереи пропорции будут скорее всего не одинаковые, поэтому при отображении вы должны задать как отображается изображение:
<ImageView
android:layout_width="88dp"
android:layout_height="88dp"
android:adjustViewBounds="true"
android:scaleType="centerCrop"
Параметр android:adjustViewBounds="true" включает масштабирование, а параметр android:scaleType указывает тип масштабирования. Вариантов масштабирования несколько, вы можете посмотреть на практике как они влияют на результат. Но нам нужен centerCrop, означающий, что картинка будет центрироваться, а то, что не помещается отрежется.
Тут всё просто: в горизонтальный LinearLayout помещаете три элемента, как в вёрстке, задаёте этому LinearLayout-у тег id и событие setOnClickListener "вешаете" на LinearLayout. (Событие setOnClickListener объявлено в базовом классе View, от которого наследуются все визуальные элементы)
- Реализуйте экран Chat List Screen согласно макету:
- Информацию необходимо запрашивать с сервера (запрос чатов пользователя). Если информация с сервера содержит дубляжи – их необходимо удалить. Если ваш пользователь еще не имеет чатов, сервер присылает пустой список, отправьте сообщение от текущего пользователя в чате с id = 1 с помощью Swagger или Postman.
- В подзаголовке ячейки необходимо отобразить последнее сообщение в соответствующем чате + имя его автора. Текст сообщения необходимо обрезать до двух строк.
- Реализуйте отображение постеров к фильмам. Для получения постеров используйте подходящий запрос из API. Если постер для данного фильма нельзя получить из API - сгенерируйте абревиатуру по следующему правилу: если название фильма состоит и одного слова - необходимо взять первые две буквы слова; иначе - первые буквы первого и второго слова.
- При нажатии на ячейку необходимо осуществлять переход на Chat Screen для выбранного фильма.
| Критерий | Баллы |
|---|---|
| Реализован запрос списка чатов пользователя | 0.5 |
| Реализован запрос сообщений чата | 0.5 |
| Ячейка таблицы соответствует макету (оценивается верстка).Корректно реализованы 3 элемента: изображение, название, подзаголовок (минус 0,1 за каждый отсутствующий или некорректный элемент). | 0.3 |
| В подзаголовке отображается последнее сообщение для данного чата | 0.2 |
| Текст сообщения обрезается до двух строк | 0.2 |
| Реализовано отображение аббревиатуры согласно Заданию | 0.3 |
| Итого | 2 |
АПИ, на мой взгляд, реализовано кривовато, не понятно как попадать в список чатов пользователя, я переделал АПИ так, чтобы вы реализовали список чатов фильма, кликнув по его постеру
GET /chats/{movieId}
Нужно учитывать, что запрос требует указания идентификатора выбранного фильма, а в остальном ничего особенного.
Учитывая, что в информации о чате мы должны показать последнее сообщение из чата, в дата класс надо добавить и свойство для этого сообщения
data class Chat(
// данные из списка чатов
chatId: String,
movieId: String,
name: String,
// дополнительные данные
avatar: String? = null, // имя файла
lastMessage: String? = null // текст последнего сообщения
)
И после заполнения списка чатов пробежаться по нему в цикле и запросить/заполнить недостающие данные.
При отображении дополнительных данных в RecycleView учитывайте, что они ещё могут быть null:
someTextView.text = item[position].lastMessage? ?: ""
if (item[position].avatar != null) {
// запросить файл у сервера
}
В информации о чате мы должны показать последнее сообщение этого чата, поэтому после получения списка чатов мы должны по каждому из них запросить и список сообщений этого чата. Последнее сообщение (тут желательно сделать проверку по дате) этого списка вписать в элемент списка чатов (ищем по Id чата)
GET /chats/{chatId}/messages
Для вывода постера нужно иметь список фильмов. Можно запросить его ещё раз, а можно в главном окне хранение фильмов сделать не в свойствах класса активности, а в свойствах класса приложения (там, где вы храните токен)
Для переключения типа аватарки фото/аббревиатура можно применить такую вёрстку:
<androidx.cardview.widget.CardView
android:layout_width="100dp"
android:layout_height="match_parent"
app:cardCornerRadius="50dp"
>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#fee"
android:textSize="50dp"
android:gravity="center"
android:text="НФ"
android:visibility="visible"
/>
<ImageView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:src="@drawable/videocamera2"
android:visibility="gone"
/>
</FrameLayout>
</androidx.cardview.widget.CardView>
CardView задаёт форму и размер, FrameLayout позволит выводить и текст и изображение в одном месте, а атрибутом visibility включается/выключается нужный элемент.
Текст в TextView, надеюсь, напишете сами
Просто добавить атрибут lines:
<TextView
android:lines="2"
...
По клику на стрелку влево (в верхней строке окна) вызвать метод finish() активности.
Переход из окна профиля по клику на "Обсуждения".
Метод для получения списка всех чатов: GET /user/chats
Activity используйте эту же самую
- Реализуйте экран Chat Screen согласно макету:
- Сообщения необходимо упорядочить от старых к новым сверху вниз. Для сегодняшних сообщений необходимо отобразить заголовок "Сегодня".
- "Облако" сообщения должно растягиваться по содержимому.
- Последовательно идущие сообщения одного автора необходимо группировать (расстояния между сообщениями должны быть меньше, как на макете).
- Реализуйте блок отправки сообщения как на макете. При вводе сообщения поле для ввода должно растягиваться по вертикали.
- При нажатии на кнопку "Отправить" необходимо отправить сообщение на сервер. При позитивном ответе от сервера необходимо отобразить сообщение в чате. При возникновении ошибки - отобразить ошибку с помощью диалогового окна.
- Необходимо валидировать поле для ввода на пустоту. При отсутствии текста сообщения необходимо отобразить ошибку с помощью диалогового окна.
| Критерий | Баллы |
|---|---|
| Реализован запрос сообщений чата | 0.5 |
| Реализована отправка сообщения в чат | 0.7 |
| Отображение сообщения соответствует макету (оценивается верстка). Корректно реализованы 3 элемента: аватар, текст, подзаголовок (минус 0,1 за каждый отсутствующий или некорректный элемент) | 0.3 |
| Сообщения упорядочены согласно Конкурсному Заданию | 0.3 |
| "Облако" сообщения растягивается по содержимому без искажений радиуса скругления углов | 0.4 |
| Отображение последовательно идущих сообщений одного автора соответствует макету (расстояния между сообщениями меньше) | 0.4 |
| Поле для ввода растягивается при добавлении текста | 0.4 |
| При получении ошибки от сервера она отображается с помощью диалогового окна | 0.3 |
| При попытке отправить пустое сообщение отображается ошибка с помощью диалогового окна | 0.3 |
| Итого | 3.6 |
Как получить список сообщений мы уже разбирали в прошлой лекции.
Для того, чтобы массив объектов (экземпляров какого-либо класса) можно было сортировать по сложному условию, класс должен реализовывать интерфейс Comparable и переопределить метод compareTo:
>data class ChatMessage( > val chatId: String, > val messageId: String, > val creationDateTime: LocalDateTime > // тут остальные поля > ): Comparable<ChatMessage> >{ > override fun compareTo(other: ChatMessage): Int { > return if(other.creationDateTime > this.creationDateTime) 1 else -1 > } >} > >... > >// после заполнения списка сообщений просто вызвать метод sort() >chatMessageList.sort() >``` Для сравнения простых свойств есть вариант проще:kt chatMessageList.sortBy(
item -> item.creationDateTime)
chatMessageList.sortByDescending(
item -> item.creationDateTime)
### Вывод даты перед группой сообщений
1. В вёрстке элемента сообщения должен быть элемент для даты
2. В адаптере **RecyclerView** сравнивать дату текущего сообщения с датой предыдущего (учитывайте, что предыдущего может не быть). Если дата отличается, то выводим дату, если не отличатется, то скрываем элемент даты (`visible = View.GONE`)
### Группировка сообщений пользователя
В принципе тоже самое что и с датой, только менять верхнюю границу сообщения
kt // считываем текущие параметры разметки элемента val param = messageTextView.layoutParams as ViewGroup.MarginLayoutParams
// устанавливаем нужную границу param.setMargins(10,10,10,10)
// применяем изменившиеся параметры к элементу messageTextView.layoutParams = param
### Экран коллекций

>9. Реализуйте экран Collections Screen согласно макету:
> * При нажатии на иконку в правом верхнем углу необходимо переходить на экран Create Collection Screen.
> * На экране необходимо отображать созданные коллекции. **Информация о коллекциях должна храниться в памяти устройства**. Необходимо хранить название коллекции и иконку.
> * Реализуйте Swipe-to-delete для удаления коллекции, в том числе из памяти устройства.
Критерий | Баллы
---------|:----:
Ячейка коллекции соответствует макету (оценивается верстка). Корректно реализованы 3 элемента: иконка, название, стрелка (минус 0,1 за каждый отсутствующий или некорректный элемент) | 0.3
Реализована возможность удаления коллекции с помощью swipe-to-delete | 0.3
**Итог** | 0.6
Так как реализовывать фичи надо атомарно, то желательно завершить реализацию этого экрана, прежде чем делать следующие.
### Хранение информация о коллекциях в памяти устройства
Вспоминаем метод *getSharedPreferences* - список коллекций можно хранить JSON-строкой (напоминаю, что хранилище оперирует только скалярными типами). В качестве идентификатора иконки использовать `id` ресурса.
Чтобы иметь идентификаторы ресурсов иконок создадим массив ресурсов (естественно эти ресурсы должны быть в проекте). Можно его объявить локально, но нам он понадобится ещё на одном экране - выбор иконки для создания коллекций, поэтому ~~вынесем его в ресурсы~~ (выковыривание из ресурсов получилось слишком трудоёмким, этот вариант правильный, но использовать будем другой):
>* в каталоге `res/values` создайте файл ресурсов (*New -> Values Resource File*) и опишите в нём массив ресурсов:
>
> ```xml
> <?xml version="1.0" encoding="utf-8"?>
> <resources>
> <integer-array name="icons">
> <item>@drawable/ico1</item>
> <item>@drawable/ico2</item>
> <item>@drawable/ico3</item>
> <item>@drawable/ico4</item>
> <item>@drawable/ico5</item>
> <item>@drawable/ico6</item>
> <item>@drawable/ico7</item>
> <item>@drawable/ico8</item>
> <item>@drawable/ico9</item>
> </integer-array>
> </resources>
> ```
>
> Причём Android Studio достаточно умный, чтобы показать нам содержимое массива
>
> 
>
>* в классе активности получим массив из ресурсов:
>
> ```kt
> // массив идентификаторов объявлен на уровне класса
> private var iconArray = arrayListOf<Int>()
>
> // в конструкторе считываем массив из ресурсов и преобразуем его в массив идентификаторов...
> val typedArray = resources.obtainTypedArray(R.array.icons)
> for(i in 0 until typedArray.length()){
> iconArray.add( typedArray.getResourceId(i, 0) )
> }
> typedArray.recycle()
> ```
Реализация через ресурсы получается достаточно сложной, проще этот массив объявить в классе приложения (MyApp):
kt val iconArray = arrayOf(
R.drawable.ico1,
R.drawable.ico2,
R.drawable.ico3,
R.drawable.ico4,
R.drawable.ico5,
R.drawable.ico6,
R.drawable.ico7,
R.drawable.ico8,
R.drawable.ico9
)
Как быть, если у нас ещё нет сохраненного списка коллекций?
При получении списка коллекций, в качестве значения по-умолчанию, вернём JSON-строку, где `id` зададим первый элемент массива ресурсов:
kt val collectionsString = myPreferences.getString(
"collections",
"""[{"id":${iconArray[0]},"name":"Первый список"}]"""
)
Преобразовать полученную JSON-строку в массив и сделать RecyclerView вы уже можете сами.
### Удаление свайпом с подтверждением
В констуктор, после создания адаптера для RecyclerView добавьте отслеживание свайпа:
kt val simpleCallback = object :
ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean = false
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
deleteItem(viewHolder.adapterPosition)
}
}
val itemTouchHelper = ItemTouchHelper(simpleCallback) itemTouchHelper.attachToRecyclerView(collectionsRecycleView)
И в классе определить метод *deleteItem*:
kt fun deleteItem(position: Int){
// удаляем элемент из списка коллекций
collectionList.removeAt(position)
// обновляем RecyclerView
collectionsRecycleView.adapter?.notifyDataSetChanged()
// самостоятельно сформируйте JSON-массив оставшихся коллекций и сохраните его в preferences
}
### Экран создания коллекций


>10. Реализуйте экран Create Collection Screen согласно макету:
> * При открытии экрана в качестве иконки должно быть выбрано случайное изображение из коллекции иконок.
> * При нажатии на кнопку "Выбрать иконку" необходимо осуществлять переход на экран Icon Selection. Реализуйте данный экран в соответствии с макетом.
> * При нажатии на кнопку "Сохранить" необходимо сохранить новую коллекцию в памяти устройства и закрыть экран.
> * Необходимо проверять на пустоту поле для ввода названия коллекции. При отсутствии значения необходимо отобразить сообщение об ошибке.
Критерий | Баллы
---------|:----:
Экран соответствует макету (оценивается верстка). Корректно реализованы 6 элементов: заголовок экрана, поле для ввода, иконка, 3 кнопки (минус 0,1 за каждый отсутствующий или некорректный элемент) | 0.6
При создании коллекции без названия отображается соответствующая ошибка | 0.3
Данные о коллекции сохраняются в памяти устройства | 1
Экран выбора иконки соответствует макету (оценивается верстка): корректно реализованы 2 элемента: заголовок, кнопка (минус 0,1 за каждый отсутствующий или некорректный элемент) | 0.2
Ячейка таблицы иконок соответствует макету (2 элемента) | 0.2
При открытии экрана отображается случайная иконка коллекции | 0.1
**Итого** | 2.4
Про экран создания коллекции писать особо нечего, вёрстка простая, запросов к АПИ нет...
### Список иконок плиткой
Намного интереснее следующий экран - выбор иконки для коллекции. Создаём новую активность и запускаем её с получением результата.
В созданной активности:
kt icoRecyclerView = findViewById(R.id.iconRecyclerView)
// в качестве менеджера разметки для RecyclerView используем класс GridLayoutManager. Во втором параметре указывается сколько колонок будет у нашей сетки icoRecyclerView.layoutManager = GridLayoutManager(this, 4)
// создаем адаптер val someClassAdapter = IconAdapter(app.iconArray, this)
// задаём обработчик клика по иконке, по которому возвращаем id выбранной иконки someClassAdapter.setItemClickListener {
val resultIntent = Intent()
resultIntent.putExtra("icoIndex", it)
setResult(RESULT_OK, resultIntent)
finish()
}
icoRecyclerView.adapter = someClassAdapter
В качестве элемента сетки у меня такой файл разметки:
xml <?xml version="1.0" encoding="utf-8"?> <ImageView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/iconImageView"
android:layout_width="match_parent"
android:layout_margin="10dp"
android:layout_height="wrap_content"/>
То есть просто **ImageView** без всяких контейнеров (иконки я импортировал вместе с фоном).
Но если внимательно перечитать критерии оценки: "Ячейка таблицы иконок соответствует макету (2 элемента)", то получается что сделал неправильно. Вы можете разместить тут **FrameLayout**, внизу разместить фон, а сверху иконку (понятно, что на реальном экзамене вы критериев не знаете, но учитывайте эти грабли - если в фигме фон и иконка раздельно, то наверное ожидается, что и в вёрстке они будут отдельными элементами).
В вызывающей активности извлекаем результат
kt override fun onActivityResult(
requestCode: Int,
resultCode: Int,
intent: Intent?)
{
super.onActivityResult(requestCode, resultCode, intent)
// проверяем код запроса
when (requestCode)
{
ICON_SELECT -> {
if (resultCode == Activity.RESULT_OK && intent != null)
{
val icoIndex = intent.getIntExtra("icoIndex", -1)
if (icoIndex>=0){
imageView.setImageResource(icoIndex)
}
}
}
}
} ```
Необходимо разработать мобильное приложение для умных часов. В работе необходимо использовать систему контроля версий Git. Для входа используйте учетную запись вида wsX, где X – это номер участника. Необходимо загрузить результаты выполнения модуля в отдельную ветку с именем “Module-X”, где Х – это номер модуля. Для каждого проекта необходим отдельный репозиторий.
Необходимо корректно обрабатывать запросы к серверу. В случае получения ошибки от сервера или отсутствия соединения с сетью Интернет необходимо отобразить соответствующий текст ошибки с помощью диалогового окна.
Необходимо строго следовать предложенному дизайну. Макеты приложения доступны по ссылке:
https://www.figma.com/file/tD64TlCMQEqlr8OTv6bW2o/KOD1.4-Variant3?node-id=0%3A1
Описание протокола API доступно по ссылке: http://swagger.kolei.ru?url=http://cinema.kolei.ru/swagger/cinema.yml
Проект приложения должен быть структурирован по экранам, то есть исходные файлы конкретного экрана должны быть в соответствующей папке. Общие для нескольких экранов классы необходимо поместить в папку
common.Разрешено использовать наработки Модуля 1.
Необходимо реализовать следующий функционал:
Настройте иконку приложения согласно макету. Следует учесть разницу в отображении иконок на различных версиях операционной системы.
| Критерий | Баллы |
|---|---|
| Создан репозиторий для проекта | 0.25 |
| Создана ветка для приложения для часов | 0.35 |
| Проект корректно сохранен в ветку для приложения для часов | 0.45 |
| Создан проект приложения для часов | 0.1 |
| Проект приложения для часов успешно собирается | 0.1 |
Проект приложения для часов имеет корректную структуру (минус 0,1 за каждый файл, содержащийся в корне проекта) | 0.5 Иконка приложения соответствует Конкурсному Заданию | 0.2 Итого | 1.95
Main Screen. При получении ошибки от сервера необходимо отобразить ее с помощью диалогового окна.| Критерий | Баллы |
|---|---|
| Реализован запрос авторизации. Запрос фиксируется сервером | 0.7 |
| Экран соответствует макету (оценивается верстка). Корректно реализованы 4 элемента: логотип, два поля для ввода, кнопка (минус 0,1 за каждый отсутствующий или некорректный элемент) | 0.4 |
| Поля для ввода валидируются на пустоту (минус 0,1 за каждое поле без валидации) | 0.2 |
| При получении ошибки от сервера она отображается с помощью диалогового окна | 0.5 |
| Email проверяется на удовлетворение шаблону из Задания | 0.3 |
| При успешной авторизации открывается экран Main Screen | 0.1 |
| Итого | 2.2 |
При нажатии на иконку должен осуществляться переход на соответствующий экран. Если экран не описан в Задании - нужно отобразить экран с надписью “В разработке”. При нажатии на кнопку Cancel (для Android - при свайпе вправо) нужно возвращаться на SignIn Screen.
| Критерий | Баллы |
|---|---|
| Экран соответствует макету (оценивается верстка). Корректно реализованы 6 элементов: три кнопки, три надписи (минус 0,1 за каждый отсутствующий или некорректный элемент) | 0.6 |
| При нажатии на иконку осуществляется переход на соответствующий экран (минус 0,1 за каждый отсутствующий переход) | 0.3 |
| При нажатии на кнопку Cancel (на Android - при свайпе вправо) осуществляется возврат на SignIn Screen | 0.1 |
| Итого | 1 |
Main Screen.| Критерий | Баллы |
|---|---|
| Реализован запрос списка чатов пользователя. Запрос фиксируется сервером | 0.5 |
| Ячейка таблицы соответствует макету | 0.1 |
| Итого | 0.6 |
| Критерий | Баллы |
|---|---|
| Реализован запрос подборки фильмов. Запрос фиксируется сервером | 0.5 |
Ячейка фильма соответствует макету. Присутствуют 3 элемента: изображение, название, кнопка (минус 0,1 за каждый отсутствующий или некорректный элемент) | 0.3 Список фильмов прокручивается по вертикали | 0.2 Итого | 1
| Критерий | Баллы |
|---|---|
| Создана презентация | 0.1 |
| Презентация содержит описание приложения для смартфона | 0.3 |
| Презентация содержит описание приложения для часов | 0.3 |
| Итого | 0.7 |
| Android TV | Содержание | |