Kotlin | Вопросы собесов
2.57K subscribers
28 photos
966 links
Download Telegram
🤔 Как решить проблему утечек памяти в RxJava?

RxJava может вызывать утечки памяти, если подписки (Disposable) не очищаются правильно.
Утечки памяти происходят, когда RxJava-события продолжают выполняться после уничтожения Activity/Fragment, удерживая ссылки на View или Context.

🚩Используем `CompositeDisposable`

CompositeDisposable собирает все подписки и очищает их при onDestroy().
class MainActivity : AppCompatActivity() {
private val disposables = CompositeDisposable() // Собираем подписки

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val disposable = Observable.interval(1, TimeUnit.SECONDS)
.subscribe { println("Tick: $it") }

disposables.add(disposable) // Добавляем подписку
}

override fun onDestroy() {
super.onDestroy()
disposables.clear() // Очищаем подписки, предотвращая утечку памяти
}
}


🚩Используем `autoDispose` (для Jetpack Lifecycle)

Если используем Jetpack ViewModel или LifecycleOwner, можно автоматически отвязывать подписки.
class MyFragment : Fragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
Observable.interval(1, TimeUnit.SECONDS)
.autoDispose(viewLifecycleOwner) // Автоочистка при `onDestroyView()`
.subscribe { println("Tick: $it") }
}
}


🚩Используем `Disposable.dispose()` вручную

Если подписка не в CompositeDisposable, её можно очистить вручную.
private var disposable: Disposable? = null

fun startObserving() {
disposable = Observable.interval(1, TimeUnit.SECONDS)
.subscribe { println("Tick: $it") }
}

fun stopObserving() {
disposable?.dispose() // Очищаем подписку
}


🚩Используем `ViewModel` + `LiveData` вместо RxJava

RxJava может удерживать Activity/Fragment, вызывая утечки памяти.
Лучше использовать ViewModel + LiveData!
class MyViewModel : ViewModel() {
private val _timer = MutableLiveData<Long>()
val timer: LiveData<Long> = _timer

init {
Observable.interval(1, TimeUnit.SECONDS)
.subscribe { _timer.postValue(it) }
}
}


Подписка в Activity (без утечек!)
viewModel.timer.observe(this) { println("Tick: $it") }


Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
🤔 В каком порядке вызывается: конструктор, конструктор суперкласса, статический блок инициализации?

Порядок:
1. Статический блок суперкласса;
2. Статический блок текущего класса;
3. Конструктор суперкласса;
4. Конструктор текущего класса.
(Статические блоки вызываются один раз при загрузке класса.)


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥4
🤔 Что знаешь о модификациях?

Модификации могут означать разные вещи в зависимости от контекста. В Android-разработке чаще всего речь идёт о следующих типах модификаций:

Модификация данных (изменение состояния)
Модификация UI (Compose Modifiers)
Модификация кода (рефакторинг, оптимизация)
Модификация системы (кастомные прошивки, рут-изменения)

🟠Модификация данных (изменение состояния)
Это изменение переменных, объектов и структур данных во время работы приложения.
class ViewModel : ViewModel() {
private val _state = MutableStateFlow("Привет, мир!")
val state: StateFlow<String> = _state

fun updateText(newText: String) {
_state.value = newText // Модификация состояния
}
}


🟠Модификация UI (Jetpack Compose Modifiers)
В 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
💊7👍1
🤔 Зачем нужна аннотация @CallSuper?

Аннотация
@CallSuper указывает, что метод в подклассе должен вызывать метод суперкласса. Это полезно при переопределении методов, где требуется сохранить логическую цепочку вызовов (например, onStart(), onDestroy() в Android).

Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥4👍2
🤔 Расскажи о контрактах equals и hashCode

Методы equals() и hashCode() используются для сравнения объектов и их корректной работы в коллекциях (Set, Map).

🚩Контракт `equals()`

