---
title: "11. Android: Работа с камерой, Notifications API"
tags: netology,Kotlin, Android, котлин, андроид, KTX,camera api, intent
---
# 11. Android: Работа с камерой, Notifications API
[TOC]
---
## Вспоминаем прошлые занятия
- Работа с Retrofit
- Intent
- RecycleView
- CRUD
---
---
## Взаимодействие с приложениями
___
### Сделаем приложение более красочным
Наше социальное приложение умеет обращаться с текстовыми постами, ставить лайки и даже репосты. Что же можно туда добавить? Ни одно из социальных приложений не обходится без изображений. Придется и нам последовать этому тренду и реализовать эту фичу. Но как же это сделать? Писать Activity, где мы будем получать доступ к камере? Обрабатывать изображение? Это легко?
Не так страшен черт, как его малюют.
___
### Взаимодействие с другими приложениями в Android
Как мы знаем, в android-е обычно присутствуют несколько Activity, каждое из который, отображает пользователю интерфейс, с которым он взаимодействует. Для перехода между ними мы создаем `intent`, который передаем системе (например через метод `startActivity()`). Система, в свою очередь, используя intent, стартует нужные компоненты приложения. Использование Intent-ов позволят даже запускать Activity из других приложений.
Intent-ы могут быть явными (explicit), чтобы стартовать определенный компонент или неявными (implicit), чтобы стартовать компонент, который может обработать предусмотренное действие (например сделать фото).
___
### Перенаправление пользователя в другое приложение
Одно из наиболее важных свойств Activity - возможность отправить пользователя в другое приложение, в зависимости от действия которое надо совершить.
Например, Если ваше приложение имеет адрес, который можно показать на карте, то вам не надо создавать свое Activity, которое будет показывать карту (что очень затратно и сложно). Вместо этого вы можете создать запрос на просмотр карты с конкретным адресом, используя Intent. Система android впоследствии запустит приложение, которое способно на эти действия.
В отличие от явных Intent-ов, которые используются в собственном приложении, для этих целей используются неявные Intent-ы.
___
### Перенаправление пользователя в другое приложение

---
### Неявные Intent-ы
Неявные intent-ы не объявляют имя класса, которое надо запустить. Вместо этого они объявляют действие, которое необходимо запустить. Действие указывает что вы хотите сделать: посмотреть, редактировать, отправить или получить какую-то информацию. Часто Intent-ы содержать дополнительные данные, которые ассоциированы с действием. Например: адрес, который надо отобразить, почта, на которую надо отправить письмо и т.д.
В зависимости от создаваемого Intent-а ваши данные могут быть в виде Uri (Uniform Resource Identifier), в виде разных типов данных или вообще отсутствовать.
___
### URI
URI — символьная строка, позволяющая идентифицировать какой-либо ресурс: документ, изображение, файл, службу, ящик электронной почты и т. д. Прежде всего, речь идёт о ресурсах сети Интернет и Всемирной паутины. URI предоставляет простой и расширяемый способ идентификации ресурсов. Расширяемость URI означает, что уже существуют несколько схем идентификации внутри URI, и ещё больше будет создано в будущем.

