Kotlin | Вопросы собесов
2.56K subscribers
30 photos
972 links
Download Telegram
🤔 Для чего нужен объект Job?

Job — это элемент управления жизненным циклом корутины. С его помощью можно:
- отменять корутину;
- отслеживать завершение;
- объединять с другими задачами;
- управлять родительскими и дочерними корутинами.


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

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

🚩Основное назначение

🟠Предоставление интерфейса пользователя
Основная задача — предоставить макет (layout), который будет содержать все элементы пользовательского интерфейса, такие как кнопки, текстовые поля, изображения и прочее, с которыми пользователь может взаимодействовать.

🟠Взаимодействие с пользователем
Служит в качестве "лица" приложения для взаимодействия с пользователем, обрабатывая пользовательские входные данные, такие как нажатие кнопок, ввод текста и т. д.

🟠Управление жизненным циклом
Android управляет активностями через заданный жизненный цикл, который определяет, как активность создается, запускается, останавливается и уничтожается. Разработчики могут переопределять методы жизненного цикла, чтобы добавить свою логику обработки для разных состояний активности.

🟠Переход между экранами
В приложении обычно есть несколько активностей, и он используется для перехода от одного экрана к другому. Для перехода между активностями используются интенты (Intents), которые не только помогают открыть новую активность, но и могут передавать данные между активностями.

🟠Взаимодействие с другими компонентами приложения
М
ожет взаимодействовать с другими компонентами приложения, такими как Services, BroadcastReceivers, и ContentProviders, используя интенты и другие механизмы Android для межкомпонентного взаимодействия.

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


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

Конструкторы не переопределяются, потому что они не наследуются. Однако можно перегружать их в одном классе (разные параметры), и вызывать конструкторы суперкласса через super().


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

В Android есть несколько способов навигации между экранами. Давай разберём основные.

🟠Activity-based навигация
Каждый экран – это отдельная Activity, переход осуществляется с помощью Intent.
val intent = Intent(this, SecondActivity::class.java)
startActivity(intent)


🟠Fragment-based навигация
Один Activity содержит несколько Fragment, навигация через FragmentManager.
supportFragmentManager.beginTransaction()
.replace(R.id.container, SecondFragment())
.addToBackStack(null)
.commit()


🟠Navigation Component (Jetpack)
Google рекомендует Navigation Component для удобной работы с Fragment.
findNavController().navigate(R.id.action_firstFragment_to_secondFragment)


🟠Bottom Navigation / Tab Navigation
Используется BottomNavigationView или TabLayout для переключения между экранами.
bottomNavigationView.setOnNavigationItemSelectedListener { item ->
when (item.itemId) {
R.id.home -> replaceFragment(HomeFragment())
R.id.profile -> replaceFragment(ProfileFragment())
}
true
}


🟠Drawer Navigation (Navigation Drawer)
Боковое меню (DrawerLayout) с пунктами навигации.
drawerLayout.openDrawer(GravityCompat.START)