Метод equals() должен:
Рефлексивность: a.equals(a)true (объект равен самому себе).
Симметричность: a.equals(b) == b.equals(a).
Транзитивность: если a == b и b == c, то a == c.
Согласованность: если a == b, то a.equals(b) всегда возвращает одно и то же, пока объект не изменится.
Сравнение с null всегда даёт false: a.equals(null) == false.
class User(val name: String, val age: Int) {
override fun equals(other: Any?): Boolean {
if (this === other) return true // Проверка на ссылочное равенство
if (other !is User) return false // Проверка типа

return name == other.name && age == other.age // Сравнение полей
}
}


val user1 = User("Alice", 25)
val user2 = User("Alice", 25)
println(user1 == user2) // true (потому что переопределён equals)


🚩Контракт `hashCode()`

Метод hashCode() должен:
Согласованность с equals(): если a == b, то a.hashCode() == b.hashCode().
Но обратное не обязательно: два разных объекта могут иметь одинаковый hashCode().
Хеш-код не должен меняться, если объект не изменился.
class User(val name: String, val age: Int) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is User) return false
return name == other.name && age == other.age
}

override fun hashCode(): Int {
return name.hashCode() * 31 + age // 31 - стандартный коэффициент
}
}


val userSet = HashSet<User>()
userSet.add(User("Alice", 25))
println(userSet.contains(User("Alice", 25))) // true


🚩Автоматическая генерация в Kotlin

Чтобы не писать equals() и hashCode() вручную, можно использовать data class:
data class User(val name: String, val age: Int)


data class автоматически создаёт equals(), hashCode(), а также copy() и toString().
val user1 = User("Alice", 25)
val user2 = User("Alice", 25)
println(user1 == user2) // true (equals)
println(user1.hashCode() == user2.hashCode()) // true


Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
🤔 Какие отличия наследования от композиции?

Наследование — это когда один класс расширяет другой, наследуя его поведение.
Композиция — это включение одного объекта в другой, при этом зависимость явно передаётся. Композиция считается более гибкой и менее связанной альтернативой наследованию.


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥2
🤔 Что происходит когда делаешь ==?

В Kotlin оператор == используется для структурного сравнения объектов.

🚩Что делает `==` в Kotlin?

Оператор == вызывает метод equals(), чтобы проверить содержимое объектов, а не их ссылки.
data class User(val name: String)

fun main() {
val user1 = User("Alice")
val user2 = User("Alice")

println(user1 == user2) // true (структурное сравнение)
println(user1 === user2) // false (сравнение ссылок)
}


🚩Чем `==` отличается от `===`?

== проверяет, равны ли данные объектов (equals()).
=== проверяет, ссылаются ли объекты на один и тот же участок памяти.
val str1 = "Hello"
val str2 = "Hello"
println(str1 == str2) // true (содержимое одинаковое)
println(str1 === str2) // true (Kotlin кеширует строки)

val obj1 = String("Hello".toCharArray())
val obj2 = String("Hello".toCharArray())
println(obj1 == obj2) // true (содержимое одинаковое)
println(obj1 === obj2) // false (разные объекты в памяти)


🚩Как работает `==` для классов?

Если equals() не переопределён, то сравниваются ссылки (как ===).
class Person(val name: String)

fun main() {
val p1 = Person("Bob")
val p2 = Person("Bob")

println(p1 == p2) // false (equals() не переопределён, сравниваются ссылки)
}


Чтобы == работал по содержимому, нужно переопределить equals()
class Person(val name: String) {
override fun equals(other: Any?): Boolean {
return other is Person && other.name == this.name
}
}

fun main() {
val p1 = Person("Bob")
val p2 = Person("Bob")

println(p1 == p2) // true (equals() сравнивает содержимое)
}


🚩Как `==` работает с `null`?

Kotlin предотвращает NullPointerException при сравнении с null:
val a: String? = null
val b: String? = "Hello"

println(a == b) // false (без NPE)
println(a == null) // true


Когда a == b, на самом деле выполняется:
a?.equals(b) ?: (b === null)


Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
🤔 Является ли LayoutInflater синглтоном и почему?