___
### Intent и URI
Есть простой способ определить действие и данные с помощью URI.
Например, ниже показано как создать неявный intent для телефонного звонка, используя Uri данные для передачи номера телефона
```kotlin=
val callIntent: Intent = Uri.parse("tel:5551234").let { number ->
Intent(Intent.ACTION_DIAL, number)
}
```
Когда ваше приложение вызывает этот intent через `startActivity()`, приложение для звонков инициирует звонок на данный номер телефона.
___
### Часто испльзуемые URI
Просмотр карты
```kotlin=
// точка на карте зависит от адреса
val mapIntent: Intent = Uri.parse(
"geo:0,0?q=1600+Amphitheatre+Parkway,+Mountain+View,+California"
).let { location ->
// Или точка на основе долготы или широты
// Uri location = Uri.parse("geo:37.422219,-122.08364?z=14");
// z параметр - масштаб
Intent(Intent.ACTION_VIEW, location)
}
```
Открытие браузера
```kotlin=
val webIntent: Intent = Uri
.parse("http://www.android.com").let { webpage ->
Intent(Intent.ACTION_VIEW, webpage)
}
```
___
### Часто испльзуемые URI
Другие виды неявных intent-ов нуждаются в дополнительных данных. Эти данные можно добавить используя различные `putExtra...` методы
```kotlin=
Intent(Intent.ACTION_SEND).apply {
// Intent не имеет URI, поэтому объявим MIME тип "text/plain"
type = HTTP.PLAIN_TEXT_TYPE
putExtra(Intent.EXTRA_EMAIL, arrayOf("jon@example.com")) // получатели
putExtra(Intent.EXTRA_SUBJECT, "заголовок письма")
putExtra(Intent.EXTRA_TEXT, "письмо")
putExtra(Intent.EXTRA_STREAM,
Uri.parse("content://path/to/email/attachment"))
}
```
___
### Проверка неявного Intent-а
Когда мы создали Intent с данными и вызвали `startActivity()` то система смотрит есть ли приложение, которое может обработать его. Если таких приложений больше одного, то система показывает диалог, где пользователь решает с помощью какого приложения обрабатывать запрос.
Если Activity в единственном роде, то система сразу же его запускает

