При создании классов по сравнению с Java произошли несколько значительных изменений и упрощений. Kotlin предлагает более лаконичный и выразительный синтаксис, что делает код более читаемым и удобным.
В Kotlin объявление классов и их конструкторов значительно упрощено.
В Java
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}В Kotlin
class Person(val name: String, val age: Int)
В Java для объявления статических членов используется ключевое слово
static. В Kotlin вместо этого используются companion object.В Java
public class MyClass {
public static final String CONSTANT = "constant";
public static void staticMethod() {
// Some code
}
}В Kotlin
class MyClass {
companion object {
const val CONSTANT = "constant"
@JvmStatic
fun staticMethod() {
// Some code
}
}
}Kotlin предоставляет специальный тип классов —
data классы, которые автоматически генерируют методы equals(), hashCode(), toString(), copy(), и componentN().В Java
public class User {
private String name;
private int age;
public User(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public boolean equals(Object o) {
// Implementation
}
@Override
public int hashCode() {
// Implementation
}
@Override
public String toString() {
// Implementation
}
}В Kotlin
data class User(val name: String, val age: Int)
В Kotlin свойства объявляются напрямую, и методы доступа (геттеры и сеттеры) генерируются автоматически.
В Java
public class Rectangle {
private int width;
private int height;
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
}
}В Kotlin
class Rectangle(var width: Int, var height: Int)
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
В Hilt ViewModel помечается аннотацией
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥2
Модификации могут означать разные вещи в зависимости от контекста. В Android-разработке чаще всего речь идёт о следующих типах модификаций:
Модификация данных (изменение состояния)
Модификация UI (Compose Modifiers)
Модификация кода (рефакторинг, оптимизация)
Модификация системы (кастомные прошивки, рут-изменения)
Это изменение переменных, объектов и структур данных во время работы приложения.
class ViewModel : ViewModel() {
private val _state = MutableStateFlow("Привет, мир!")
val state: StateFlow<String> = _state
fun updateText(newText: String) {
_state.value = newText // Модификация состояния
}
}В Jetpack Compose модификации (
Modifier) используются для изменения внешнего вида и поведения UI-элементов. Text(
text = "Привет!",
modifier = Modifier
.padding(16.dp) // Добавляем отступ
.background(Color.Blue) // Меняем фон
.clickable { println("Нажали!") } // Добавляем клик
)
В программировании модификация кода означает его улучшение без изменения основной логики.
fun sum(a: Int, b: Int): Int {
return a + b
}После рефакторинга
fun sum(a: Int, b: Int) = a + b
Это изменения в самой операционной системе Android, например:
- Рут-доступ (
root), позволяющий изменять системные файлы. - Кастомные прошивки (например, LineageOS).
- Изменение build.prop для скрытия рут-доступа.
ro.build.version.sdk=33
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🤔4👍1
Ссылочные типы в Kotlin:
- неизменяемые, если это требуется по архитектуре;
- nullable или not-null — в зависимости от требований;
- должны реализовывать equals, hashCode и toString, особенно если используются в коллекциях;
- быть легковесными, если часто копируются;
- желательно — final или sealed, если поведение фиксировано.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥1
dp означает "density-independent pixels" или "пиксели, не зависящие от плотности". Это виртуальная единица измерения, которая используется для обеспечения одинакового визуального размера элементов пользовательского интерфейса на экранах с различной плотностью пикселей. Это важно, поскольку устройства Android могут иметь различные размеры и разрешения экранов, и использование dp помогает создать консистентный пользовательский интерфейс на всех этих устройствах.
Базовая логика такова, что 1 dp равен одному пикселю на экране с плотностью 160 точек на дюйм (dpi). Это плотность соответствует "среднему" уровню, который называется "mdpi" в терминах Android. На таком экране:
1 dp = 1 px
На устройствах с более высокой или ниже плотностью экрана dp масштабируются соответственно:
На устройстве с плотностью 320 dpi (high density, hdpi), 1 dp = 2 px.
На устройстве с плотностью 240 dpi (high density, hdpi), 1 dp = 1.5 px.
И так далее.
Обеспечить, чтобы элементы интерфейса (как размеры, так и отступы) выглядели одинаково на разных устройствах, несмотря на физические различия в размерах экранов и их разрешениях. Это позволяет создавать приложения, которые хорошо выглядят и функционируют на множестве устройств без необходимости перепроектирования интерфейса под каждое конкретное устройство.
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Click me!"
android:padding="16dp" />
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2💊1
– Хранить выбранную тему в SharedPreferences,
– При старте приложения (или Activity) применять тему до setContentView,
– В случае использования DayNight — можно использовать AppCompatDelegate.setDefaultNightMode().
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3🔥2🤔1
Jetpack Compose – это декларативный UI-фреймворк от Google для создания интерфейсов в Android. Вместо традиционных
XML + View в Compose используется функции-компоненты, которые описывают UI. Декларативный подход – UI создаётся через функции, без XML.
еактивность – UI автоматически обновляется, если данные изменились.
Компонентный подход – UI состоит из маленьких, переиспользуемых функций.
Composable-функции (
@Composable) – основной строительный блок Compose. Вместо
Activity и Fragment с findViewById() используется @Composable-функции. @Composable
fun Greeting(name: String) {
Text(text = "Привет, $name!")
}
@Composable
fun MyScreen() {
Column {
Greeting("Андрей")
Greeting("Мария")
}
}
@Preview
@Composable
fun PreviewMyScreen() {
MyScreen()
}
Jetpack Compose использует реактивный подход, где UI обновляется при изменении состояния.
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) } // Реактивное состояние
Column {
Text("Счётчик: $count")
Button(onClick = { count++ }) {
Text("Увеличить")
}
}
}
Текст
Text(text = "Привет, мир!", fontSize = 20.sp)
Кнопка
Button(onClick = { /* действие */ }) {
Text("Нажми меня")
}Ввод текста (
TextField) var text by remember { mutableStateOf("") }
TextField(value = text, onValueChange = { text = it })Размещение элементов (вертикально/горизонтально)
Column {
Text("Первый элемент")
Text("Второй элемент")
}Row {
Text("Слева")
Text("Справа")
}Compose работает с MVVM через
ViewModel и LiveData / StateFlow. class MyViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
val count: StateFlow<Int> = _count
fun increment() {
_count.value++
}
}@Composable
fun CounterScreen(viewModel: MyViewModel = viewModel()) {
val count by viewModel.count.collectAsState()
Column {
Text("Счётчик: $count")
Button(onClick = { viewModel.increment() }) {
Text("Добавить")
}
}
}
Вместо
Fragment используется NavController. @Composable
fun MyApp() {
val navController = rememberNavController()
NavHost(navController, startDestination = "home") {
composable("home") { HomeScreen(navController) }
composable("details") { DetailsScreen() }
}
}
Теперь можно перейти на другой экран так
navController.navigate("details")Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
Bundle — это контейнер для передачи данных между компонентами Android, такими как Activity, Fragment или Service. Он хранит пары ключ-значение и может сохраняться при пересоздании компонента.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥3
Термин "Extensions" (расширения) используется для обозначения функциональности, которая позволяет добавлять новые возможности к существующим классам без изменения их исходного кода. Этот концепт особенно популярен к примеру в Swift и Kotlin, где расширения используются для улучшения читаемости кода, упрощения его структуры и добавления удобных методов для работы с уже существующими типами данных.
Расширения позволяют добавить новую функциональность к классам, даже если исходный код этих классов недоступен для изменений (например, классы из стандартной библиотеки или сторонних библиотек). Это делается путём объявления функций или свойств, которые могут быть вызваны на экземплярах этих классов, как если бы они были частью оригинальных классов.
// Расширение класса String для проверки, пуста ли строка или состоит из пробелов
fun String.isNullOrBlank(): Boolean {
return this == null || this.trim().isEmpty()
}
// Использование расширения
val myString = " "
println(myString.isNullOrBlank()) // Выведет: true
Использует очень похожий подход, позволяя добавлять новые методы и вычисляемые свойства к существующим типам. Расширения могут даже добавлять новые протоколы к типам, что делает их чрезвычайно мощным инструментом в разработке под iOS и macOS.
// Расширение стандартного типа String для добавления метода инвертирования строки
extension String {
func reversedString() -> String {
return String(self.reversed())
}
}
let myString = "Hello"
print(myString.reversedString()) // Выведет: "olleH"
Расширения позволяют держать методы, связанные с определёнными типами данных, ближе к использованию этих типов.
Расширения предоставляют альтернативный способ добавления функциональности к классам без использования наследования, что помогает избежать избыточной иерархии и увеличения сложности системы.
Методы, добавленные через расширения, могут быть легко переиспользованы в различных частях приложения.
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
Если объект-ключ не переопределяет equals() и hashCode(), то HashMap.get() не сможет найти значение по эквивалентному, но не тому же экземпляру, и вернёт null.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥3
data class — класс для хранения данных с автогенерацией equals(), hashCode(), copy(). sealed class — ограниченная иерархия классов, используется для when. data class автоматически создаёт: equals() и hashCode() → сравнение объектов по значениям. copy() → удобное копирование с изменением параметров. toString() → красивый вывод. data class User(val id: Int, val name: String)
fun main() {
val user1 = User(1, "Alice")
val user2 = user1.copy(name = "Bob") // Создаём копию с новым именем
println(user1) // User(id=1, name=Alice)
println(user2) // User(id=1, name=Bob)
}
sealed class используется, когда есть фиксированное число подклассов. sealed class NetworkState {
object Loading : NetworkState()
data class Success(val data: String) : NetworkState()
data class Error(val message: String) : NetworkState()
}
fun handleState(state: NetworkState) {
when (state) {
is NetworkState.Loading -> println("Загрузка...")
is NetworkState.Success -> println("Данные: ${state.data}")
is NetworkState.Error -> println("Ошибка: ${state.message}")
}
}Да! Это лучший вариант для управления состояниями:
sealed class Result {
object Loading : Result()
data class Success(val data: String) : Result()
data class Error(val message: String) : Result()
}Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1
2. Он срабатывает до выполнения конструктора.
3. В init нельзя обращаться к поздноинициализированным (lateinit) свойствам, так как они могут ещё не быть доступны.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥3💊2
В Android существует несколько способов запуска асинхронных операций. Наиболее распространённые из них включают использование
AsyncTask (хотя он уже устарел), HandlerThread, AsyncTaskLoader, ExecutorService и современные подходы с использованием Kotlin Coroutines и библиотеки WorkManager.AsyncTask был одним из первых инструментов для выполнения асинхронных задач, однако его использование сейчас не рекомендуется из-за проблем с управлением жизненным циклом и утечками памяти.private class MyAsyncTask extends AsyncTask<Void, Void, String> {
@Override
protected String doInBackground(Void... voids) {
// Выполнение длительной операции
return "Result";
}
@Override
protected void onPostExecute(String result) {
// Обновление UI после завершения операции
}
}
// Запуск
new MyAsyncTask().execute();HandlerThread — это удобный способ создания фонового потока с циклом обработки сообщений.HandlerThread handlerThread = new HandlerThread("MyHandlerThread");
handlerThread.start();
Handler handler = new Handler(handlerThread.getLooper());
handler.post(new Runnable() {
@Override
public void run() {
// Выполнение длительной операции
}
});ExecutorService из стандартной библиотеки Java предоставляет более гибкий способ управления потоками.ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(new Runnable() {
@Override
public void run() {
// Выполнение длительной операции
}
});
Корутины в Kotlin — это современный и мощный инструмент для асинхронного программирования. Они упрощают работу с асинхронными задачами и обеспечивают безопасность работы с UI.
import kotlinx.coroutines.*
fun performAsyncTask() {
GlobalScope.launch(Dispatchers.IO) {
// Выполнение длительной операции
val result = longRunningTask()
withContext(Dispatchers.Main) {
// Обновление UI после завершения операции
}
}
}
suspend fun longRunningTask(): String {
delay(1000) // Симуляция длительной операции
return "Result"
}
WorkManager — это библиотека для выполнения фоновых задач, которая обеспечивает выполнение задач даже после закрытия приложения или перезагрузки устройства.public class MyWorker extends Worker {
public MyWorker(@NonNull Context context, @NonNull WorkerParameters params) {
super(context, params);
}
@NonNull
@Override
public Result doWork() {
// Выполнение длительной операции
return Result.success();
}
}
// Запуск Worker
WorkManager workManager = WorkManager.getInstance(context);
OneTimeWorkRequest request = new OneTimeWorkRequest.Builder(MyWorker.class).build();
workManager.enqueue(request);RxJava — это библиотека для реактивного программирования, которая также может быть использована для выполнения асинхронных задач.
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.core.Observable;
import io.reactivex.rxjava3.schedulers.Schedulers;
Observable.fromCallable(() -> {
// Выполнение длительной операции
return "Result";
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(result -> {
// Обновление UI после завершения операции
});
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3🔥1
Типы object:
- object declaration — синглтон, один экземпляр на приложение.
- companion object — вложенный синглтон, связан с классом.
- object expression — анонимный объект (вспомогательные конструкции).
Компилируются как обычные классы с единственным экземпляром и скрытым статическим полем.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥3
В Kotlin можно добавлять свойства-расширения (
extension properties), но только с кастомным get (геттером). Расширяемые свойства могут быть только вычисляемыми (
val), потому что нельзя создать field внутри расширения. val String.firstChar: Char
get() = this[0]
fun main() {
println("Kotlin".firstChar) // K
}
Для
var нужно и get(), и set(), но всё равно нельзя использовать field. var StringBuilder.lastChar: Char
get() = this[length - 1]
set(value) {
this.setCharAt(length - 1, value)
}
fun main() {
val sb = StringBuilder("Hello")
println(sb.lastChar) // o
sb.lastChar = '!'
println(sb) // Hell!
}
Такой код НЕ скомпилируется!
var String.someProperty: String = "Default" // Ошибка!
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
Все классы в Java неявно наследуются от класса Object, который находится в пакете java.lang.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥1
Ключевое слово
object в Kotlin имеет несколько важных применений, и оно является одной из наиболее мощных и уникальных возможностей языка. Kotlin предоставляет встроенную поддержку для создания singleton-объектов. Это объект, который имеет единственный экземпляр в приложении.
object Database {
val name = "MainDatabase"
fun connect() {
println("Подключение к базе данных $name")
}
}
fun main() {
Database.connect() // Подключение к базе данных MainDatabase
}Ключевое слово
object может использоваться для создания анонимных объектов (объектов без имени). Это полезно, если нужно создать временный объект или реализовать интерфейс/абстрактный класс.fun main() {
val listener = object : ClickListener {
override fun onClick() {
println("Кнопка нажата")
}
}
listener.onClick()
}
interface ClickListener {
fun onClick()
}Ключевое слово
object можно использовать внутри класса для объявления компаньон-объекта. Это позволяет создавать статические методы и переменные в классе.class User(val name: String) {
companion object {
fun createGuest() = User("Guest")
}
}
fun main() {
val guest = User.createGuest()
println(guest.name) // Guest
}Ключевое слово
object может использоваться для объявления объектов в коде прямо "на месте".val myObject = object {
val x = 10
fun printX() {
println(x)
}
}
fun main() {
myObject.printX() // 10
}Вместо написания множества шаблонного кода для Singleton, анонимных объектов или статических методов, вы получаете готовую реализацию из коробки.
В случае
object, компилятор Kotlin автоматически гарантирует, что объект создаётся в потокобезопасном режиме.object можно использовать практически везде: глобально, локально, внутри классов и функций.Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
- Bubble API (мессенджеры).
- Жесткое ограничение на фоновые доступы к геолокации.
- One-time permissions.
- Уточнение доступа к клипборду.
- Запрет на несанкционированный доступ к камере/микрофону в фоне.
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥5💊1
Map – это коллекция пар ключ-значение. Каждый ключ уникален, а значения могут повторяться. В Kotlin Map не наследуется от Collection, но является частью стандартных коллекций.В Kotlin есть два основных типа
Map: Map (неизменяемая) – нельзя добавлять/удалять элементы после создания. MutableMap (изменяемая) – можно добавлять, удалять и изменять элементы. Создаётся с помощью
mapOf()val users = mapOf(1 to "Alice", 2 to "Bob", 3 to "Charlie")
println(users[1]) // Выведет: Alice
Создаётся с помощью
mutableMapOf()val users = mutableMapOf(1 to "Alice", 2 to "Bob")
users[3] = "Charlie" // Добавляем элемент
users[1] = "Alex" // Изменяем значение по ключу
users.remove(2) // Удаляем элемент
println(users) // {1=Alex, 3=Charlie}
map[key] – получить значение (или null, если ключа нет). map.getValue(key) – получить значение (или исключение, если ключа нет). map.getOrDefault(key, defaultValue) – вернуть значение или defaultValue, если ключа нет. map.getOrElse(key) { default } – если ключа нет, выполнить лямбда-выражение. val users = mapOf(1 to "Alice", 2 to "Bob")
println(users[1]) // Alice
println(users.getOrDefault(3, "Unknown")) // Unknown
map.containsKey(key) – есть ли ключ? map.containsValue(value) – есть ли значение? println(users.containsKey(2)) // true
println(users.containsValue("Charlie")) // false
Перебор элементов
for ((key, value) in users) {
println("ID: $key, Name: $value")
}Фильтрация
Mapval filtered = users.filter { (key, value) -> key % 2 == 0 }
println(filtered) // {2=Bob}mapOf() – не гарантирует порядок. linkedMapOf() – сохраняет порядок добавления. Обычный
HashMap (mutableMapOf()) – O(1) для поиска по ключу. TreeMap – O(log n), но поддерживает сортировку по ключам. val sortedUsers = sortedMapOf(3 to "Charlie", 1 to "Alice", 2 to "Bob")
println(sortedUsers) // {1=Alice, 2=Bob, 3=Charlie}
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4🔥1
2. Байткод преобразуется в DEX-файл (classes.dex).
3. Собираются ресурсы (aapt, aapt2).
4. Всё упаковывается в .apk.
5. Приложение подписывается ключом (debug/release).
Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥6👍2
Различия между Android 7 (Nougat) и Android 8 (Oreo) заключаются в добавленных функциях, изменениях в производительности, безопасности, работе приложений и пользовательском интерфейсе.
Android 7 (Nougat):
Впервые представлена технология Doze on the Go, которая снижает энергопотребление приложений, когда устройство неактивно.
Улучшена многозадачность благодаря Split-Screen Mode (режим разделения экрана).
Android 8 (Oreo):
Введена система Vitals, которая отслеживает поведение приложений и оптимизирует их производительность, снижая потребление батареи и ресурсов.
Фоновое ограничение (Background Limits): приложения, работающие в фоновом режиме, имеют жёсткие ограничения на использование ресурсов (например, CPU и батареи).
Android 7 (Nougat):
Режим разделения экрана (Split-Screen): позволяет одновременно запускать два приложения. Быстрая смена между последними приложениями через двойное нажатие кнопки недавних задач.
Android 8 (Oreo):
Картинка в картинке (Picture-in-Picture): возможность сворачивать видео или навигационные приложения в маленькое окно, которое остается поверх других приложений.
Уведомления стали более функциональными: появились каналы уведомлений (Notification Channels), позволяющие группировать и настраивать уведомления от приложений.
Уведомления с задержкой: появилось возможность откладывать уведомления на некоторое время (Snooze).
Android 7 (Nougat):
Улучшения в системе ART (Android Runtime) позволили ускорить запуск приложений по сравнению с предыдущими версиями.
Android 8 (Oreo):
Добавлена функция Instant Apps: позволяет использовать приложения без их предварительной установки.
Приложения запускаются быстрее благодаря оптимизациям ART и сокращению кода, который нужно интерпретировать при старте.
Android 7 (Nougat):
Впервые введена функция Direct Boot, позволяющая приложениям выполнять задачи (например, будильник или приём SMS) до ввода PIN-кода.
Android 8 (Oreo):
Представлен Google Play Protect — система защиты, которая сканирует приложения на наличие вредоносного кода.
Улучшена безопасность установки приложений из сторонних источников: теперь каждое приложение требует отдельного разрешения на установку.
Укрепление работы шифрования.
Android 7 (Nougat): Поддержка новых API для работы с графикой, таких как Vulkan API для улучшения производительности в играх.
Android 8 (Oreo):
Новый формат LDAC для передачи высококачественного звука через Bluetooth (например, для беспроводных наушников).
Поддержка Wide Color Gamut для улучшения отображения цветов на экранах с расширенным динамическим диапазоном (HDR).
Android 7 (Nougat):
Уведомления стали интерактивными, добавлена возможность ответа прямо из уведомления.
Поддержка группировки уведомлений.
Android 8 (Oreo):
Каналы уведомлений: теперь можно настроить типы уведомлений, которые вы хотите получать от приложения (например, отключить уведомления о рекламных акциях, оставив только важные).
Добавлена возможность временного откладывания уведомлений (Snooze).
Android 7 (Nougat):
Поддержка многоязычия: возможность выбрать несколько языков одновременно.
Android 8 (Oreo):
Автоматическое заполнение форм (Autofill): помогает автоматически заполнять пароли и другую информацию в приложениях.
Обои адаптируются под светлую или тёмную тему интерфейса.
Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2💊1