🟠Deep Links и App Links
Навигация через ссылки (например, myapp://profile/123).
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<data android:scheme="myapp" android:host="profile"/>
</intent-filter>


🟠Navigation в Jetpack Compose
Используется NavHost и NavController.
NavHost(navController, startDestination = "home") {
composable("home") { HomeScreen(navController) }
composable("profile") { ProfileScreen(navController) }
}


Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍4
🤔 Что такое multibinding в Dagger?

Multibinding позволяет создать коллекции зависимостей одного типа — например, Set или Map. Это особенно полезно для регистрации нескольких обработчиков событий, плагинов или реализаций одного интерфейса.


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

В многомодульных проектах каждый модуль может иметь свой AndroidManifest.xml, чтобы:
Задавать зависимости (uses-permission, uses-feature) для конкретного модуля.
Определять компоненты (Activity, Service, BroadcastReceiver) для каждого модуля.
Автоматически объединять манифесты всех модулей в AndroidManifest.xml главного (app) модуля.

🚩Как объединяются манифесты в многомодульном проекте?

При сборке Gradle автоматически сливает (merge) все AndroidManifest.xml в один итоговый файл.
app/  
├── src/main/AndroidManifest.xml ← Главный манифест
├── java/com/example/MainActivity.kt
├── res/
├── build.gradle
feature_login/
├── src/main/AndroidManifest.xml ← Манифест модуля `login`
├── java/com/example/login/LoginActivity.kt
├── res/
├── build.gradle
feature_chat/
├── src/main/AndroidManifest.xml ← Манифест модуля `chat`
├── java/com/example/chat/ChatActivity.kt
├── res/
├── build.gradle


🚩Как работает объединение манифестов?

При сборке манифесты модулей сливаются в манифест главного модуля.
<manifest package="com.example.app">
<application>
<activity android:name=".MainActivity" />
</application>
</manifest>


feature_login (feature_login/src/main/AndroidManifest.xml)
<manifest>
<application>
<activity android:name=".login.LoginActivity" />
</application>
</manifest>


feature_chat (feature_chat/src/main/AndroidManifest.xml)
<manifest>
<application>
<activity android:name=".chat.ChatActivity" />
</application>
</manifest>


После объединения итоговый AndroidManifest.xml выглядит так
<manifest package="com.example.app">
<application>
<activity android:name=".MainActivity" />
<activity android:name=".login.LoginActivity" />
<activity android:name=".chat.ChatActivity" />
</application>
</manifest>


🚩Зачем модулям свой `AndroidManifest.xml`?

🟠Разные разрешения (`uses-permission`) для каждого модуля
Например, модуль camera требует CAMERA, но другие модули — нет.
<uses-permission android:name="android.permission.CAMERA" />


🟠Разные `Activity`, `Service`, `BroadcastReceiver` в каждом модуле
Каждый модуль добавляет только свои компоненты (например, LoginActivity в feature_login).

🟠Разные зависимости для разных модулей
Например, модуль feature_map использует Google Maps, но feature_login — нет.
<uses-library android:name="com.google.android.maps" />


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

На Android популярная библиотека для выявления утечек памяти — LeakCanary от Square. Она отслеживает потенциальные утечки (Activity, Fragment, Context, View) и уведомляет разработчика при обнаружении утечки с подробной трассировкой.

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

DiffUtil — это утилита для быстрого обновления списков в RecyclerView. Она сравнивает старый и новый список и находит различия, чтобы обновлять только изменённые элементы, а не весь список.

🚩Проблема без `DiffUtil`

Обычное обновление списка без DiffUtil перерисовывает всё, даже если изменился один элемент.
adapter.notifyDataSetChanged() // Полностью обновляет список 


🟠Решение с `DiffUtil`
DiffUtil находит различия между старым и новым списком и обновляет только изменённые элементы.
class MyDiffUtilCallback(
private val oldList: List<User>,
private val newList: List<User>
) : DiffUtil.Callback() {

override fun getOldListSize() = oldList.size
override fun getNewListSize() = newList.size

override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition].id == newList[newItemPosition].id
}

override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
return oldList[oldItemPosition] == newList[newItemPosition] // Используем data class (авто `equals()`)
}
}


🚩Как использовать `DiffUtil` в `RecyclerView.Adapter`?

После создания DiffUtil.Callback, вызываем DiffUtil.calculateDiff() и передаём результат в adapter.
fun updateList(newList: List<User>) {
val diffCallback = MyDiffUtilCallback(userList, newList)
val diffResult = DiffUtil.calculateDiff(diffCallback)

userList = newList // Обновляем старый список
diffResult.dispatchUpdatesTo(this) // Обновляем только изменённые элементы
}


🟠`ListAdapter` – ещё проще!
Если используем ListAdapter, DiffUtil уже встроен.
class UserAdapter : ListAdapter<User, UserViewHolder>(DIFF_CALLBACK) {

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_user, parent, false)
return UserViewHolder(view)
}

override fun onBindViewHolder(holder: UserViewHolder, position: Int) {
holder.bind(getItem(position)) // `getItem()` уже работает с `ListAdapter`
}

companion object {
private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<User>() {
override fun areItemsTheSame(oldItem: User, newItem: User): Boolean =
oldItem.id == newItem.id

override fun areContentsTheSame(oldItem: User, newItem: User): Boolean =
oldItem == newItem
}
}
}


Обновление списка в ListAdapter
adapter.submitList(newList) // DiffUtil работает внутри 🚀


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

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

Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥1
🤔 В каких случаях используют atomic, в каких synchronized?

В Kotlin для работы с многопоточностью можно использовать atomic переменные или synchronized блоки. Они помогают избежать гонок данных (race conditions), когда несколько потоков одновременно изменяют одну и ту же переменную.

🚩Когда использовать `atomic` переменные?

Atomic переменные (AtomicInteger, AtomicLong, AtomicReference и др.) используются, когда нужно быстро и безопасно обновлять одно значение без блокировки потока.
Высокая производительность – атомарные операции работают быстрее, чем synchronized.
Потокобезопасность – операции выполняются без блокировок (lock-free).
Удобство – простые методы incrementAndGet(), compareAndSet() и т. д.
Работает только с одним значением – не подходит для сложных операций.
Не подходит для зависимых изменений (например, если изменение одной переменной зависит от другой).

import java.util.concurrent.atomic.AtomicInteger

val counter = AtomicInteger(0)