Диалог выбора приложения
---
### Проверка неявного Intent-а
Как вы поняли приложения, которое обрабатывает неявный Intent может и не быть. Для этого надо дополнительно проверить Intent перед запуском в свободное плавание.
```kotlin=
// Строим intent
val location = Uri
.parse("geo:0,0?q=1600+Amphitheatre+Parkway,+Mountain+View,+California")
val mapIntent = Intent(Intent.ACTION_VIEW, location)
// Проверяем может ли кто-то обработать наш intent
val activities: List<ResolveInfo> =
packageManager.queryIntentActivities(mapIntent, 0)
val isIntentSafe: Boolean = activities.isNotEmpty()
// Если кто-то готов нас принять, то отправляем intent системе.
if (isIntentSafe) {
startActivity(mapIntent)
}
```
---
### Получение результата в Activity
Старт другого Activity не должно быть односторонним. Чтобы получить какие-то данные в ответ вы можете воспользоваться методом `startActivityForResult()`
Конечно, Activity, которое мы стартуем должно быть спроектировано таким образом, чтобы возвращать результат. Когда она это сделает, то она отправляет другой Intent объект с результатом. Ваше activity получает этот результат в callback-е `onActivityResult()`
___
### Получение результата в Activity
В ``startActivityForResult()`` используется такой же intent, как и в `startActivity()`, но вам надо передать `int` значение, которое вернется обратно вместе с результатом
```kotlin=
const val PICK_CONTACT_REQUEST = 1 // The request code
...
private fun pickContact() {
...
startActivityForResult(pickContactIntent, PICK_CONTACT_REQUEST)
}
}
```
Когда работа сделана и возвращается результат, то система вызывает `onActivityResult()` в нашем Activity. Этот метод принимает 3 аргумента:
1. Код, который передавался в `startActivityForResult()`;
2. Код результата, который нам передала вызванная Activity; Это или `RESULT_OK`, если все прошло успешно, или `RESULT_CANCELED`, если что-то пошло не так;
3. Intent который несет в себе данные.
---
## Работа с камерой (с фото)
___
## Делаем фото
Теперь пришло время познакомиться непосредственно с фото и взамодействием с ним. А именно как сделать фото с помощью делегирования этого действия другому приложение, которое умеет обращаться с камерой
___
### Делаем фото с помощью приложения камеры
Как вы догадались, для того чтобы получить фото, нам нужно взаимодействовать с приложением камеры через intent.
Этот процесс включает в себя 3 шага:
1. Создание нужного Intent-a;
2. Старт нужного Activity;
3. Код, который обработает результат.
Запрос на фото может выглядеть так:
```kotlin=
val REQUEST_IMAGE_CAPTURE = 1
private fun dispatchTakePictureIntent() {
Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
takePictureIntent.resolveActivity(packageManager)?.also {
startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE)
}
}
}
```
___
### Получение изображения
Приложение камеры Android возвращает фотографию в Intent-е в колбек `onActivityResult()` в виде `Bitmap`, который доступен по ключу "data"
Следующий код получает изображение и отображает его в `ImageView`
```kotlin=
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
val imageBitmap = data.extras.get("data") as Bitmap
imageView.setImageBitmap(imageBitmap)
}
}
```
___
## Glide
GLide это быстрый и эффективный фреймворк для управления медиа и загрузки изображений, который инкапсулирует в себе кеширование, декодирования и предоставляет пользователю удобный интерфейс.
Для добавление Glide используются следующие зависимости:
```groovy=
dependencies {
implementation 'com.github.bumptech.glide:glide:4.10.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.10.0'
}
```
___
### Использование Glide
В простых случаях Glide можно использовать следующим образом:
```java=
// Для одного вью
@Override public void onCreate(Bundle savedInstanceState) {
...
ImageView imageView = (ImageView) findViewById(R.id.my_image_view);
Glide.with(this).load("http://goo.gl/gEgYUd").into(imageView);
}
// в адаптерах
@Override public View getView(int position, View recVw, ViewGroup container) {
...
String url = myUrls.get(position);
Glide
.with(context)
.load(url)
.centerCrop()
.placeholder(R.drawable.loading_spinner)
.into(myImageView);
return myImageView;
}
```
___
## Функционал фото в приложении
Итак, давайте реализуем в нашем приложение загрузку фото. Для этого, как всегда нам понадобится добавить API, код и модифицировать UI. В адаптере добавим загрузку фото с помощью Glide.
___
### Функционал фото (API)
Загрузка больших бинарных данных в retrofit-е осуществляется с помощью аннотации `@Multipart` и `@Part`.
Обратно нам возвращается модель `AttachmentModel`
```kotlin=
// API.kt
...
@Multipart
@POST("api/v1/media")
suspend fun uploadImage(@Part file: MultipartBody.Part):
Response<AttachmentModel>
...
```
```kotlin=
data class AttachmentModel(val id: String, val mediaType: AttachmentType) {
val url
get() = "$BASE_URL/api/v1/static/$id"
}
```
### Функционал фото (API)
Этот API будем вызывать, как обычно, через репозиторий. В репозиторий мы передаем `Bitmap`, который нам будет возвращать приложение камеры. Задача репозитория преобразовать Bitmap в формат, понятный для API
```kotlin=
// Repository.kt
...
suspend fun upload(bitmap: Bitmap): Response<AttachmentModel> {
// Создаем поток байтов
val bos = ByteArrayOutputStream()
// Помещаем Bitmap в качестве JPEG в этот поток
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos)
val reqFIle =
// Создаем тип медиа и передаем массив байтов с потока
RequestBody.create(MediaType.parse("image/jpeg"), bos.toByteArray())
val body =
// Создаем multipart объект, где указываем поле, в котором
// содержатся посылаемые данные, имя файла и медиафайл
MultipartBody.Part.createFormData("file", "image.jpg", reqFIle)
return API.uploadImage(body)
}
```
___
### Функционал фото (UI)
Теперь В `CreatePostActivity` добавляем иконки, при нажатии на которые будем запускать тот или иной intent для взаимодействия с другими приложениями.
Выбирать можно только один из медиафайлов, поэтому при выборе и загрузке одного медиа отключаем возможность дальнейшей загрузки для других.
| До выбора медиа | После выбора фото
| -------- | -------- |
|  | |
___
### Функционал фото (код)
Теперь остается при нажатии на иконку изображения запустить intent для работы с приложением камеры и ждать результата в `onActivityResult()`. При успешном результате показываем крутилку, загружаем изображение и получаем результат загрузки. Если результат успешный то запоминаем `attchmentModel`, чтобы передать его при создании поста.
___
### Функционал фото (код)
```kotlin=
// CreatePostAcivity.kt
...
val REQUEST_IMAGE_CAPTURE = 1
private var dialog: ProgressDialog? = null
private var attachmentModel: AttachmentModel? = null
...
attachPhotoImg.setOnClickListener {
dispatchTakePictureIntent()
}
createPostBtn.setOnClickListener {
...
val result =
Repository.createPost(contentEdt.text.toString(), attachmentModel)
...
override fun onActivityResult(requestCode: Int,
resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == RESULT_OK) {
val imageBitmap = data?.extras?.get("data") as Bitmap?
imageBitmap?.let {
launch {
dialog = createProgressDialog()
val imageUploadResult = Repository.upload(it)
dialog?.dismiss()
if (imageUploadResult.isSuccessful) {
imageUploaded()
attachmentModel = imageUploadResult.body()
} else {
toast("Can't upload image")
...
```
___
### Функционал фото (код)
```kotlin=
// CreatePostActivity.kt
private fun imageUploaded() {
transparetAllIcons()
// Показываем красную галочку над фото
attachPhotoDoneImg.visibility = View.VISIBLE
}
// Устанавливаем все иконки в полупрозрачный серый цвет
private fun transparetAllIcons() {
attachPhotoImg.setImageResource(R.drawable.ic_add_a_photo_inactive)
attachAudioImg.setImageResource(R.drawable.ic_add_an_audio_inactive)
attachVideoImg.setImageResource(R.drawable.ic_add_a_video_inactive)
}
// Отправка intent-а для приложения камеры
private fun dispatchTakePictureIntent() {
Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
takePictureIntent.resolveActivity(packageManager)?.also {
startActivityForResult(takePictureIntent, REQUEST_IMAGE_CAPTURE)
}
...
```
___
### Функционал фото (адаптер)
В адаптере, ниже контента надо создать view, в котором, в зависимости от типа медиа, будут отображаться либо изображения, либо аудио, либо видео. Для этого будет достаточно `FragmentLayout-a`. Так же для изображения желательно указать максимальный размер `app:layout_constraintHeight_max="xxxdp"` и установить аттрибут `android:adjustViewBounds="true"`
```xml=
//item_post.xml
...
<FrameLayout
android:id="@+id/mediaContainer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="16dp"
app:layout_constraintBottom_toTopOf="@id/likeBtn"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/contentTv">
<ImageView
android:id="@+id/photoImg
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
tools:src="@drawable/example"
app:layout_constraintHeight_max="400dp" />
</FrameLayout>
```
___
### Функционал фото (адаптер)
При биндинге в `ViewHolder`-е проверяем, есть ли `attachmentModel` и если есть, то какого он типа. Дальше предоставляем работу Glide
```kotlin=
// PostAdapter.kt
when (post.attachment?.mediaType) {
AttachmentType.IMAGE -> loadImage(photoImg, post.attachment.url) }
...
private fun loadImage(photoImg: ImageView, imageUrl: String) {
Glide.with(photoImg.context)
.load(imageUrl)
.into(photoImg)
}
```
___
### Функционал фото (результат)