Нет, LayoutInflater — не синглтон, но может быть получен как скоуп-зависимый объект (getSystemService) в Context. Однако его можно переиспользовать, так как он не хранит состояния между вызовами.


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
💊4🔥2
🤔 Зачем нужно разделять отображение и бизнес-логику?

Разделение UI и бизнес-логики делает код понятнее, тестируемее и проще в поддержке.
Это ключевой принцип чистой архитектуры (Clean Architecture) и паттернов MVP, MVVM, MVI.

🚩Что такое UI и бизнес-логика?

Плохой код (UI + логика в Activity)
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val button = findViewById<Button>(R.id.button)
button.setOnClickListener {
val result = fetchData() // Логика в UI
button.text = result
}
}

fun fetchData(): String {
return "Данные с сервера" // Тут должна быть ViewModel
}
}


🚩Как правильно разделять UI и логику? (MVVM)

Разделим код на Activity + ViewModel
MainActivity (только UI)
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val button = findViewById<Button>(R.id.button)

viewModel.text.observe(this) { text ->
button.text = text // UI обновляется из ViewModel
}

button.setOnClickListener {
viewModel.loadData() // Вызываем бизнес-логику
}
}
}


MainViewModel (бизнес-логика)
class MainViewModel : ViewModel() {
private val _text = MutableLiveData<String>()
val text: LiveData<String> = _text

fun loadData() {
_text.value = "Данные с сервера" // UI не знает, откуда данные
}
}


Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
🤔 Как бороться с тем, что из-за deeplink Activity может открыться много раз?

Используй правильные launchMode и intent flags:
- В manifest:
- В коде:
Также можно использовать taskAffinity и фильтрацию в onNewIntent().


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥1💊1
🤔 Расскажи про ключевое слово Object в Kotlin?

Ключевое слово object в Kotlin имеет несколько важных применений, и оно является одной из наиболее мощных и уникальных возможностей языка.

🟠Объявление объекта-одиночки (Singleton)
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()
}


🟠Компаньон-объекты (Companion Object)
Ключевое слово 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
}


🚩Почему `object` так полезен?

🟠Лаконичность
Вместо написания множества шаблонного кода для Singleton, анонимных объектов или статических методов, вы получаете готовую реализацию из коробки.

🟠Потокобезопасность
В случае object, компилятор Kotlin автоматически гарантирует, что объект создаётся в потокобезопасном режиме.

🟠Гибкость
object можно использовать практически везде: глобально, локально, внутри классов и функций.

Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4🔥1
🤔 Для чего нужны inline функции?

Inline-функции в Kotlin позволяют встроить тело функции непосредственно в место её вызова, что уменьшает накладные расходы на вызовы функций и повышает производительность. Это особенно полезно при работе с лямбда-выражениями, так как позволяет избежать создания объектов для лямбд и снижает нагрузку на сборщик мусора. Inline-функции полезны для простых функций, которые часто вызываются в циклах или асинхронных операциях. Они позволяют писать оптимизированный и эффективный код.

Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥3👍1
🤔 Зачем нужны Data Class и Sealed Classes?

Data Class и Sealed Class решают разные задачи и обеспечивают улучшения в организации кода, управлении состоянием и безопасности типов. Они вносят значительные упрощения и повышают читаемость кода в Kotlin-проектах.

🚩Зачем они нужны

Data Class предназначены для хранения данных и автоматически предоставляют ряд полезных методов, что упрощает разработку и уменьшает объем шаблонного кода. Основные причины использования Data Class:

🟠Сокращение кода
Автоматически генерирует методы equals(), hashCode(), и toString(), а также copy() и компонентные функции для объектов данных. Это избавляет от необходимости ручной реализации этих методов, что уменьшает количество кода и возможность ошибок.

🟠Упрощение передачи данных
Идеально подходят для передачи данных между различными частями приложения, например, между слоями в архитектуре MVVM или при передаче данных между активностями и фрагментами.

🟠Поддержка неизменяемости
С помощью них легко создавать неизменяемые объекты, что способствует безопасной работе с данными, особенно в многопоточной среде.

