Что такое Context и какой он бывает
android.content.Context ← абстрактный базовый класс
├── ContextImpl ← внутренняя реализация
├── ContextWrapper ← декоратор
│ ├── Application ← живёт всё время жизни процесса
│ ├── ContextThemeWrapper ← добавляет тему
│ │ └── Activity ← живёт до destroy экрана
│ └── Service ← живёт пока работает сервис| Тип | Когда использовать | Типичные ошибки |
|---|---|---|
Application | Синглтоны, репозитории, DI-граф | Попытка показать Dialog / надуть View с темой Activity |
Activity | UI: Dialog, inflate, startActivity | Утечка памяти, если хранить дольше onDestroy |
Service | Фоновые операции | То же, что и Activity |
Уже на этом этапе видно: под видом одного типа скрываются сущности с принципиально разным временем жизни и возможностями.
S — Single Responsibility Principle
Проблема
Context содержит ~100+ публичных методов, сгруппированных в совершенно несвязанные области:
// Ресурсы
getResources()
getString(R.string.app_name)
getColor(R.color.primary)
getDrawable(R.drawable.icon)
// Файловая система
openFileInput("data.txt")
getFilesDir()
getCacheDir()
deleteFile("data.txt")
// Запуск компонентов
startActivity(intent)
startService(intent)
sendBroadcast(intent)
// Разрешения
checkSelfPermission(Manifest.permission.CAMERA)
requestPermissions(...)
// Системные сервисы (Service Locator!)
getSystemService(Context.LOCATION_SERVICE)
getSystemService(Context.CLIPBOARD_SERVICE)
// Темы и UI
getTheme()
setTheme(R.style.AppTheme)
// Базы данных
openOrCreateDatabase("app.db", MODE_PRIVATE, null)
databaseList()
deleteDatabase("app.db")
// Уведомления, обои, вибрация, буфер обмена, пакетный менеджер...У этого класса нет одной причины для изменения. Любая новая системная capability в Android добавляется сюда же. Это антипаттерн God Object в чистом виде.
Что делать на уровне приложения
Мы не можем изменить SDK, но можем не плодить зависимость от Context внутри своей кодовой базы:
// ❌ Плохо: класс зависит от Context целиком
class UserRepository(private val context: Context) {
fun getDb() = context.openOrCreateDatabase("users.db", MODE_PRIVATE, null)
fun getCacheDir() = context.cacheDir
}
// ✅ Хорошо: зависимость только от того, что нужно
class UserRepository(
private val dbFactory: DatabaseFactory,
private val cacheDir: File
)O — Open/Closed Principle
Проблема
Context закрыт для расширения. Вы не можете добавить в него новую системную возможность, не дожидаясь нового API в Android SDK. Каждый новый сервис — новый метод getSystemService или новый метод в самом Context.
Исторический пример: чтобы получить WindowManager, вы пишете:
val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManagerА начиная с API 23 появился context.getSystemService(WindowManager::class.java). Старый код работает, но новый способ не расширяет старый, а добавляет параллельный путь.
Что делать
Оборачивать Context-зависимости в свои абстракции, открытые для расширения:
// Абстракция в вашем модуле
interface ResourceManager {
fun string(@StringRes id: Int): String
fun color(@ColorRes id: Int): Int
fun drawable(@DrawableRes id: Int): Drawable
}
// Реализация на границе с фреймворком
class AndroidResourceManager(private val context: Context) : ResourceManager {
override fun string(id: Int) = context.getString(id)
override fun color(id: Int) = ContextCompat.getColor(context, id)
override fun drawable(id: Int) = ContextCompat.getDrawable(context, id)!!
}Новая возможность = новый метод в ResourceManager, а не правка по всему проекту.
L — Liskov Substitution Principle
Проблема
Это самое больное место Android Context. Формально Activity — это Context. Но замена одного типа Context на другой ломает поведение:
fun showDialog(context: Context) {
// Скомпилируется с ЛЮБЫМ Context
AlertDialog.Builder(context) // ← но с Application Context будет краш
.setTitle("Удалить?")
.setMessage("Файл будет удалён навсегда")
.setPositiveButton("Да") { _, _ -> deleteFile() }
.show()
}| Что делаем | Application | Activity | Service |
|---|---|---|---|
getString() | ✅ | ✅ | ✅ |
startActivity() | ⚠️ нужен FLAG_ACTIVITY_NEW_TASK | ✅ | ⚠️ нужен флаг |
showDialog() | ❌ краш | ✅ | ❌ краш |
inflate(R.layout.x, root) | ⚠️ без темы | ✅ с темой | ⚠️ без темы |
getTheme() | базовая | тема из манифеста | базовая |
Подстановка Application вместо Activity в код, ожидающий UI-контекст, приводит к BadTokenException. Это прямое нарушение LSP: подтип не сохраняет контракт базового типа.
Что делать
// ❌ Плохо: принимаем абстрактный Context, не зная, что внутри
fun inflateCard(context: Context): View {
return LayoutInflater.from(context).inflate(R.layout.card, null)
}
// ✅ Хорошо: явно требуем контекст с темой
fun inflateCard(context: ContextThemeWrapper): View {
return LayoutInflater.from(context).inflate(R.layout.card, null)
}
// ✅ Ещё лучше: принимаем готовый LayoutInflater
fun inflateCard(inflater: LayoutInflater): View {
return inflater.inflate(R.layout.card, null)
}В современном Android (Jetpack Compose) эта проблема частично уходит: LocalContext передаётся через Composition, и тип контекста контролируется средой.
I — Interface Segregation Principle
Проблема
Когда вы передаёте Context в утилиту или хелпер, потребитель получает весь набор из 100+ методов, даже если ему нужен один:
// Нужен только getString()
fun formatPrice(context: Context, amount: Long): String {
return context.getString(R.string.price_format, amount / 100.0)
}
// Но вызывающий код вынужден передать ВЕСЬ контекст
formatPrice(activity, 19990L)Потребитель не должен знать о startActivity, openDatabase, sendBroadcast, если он просто форматирует строку. Но сигнатура метода говорит обратное.
Что делать
// ✅ Передаём минимально необходимое
fun formatPrice(resources: Resources, amount: Long): String {
return resources.getString(R.string.price_format, amount / 100.0)
}
// ✅ Или выделяем узкий интерфейс
interface StringProvider {
fun string(@StringRes id: Int, vararg args: Any): String
}
fun formatPrice(strings: StringProvider, amount: Long): String {
return strings.string(R.string.price_format, amount / 100.0)
}В модульной архитектуре это критично: feature-модуль не должен зависеть от Context, если ему нужны только ресурсы или строки.
D — Dependency Inversion Principle
Проблема
Context — это конкретный класс фреймворка, а не абстракция. Любой код, принимающий Context, привязан к Android SDK:
// ❌ Доменная логика зависит от Android
class TaxCalculator(private val context: Context) {
fun calculate(order: Order): BigDecimal {
val rate = context.resources
.openRawResource(R.raw.tax_rates) // ← Android в домене
.bufferedReader().use { it.readText() }
return parseAndCalculate(rate, order)
}
}Такой класс невозможно протестировать без Robolectric или инструментальных тестов, потому что Context нельзя просто замочить.
Отдельная боль — getSystemService, который является встроенным Service Locator:
val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManagerВы получаете конкретную реализацию по строковой константе. Ни инверсии, ни типобезопасности.
Что делать
// ✅ Домен не знает про Android
interface TaxRateProvider {
fun loadRates(): List<TaxRate>
}
class TaxCalculator(private val rateProvider: TaxRateProvider) {
fun calculate(order: Order): BigDecimal {
val rates = rateProvider.loadRates()
return parseAndCalculate(rates, order)
}
}
// ✅ Реализация на границе
class AndroidTaxRateProvider(private val context: Context) : TaxRateProvider {
override fun loadRates(): List<TaxRate> {
return context.resources.openRawResource(R.raw.tax_rates)
.bufferedReader().use { it.readText() }
.let(::parseRates)
}
}Тестирование становится тривиальным:
@Test
fun `calculate tax for order`() {
val calculator = TaxCalculator(FakeTaxRateProvider(listOf(TaxRate("RU", 0.20))))
val result = calculator.calculate(order)
assertEquals(BigDecimal("20.00"), result)
}Сводная таблица
| Принцип | Нарушение в Android Context | Практическое решение |
|---|---|---|
| SRP | ~100 методов: ресурсы, файлы, intents, сервисы, БД, разрешения | Обёртки-адаптеры с одной зоной ответственности |
| OCP | Новые возможности = новые методы в SDK, не расширяемо | Свои интерфейсы поверх Context |
| LSP | Application ≠ Activity для UI-операций, подстановка ломает поведение | Явные типы (ContextThemeWrapper, Activity), не абстрактный Context |
| ISP | Передавая Context, отдаём 100+ методов ради одного getString | Передача Resources, StringProvider, конкретных данных |
| DIP | Домен зависит от конкретного класса фреймворка | Абстракции на границе, Context только в инфраструктурном слое |
Практические правила для Android-проекта
1. Context не должен покидать инфраструктурный слой
┌─────────────────────────────┐
│ Presentation │ ← Activity/Fragment знают про Context
├─────────────────────────────┤
│ Domain │ ← НЕ знает про Context. Вообще.
├─────────────────────────────┤
│ Data / Infra │ ← Context живёт здесь, за интерфейсами
└─────────────────────────────┘2. Не храните Context в синглтонах без необходимости
// ❌ Утечка Activity
object Analytics {
lateinit var context: Context // если передали Activity — утечка
}
// ✅ Application Context, если действительно нужен
object Analytics {
fun init(app: Application) {
this.context = app.applicationContext // именно applicationContext
}
}3. Используйте applicationContext, когда не нужен UI
class ApiClient(context: Context) {
private val appContext = context.applicationContext // отвязываемся от Activity
private val cacheDir = appContext.cacheDir
}4. Минимизируйте передачу через extension-функции
// Вместо передачи Context повсюду
fun View.showSnackbar(message: String) {
Snackbar.make(this, message, Snackbar.LENGTH_SHORT).show()
}
// Вместо context.getString(...)
fun Fragment.toast(@StringRes resId: Int) {
Toast.makeText(requireContext(), resId, Toast.LENGTH_SHORT).show()
}5. В Compose-мире Context уже не течёт как раньше
@Composable
fun PriceCard(amount: Long) {
// Context доступен через LocalContext, но не передаётся в бизнес-логику
val context = LocalContext.current
// Лучше: вынести форматирование в ViewModel или use-case
val formatted = viewModel.formatPrice(amount)
Text(formatted)
}Историческая справка: почему так вышло
Context появился в Android 1.0 (2008), когда Java не имела DI-фреймворков, Kotlin не существовал, а архитектура приложения сводилась к одному Activity. Context был единственной точкой входа к системе, и это было оправдано для того уровня сложности.
Сегодня, когда приложение — это десятки модулей, Clean Architecture, Hilt/Koin, Compose и многопроцессность, Context остаётся архитектурным долгом уровня SDK, который невозможно удалить без обратной несовместимости.
Заключение
Context в Android — это вынужденный God Object на уровне фреймворка, который мы не можем исправить. Но мы можем и должны контролировать его влияние в нашем коде:
- Не передавать Context глубже, чем необходимо.
- Оборачивать в узкие интерфейсы на границе с фреймворком.
- Не хранить Activity Context дольше жизненного цикла экрана.
- Не пускать Context в доменный слой.
- Понимать, какой именно тип Context нужен: Application, Activity или Service.
Context — это мост между вашим приложением и Android. Мост должен быть на границе, а не проходить через каждую комнату в доме.