___
## Нотификации
___
## Нотификации
Нотификация - это сообщение, которое показывает система Android вне вашего приложения. Пользователь может нажать на нотификацию, что бы открыть ваше приложение или совершить какое-то действие непосредственно с нотификацией.
Нотификации могут появляться в разных частях и в разных форматах. Например, иконка в статус баре, более детальная нотификация, нотификация в виде маленького значка над иконкой приложения и т.д.

*Иконка нотификации в статус баре.*
___
## Нотификации
Пользователи могут потянуть за статус бар, что бы раскрыть нотификацию, где можно увидеть более детальную информацию.
Пользователи могут потянуть за нотификацию, что бы можно было увидеть дополнительные элементы view, например кнопки.

___
### Heads-up нотификации
Начиная с Android 5.0 нотификации могут ненадолго появляться в плавающем окне. Их называют heads-up нотификация. Это поведения является нормальным для важных нотификаций о которых пользователь должен быть оповещен немедленно. При этом устройство должно быть разблокировано.

___
### Анатомия нотификации
Нотификация имеет следующую структуру:

1. Маленькая иконка. Это обязательно и устанавливается с помощью `setSmallIcon()`
2. Название приложения, которое предоставляет система
3. Время, которое предоставляет система. Его можно переопределить с помощью `setWhen()` или спрятать с помощью `setShowWhen(false)`.
4. Большая иконка. Это опциональный параметр. Устанавливается через `setLargeIcon()`
5. Заголовок. Является опциональным и устанавливается через `setContentTitle()`
6. Тест. Является опциональным и устанавливается через `setContentText()`
___
### Каналы нотификаций
Начиная с Android 8.0 все нотификации должны быть присоединены к каналам, иначе они не появятся. Благодаря группировке ваших нотификаций с помощью каналов, пользователь может отключить тот или иной канал(вместо отключения всех нотификаций приложения), а также контролировать визуальное и аудиальное сопровождение каждого канала