🚩Зачем они нужны

Sealed Class используются для определения закрытых иерархий классов, где все потомки известны и ограничены. Они полезны по следующим причинам:

🟠Полное покрытие случаев в `when`
Гарантируют, что все возможные подтипы обработаны в выражениях when, что предотвращает ошибки во время выполнения из-за пропущенных случаев. Это упрощает управление состояниями и делает код более безопасным и предсказуемым.

🟠Ограниченное наследование
Ограничивают возможность создания подклассов за пределами файла, в котором они объявлены. Это предотвращает неожиданное наследование и сохраняет иерархию классов контролируемой и понятной.

🟠Инкапсуляция
Поскольку все расширения таких классов должны быть объявлены в том же файле, это способствует лучшей инкапсуляции и организации кода.

data class User(val name: String, val age: Int)


Пример Sealed Class
sealed class Result {
data class Success(val data: String) : Result()
data class Failure(val error: Throwable) : Result()
}

fun handleResult(result: Result) {
when (result) {
is Result.Success -> println("Success with data: ${result.data}")
is Result.Failure -> println("Failure with error: ${result.error.message}")
}
}


Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3
🤔 Где создается композиция для вызова composable функции?

Композиция создаётся внутри функции setContent, которая задает точку входа для composable функций в Activity или Fragment. Она инициирует рендеринг интерфейса и управление состоянием.


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥3
🤔 Как в Ретрофите добавить возможность возвращать типы Rxjava?

Чтобы Retrofit мог возвращать Observable, Single, Maybe или Flowable из RxJava, нужно добавить RxJava Adapter.

🚩Добавление зависимости

В build.gradle.kts (Kotlin DSL)
dependencies {
implementation("com.squareup.retrofit2:adapter-rxjava3:2.9.0") // Адаптер для RxJava 3
implementation("io.reactivex.rxjava3:rxjava:3.1.8") // RxJava 3
}


🚩Подключение `RxJava3CallAdapterFactory`

Добавляем адаптер в Retrofit.Builder
val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.addConverterFactory(GsonConverterFactory.create()) // Преобразование JSON
.addCallAdapterFactory(RxJava3CallAdapterFactory.create()) // Поддержка RxJava
.build()


🚩Использование RxJava в API

Теперь можно возвращать RxJava-объекты вместо Call<>.
interface ApiService {
@GET("users/{id}")
fun getUser(@Path("id") userId: Int): Single<User>
}


Пример с Observable<> (несколько данных или обновления)
interface ApiService {
@GET("users")
fun getUsers(): Observable<List<User>>
}


Пример с Flowable<> (если нужен Backpressure)
interface ApiService {
@GET("posts")
fun getPosts(): Flowable<List<Post>>
}


🚩Подписка и обработка результата

Пример подписки в ViewModel (RxJava 3 + LiveData)
class UserViewModel(private val apiService: ApiService) : ViewModel() {

private val _userLiveData = MutableLiveData<User>()
val userLiveData: LiveData<User> = _userLiveData

fun fetchUser(userId: Int) {
apiService.getUser(userId)
.subscribeOn(Schedulers.io()) // Запрос в фоновом потоке
.observeOn(AndroidSchedulers.mainThread()) // Обновление UI в главном потоке
.subscribe({ user ->
_userLiveData.value = user
}, { error ->
Log.e("UserViewModel", "Ошибка загрузки", error)
})
}
}


Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
🤔 Какие особенности отличия deep link от app link?