fun increment() {
counter.incrementAndGet() // Увеличиваем значение безопасно
}


🟠Когда использовать `synchronized`?
Используется, когда нужно защитить сразу несколько связанных операций от одновременного выполнения в нескольких потоках.
Подходит для сложных операций с несколькими переменными.
Работает с любыми типами данных.
Снижает производительность – потоки ждут, пока один поток закончит работу.
Может вызывать deadlock (взаимную блокировку).
val lock = Any()
var count = 0

fun increment() {
synchronized(lock) {
count++ // Только один поток может изменять `count` одновременно
}
}


Ставь 👍 и забирай 📚 Базу знаний
Please open Telegram to view this post
VIEW IN TELEGRAM
👍2
🤔 Как SVG-строку показать в виде векторного файла?

– Сохранить строку как *.svg в файл и загрузить через SvgDecoder,
– Использовать библиотеки (Glide, Coil, androidsvg),
– Преобразовать в Drawable и отобразить через ImageView.


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

При создании классов по сравнению с 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
}
}
}


🟠Data классы
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
🤔 Как заинжектить ViewModel в Dagger Hilt?

В Hilt ViewModel помечается аннотацией
@HiltViewModel, а зависимости внедряются через конструктор. ViewModel создаётся с помощью by viewModels() в Activity или Fragment, автоматически подставляя зависимости.

Ставь 👍 если знал ответ, 🔥 если нет
Забирай 📚 Базу знаний
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 // Модификация состояния
}
}


🟠Модификация 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
🤔4👍1
🤔 Каким критериям должны соответствовать ссылочные типы?

Ссылочные типы в Kotlin:
- неизменяемые, если это требуется по архитектуре;
- nullable или not-null — в зависимости от требований;
- должны реализовывать equals, hashCode и toString, особенно если используются в коллекциях;
- быть легковесными, если часто копируются;
- желательно — final или sealed, если поведение фиксировано.


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

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
🤔 Как работает jetpackCompose?

Jetpack Compose – это декларативный UI-фреймворк от Google для создания интерфейсов в Android. Вместо традиционных XML + View в Compose используется функции-компоненты, которые описывают UI.

🟠Главные принципы работы Jetpack Compose
Декларативный подход – UI создаётся через функции, без XML.
еактивность – UI автоматически обновляется, если данные изменились.
Компонентный подход – UI состоит из маленьких, переиспользуемых функций.
Composable-функции (@Composable) – основной строительный блок Compose.

🟠Как создаётся UI в Compose?
Вместо Activity и Fragment с findViewById() используется @Composable-функции.
@Composable
fun Greeting(name: String) {
Text(text = "Привет, $name!")
}

@Composable
fun MyScreen() {
Column {
Greeting("Андрей")
Greeting("Мария")
}
}

@Preview
@Composable
fun PreviewMyScreen() {
MyScreen()
}


🟠Реактивность: автоматическое обновление UI
Jetpack Compose использует реактивный подход, где UI обновляется при изменении состояния.
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) } // Реактивное состояние

Column {
Text("Счётчик: $count")
Button(onClick = { count++ }) {
Text("Увеличить")
}
}
}


🟠Основные UI-компоненты в Compose
Текст
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("Справа")
}


🟠Взаимодействие с ViewModel
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("Добавить")
}
}
}


🟠Навигация в Jetpack Compose
Вместо 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?

Bundle — это контейнер для передачи данных между компонентами Android, такими как Activity, Fragment или Service. Он хранит пары ключ-значение и может сохраняться при пересоздании компонента.


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

Термин "Extensions" (расширения) используется для обозначения функциональности, которая позволяет добавлять новые возможности к существующим классам без изменения их исходного кода. Этот концепт особенно популярен к примеру в Swift и Kotlin, где расширения используются для улучшения читаемости кода, упрощения его структуры и добавления удобных методов для работы с уже существующими типами данных.

🚩Расширения в Kotlin

Расширения позволяют добавить новую функциональность к классам, даже если исходный код этих классов недоступен для изменений (например, классы из стандартной библиотеки или сторонних библиотек). Это делается путём объявления функций или свойств, которые могут быть вызваны на экземплярах этих классов, как если бы они были частью оригинальных классов.
// Расширение класса String для проверки, пуста ли строка или состоит из пробелов
fun String.isNullOrBlank(): Boolean {
return this == null || this.trim().isEmpty()
}

// Использование расширения
val myString = " "
println(myString.isNullOrBlank()) // Выведет: true


🚩Расширения в Swift

Использует очень похожий подход, позволяя добавлять новые методы и вычисляемые свойства к существующим типам. Расширения могут даже добавлять новые протоколы к типам, что делает их чрезвычайно мощным инструментом в разработке под 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