### Создание простой нотификации
Итак, что бы создать простую нотификацию надо создать канал с помощью `NotificationCompat.Builder` и передать какой-либо контент. Также мы можем указать приоритет нотификации через `setPriority()`. Это будет работать на Andorid 7.1 и ниже. На Android-е 8.0 и выше вы должны использовать приоритеты канала.
```kotlin=
var builder = NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.notification_icon)
.setContentTitle(textTitle)
.setContentText(textContent)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
```
___
### Создание простой нотификации
По умолчанию, текст нотификации обрезается, чтобы поместиться в одну линию. Если вы хотите, чтобы нотификация была больше, вы можете добавить шаблон стиля через `setStyle()`. Например, следующий код создает нотификацию с большим текстом
```kotlin=
var builder = NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.notification_icon)
.setContentTitle("My notification")
.setContentText("Much longer text that cannot fit one line...")
.setStyle(NotificationCompat.BigTextStyle()
.bigText("Much longer text that cannot fit one line..."))
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
```
### Создание каналов и установка приоритета
Как мы помним, для отображения нотификаций на Android 8.0 и выше нам понадобятся каналы. Каналы должны быть зарегистрированы в системе. Сделать это можно следующим образом:
```kotlin=
private fun createNotificationChannel() {
// Создает канал только на API 26+, потому ччто
// класс NotificationChannel новый и недоступен на предыдущих версиях
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val name = getString(R.string.channel_name)
val descriptionText = getString(R.string.channel_description)
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel(CHANNEL_ID, name, importance).apply {
description = descriptionText
}
// Зарегистрировать канал
val notificationManager: NotificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
```
Этот код нужно выполнять как можно раньше. Повторное выполнение кода приемлемо, т.к. если канал уже зарегистрирован, то ничего не произойдет.
___
### Действия при нажатии на нотификацию
Каждая нотификация должна что-то делать при нажатии на нее. Обычно при этом открывается Activity, которая связана с нотификацией. Что бы это сделать, вы должны указать PendingIntent и передать в него необходимый компонент для запуска.
```kotlin=
// Создание явного intent-а для Activity
val intent = Intent(this, FeedActivity::class.java)
val pendingIntent: PendingIntent =
PendingIntent.getActivity(this, 0, intent, 0)
val builder = NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.notification_icon)
.setContentTitle("Вам поставили лайк")
.setContentText("Ура!!!!!!!!!!!!!!")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
// Передаем интент, который запустит pendingIntent
.setContentIntent(pendingIntent)
// Интент удалиться при нажатии
.setAutoCancel(true)
```
___
### Показываем нотификацию
Самое последнее, что нам остается сделать после кропотливой работы - это показать нотификацию. Сделать это довольно просто. Для этого достаточно вызвать `NotificationManagerCompat.notify()` передавая в качестве параметра уникальный ID нотификации и результат билдера `NotificationCompat.Builder.build()`
```kotlin=
with(NotificationManagerCompat.from(this)) {
// notificationId уникальное значение для каждой нотификации,
// которое вы должны указать
notify(notificationId, builder.build())
}
```
---
### Обновление нотификации
Чтобы обновить нотификацию после того, как она была показана надо вызывать `NotificationManagerCompat.notify()` снова с тем же id, что и прежде. Если предыдущая нотификация была удалена, то появится новая нотификация.
*Андроид накладывает лимит на количество обновлений нотификаций. Если вы обновляете их слишком часто (много раз за секунду), то система может откинуть ваши обновления*
___
### Удаление нотификации
Нотификации остаются видимыми пока не случится одно из следующих условий:
* Пользователь удалит нотификацию;
* Пользователь нажмет на нотификацию;
* Вы вызвали метод `cancell()` для id отображаемой нотификации.
* Вы вызвали метод `cancelAll()` который удаляет все нотификации, показанные прежде вами;
* Если был установлен таймер на нотификацию через `setTimeoutAfter()` и он истек.
___
## Функционал нотификаций в приложении
Давайте добавим в наше приложение нотификацию. А именно оповестим пользователя о том, что меда загрузилось. Для этого создадим вспомогательный класс, который будет внутри себя работать с Notifications API.
При создании нотификаций будем проверять создан ли канал "media uploading". Если нет, то создаем.
Добавим в него метод для оповещения пользователя.
```kotlin=
fun mediaUploaded(type: AttachmentType, context: Context)
```
___
### Функционал нотификаций в приложении (код)
```kotlin=
object NotifictionHelper {
private val UPLOAD_CHANEL_ID = "upload_chanel_id"
private var channelCreated = false
private var lastNotificationId: Int? = null
// Создание канала
private fun createNotificationChannel(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val name = "Media uploading"
val descriptionText = "Notifies when media upload during post creation"
val importance = NotificationManager.IMPORTANCE_HIGH
val channel = NotificationChannel(UPLOAD_CHANEL_ID, name, importance)
.apply {
description = descriptionText
}
// Регистрация канала в системе
val notificationManager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE)
as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
...
```
___
### Функционал нотификаций в приложении (код)
```kotlin=
...
fun mediaUploaded(type: AttachmentType, context: Context) {
// Создаем канал, если он не создан
createNotificationChannelIfNotCreated(context)
val builder = NotificationCompat.Builder(context, UPLOAD_CHANEL_ID)
.setSmallIcon(R.mipmap.ic_launcher_round)
.setContentTitle("Media uploaded")
.setContentText("your ${type.name.toLowerCase()} successfully uploaded.")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
// Устанавливаем приоритет
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
builder.setPriority(NotificationManager.IMPORTANCE_HIGH)
}
with(NotificationManagerCompat.from(context)) {
val notificationId = Random().nextInt(100000)
lastNotificationId = notificationId
notify(notificationId, builder.build())
}
}
private fun createNotificationChannelIfNotCreated(context: Context) {
if (!channelCreated) {
createNotificationChannel(context)
channelCreated = true
}
}
}
```
___
### Функционал нотификаций в приложении (код)
```kotlin=
// CreatePostActivity.kt
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?
) {
...
dialog = createProgressDialog()
val imageUploadResult = Repository.upload(it)
// Оповещаем пользователя
NotifictionHelper
.mediaUploaded(AttachmentType.IMAGE, this@CreatePostActivity)
dialog?.dismiss()
...
```
___
### Функционал нотификаций в приложении (результат)

___
## Итоги
---
### Итоги
В этой лекции мы познакомились с взаимодействиями с другими приложение. Узнали как работать с камерой, как загружать большие данные, а именно: изображения, аудио, видео на сервер. Рассмотрели нотификации с точки зрения разработчика и реализовали в приложении простую нотификацию.
В следующий лекции мы более подробно поработаем с нотификациями и пушами.