- Deep link: работает через схему URI (например, myapp://), требует настройки <intent-filter>.
- App link: использует HTTP/HTTPS ссылки и требует подтверждения владения доменом через файл assetlinks.json.


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥2
🤔 Как выглядит алгоритм запроса данных из двух таблиц и какая у него сложность?

Запрос данных из двух таблиц обычно выполняется с помощью операции объединения (JOIN) в SQL. Алгоритм и его сложность зависят от типа объединения, структуры данных и используемой базы данных.

🚩Пример запроса

Предположим, у нас есть две таблицы: employees и departments. Мы хотим получить список сотрудников вместе с их отделами.
SELECT employees.name, departments.name
FROM employees
INNER JOIN departments ON employees.department_id = departments.id;


🚩Алгоритмы выполнения JOIN

1⃣Nested Loop Join (Вложенные циклы)
Для каждой строки из первой таблицы выполняется поиск соответствующих строк во второй таблице.
for each row in employees:
for each row in departments:
if row.employees.department_id == row.departments.id:
yield (row.employees.name, row.departments.name)


2⃣Hash Join (Хеш-объединение)
Создается хеш-таблица для одной из таблиц на основе ключа соединения. Для каждой строки из другой таблицы проверяется наличие соответствующего ключа в хеш-таблице.
hash_table = {}
for each row in departments:
hash_table[row.id] = row.name
for each row in employees:
if row.department_id in hash_table:
yield (row.name, hash_table[row.department_id])


3⃣Sort-Merge Join (Сортировка и слияние)
Обе таблицы сортируются по ключу соединения, затем выполняется слияние отсортированных списков.
sorted_employees = sort(employees, key=lambda x: x.department_id)
sorted_departments = sort(departments, key=lambda x: x.id)
i, j = 0, 0
while i < len(sorted_employees) and j < len(sorted_departments):
if sorted_employees[i].department_id == sorted_departments[j].id:
yield (sorted_employees[i].name, sorted_departments[j].name)
i += 1
j += 1
elif sorted_employees[i].department_id < sorted_departments[j].id:
i += 1
else:
j += 1


Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
💊3👍1
🤔 Какой архитектурный паттерн реализуется благодаря ViewModel?

ViewModel реализует паттерн MVVM (Model-View-ViewModel). Этот паттерн разделяет представление (UI) и логику обработки данных, обеспечивая независимость между View и Model. ViewModel служит посредником, управляя состоянием и предоставляя данные View, сохраняя их при поворотах экрана или других изменениях конфигурации.

Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4🔥2
🤔 Можешь рассказать отличия и в каких случаях их использовать list set map?

В Kotlin есть три основных типа коллекций:
List — упорядоченный список элементов.
Set — множество уникальных элементов.
Map — коллекция пар "ключ-значение".

🚩List — упорядоченный список

List — это коллекция, в которой можно хранить дубликаты, а элементы доступны по индексу.
val numbers = listOf(1, 2, 3, 4, 5) // Immutable (нельзя изменять)
val mutableNumbers = mutableListOf(1, 2, 3) // Можно изменять

mutableNumbers.add(4) // Добавляем элемент
mutableNumbers.removeAt(1) // Удаляем элемент по индексу

println(mutableNumbers) // [1, 3, 4]


🚩Set — множество уникальных элементов

Set — это коллекция, в которой нет дубликатов.
val numbers = setOf(1, 2, 3, 3, 4, 5) // Дубликаты удаляются автоматически
println(numbers) // [1, 2, 3, 4, 5]

val mutableNumbers = mutableSetOf(1, 2, 3)
mutableNumbers.add(3) // Не добавится, потому что уже есть
mutableNumbers.add(4) // Добавится

println(mutableNumbers) // [1, 2, 3, 4]


🚩Map — коллекция пар "ключ-значение"

Map — это структура данных, в которой каждому ключу соответствует одно значение.
val userMap = mapOf(
1 to "Alice",
2 to "Bob",
3 to "Charlie"
) // Immutable

val mutableUserMap = mutableMapOf(1 to "Alice", 2 to "Bob")
mutableUserMap[3] = "Charlie" // Добавляем новый ключ-значение
mutableUserMap.remove(1) // Удаляем элемент по ключу

println(mutableUserMap) // {2=Bob, 3=Charlie}


Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
🤔 Без чего сервис не запустится в новых версиях Android?

Сервис не запустится без разрешения и foreground-нотификации, если он работает в фоне на Android 8.0 и выше. Также необходимо явно указать сервис в манифесте, и для долгоживущих задач использовать foregroundService, иначе система его завершит.


Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍1🔥1