Почему нельзя LocalContext.current вызывать в Use Case или репозитории?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Почему LocalContext.current нельзя вызывать в Use Case или Repository
Основная причина заключается в нарушении принципов чистой архитектуры (Clean Architecture) и разделения ответственности (Separation of Concerns), которые являются фундаментальными для создания масштабируемых, тестируемых и устойчивых к изменениям Android приложений.
Архитектурные принципы и слои
В современной Android разработке, особенно с использованием таких подходов как MVVM, MVI или Clean Architecture, принято разделять код на четкие слои:
- Presentation Layer (UI) —
Activity,Fragment,Compose UI,ViewModel. - Domain Layer (Business Logic) — Use Cases (Интеракторы).
- Data Layer — Repository,
DataSource(Local, Remote).
Ключевое правило: Domain и Data слои должны быть независимы от платформы и фреймворка. Они не должны знать о Android SDK, Context, Activity, Fragment или Jetpack Compose. Это позволяет:
- Тестировать бизнес-логику и данные без эмулятора/устройства. Use Case и Repository можно тестировать с помощью обычных юнит-тестов (JUnit) на JVM.
- Переносить логику на другие платформы (например, на серверную часть).
- Изолировать изменения. Если Android SDK или Compose изменятся, это не затронет ядро бизнес-логики.
- Предотвращать случайные зависимости.
Context— это тяжелый, платформо-специфичный объект, который несет в себе множество зависимостей (ресурсы, системные сервисы, жизнь Activity).
LocalContext.current — это Compose-specific API, который предоставляет Context внутри области композиции (Composition). Его использование напрямую в Use Case или Repository мгновенно привязывает эти архитектурные слои к Android и Jetpack Compose, разрушая их независимость.
Проблемы прямого использования LocalContext.current в низких слоях
-
Невозможность юнит-тестирования. Код, зависящий от
LocalContext.current, невозможно запустить в тестовой среде JVM. Это нарушает базовый принцип тестирования.// Плохой пример: Use Case зависит от Context class BadUserUseCase { fun execute(): User { val context = LocalContext.current // ОШИБКА: доступ только в Composition! // ... логика, использующая context } } // Тест для такого UseCase НЕВОЗМОЖЕН в JUnit class BadUserUseCaseTest { @Test fun testExecute() { val useCase = BadUserUseCase() useCase.execute() // Упадет, так как нет Composition и LocalContext } } -
Смешение ответственности. Use Case должен отвечать только за бизнес-правила (например, "пользователь может совершить покупку, если у него достаточный баланс"). Он не должен знать, как получить доступ к файловой системе (
context.filesDir) или запустить сервис. Repository должен абстрагировать источник данных ("получить пользователя из сети или базы"), но не реализовывать Android-специфичные операции для этого. -
Сложность и ненадежность получения
Context.LocalContext.currentдоступен только внутри CompositionScope во время выполнения композиции. Вызов его вне этой области (например, внутри фонового корутин-вызова или инициализации класса) приведет к исключению.// Этот код приведет к ошибке при выполнении class MyRepository { // Инициализация происходит при создании объекта, вне Composition private val context = LocalContext.current // IllegalCompositionException! fun fetchData(): Flow<Data> { // Даже если вызвать здесь, это может быть вне композиции val ctx = LocalContext.current // Опасно и неправильно! return flow { ... } } }
Правильный подход: Dependency Injection (DI)
Правильный способ предоставить Context (или его специфичные части, например ApplicationContext) в нижние слои — использовать инъекцию зависимостей (Dependency Injection).
Context(обычноApplicationContext) внедряется как зависимость через конструктор Repository.- Use Case получает все необходимые данные через свои параметры или зависимости (например, Repository), и не имеет прямого доступа к
Context.
// Правильный пример с Dependency Injection (используем Hilt для примера)
// Data Layer: Repository получает Context через DI
@Singleton
class CorrectUserRepository @Inject constructor(
private val appContext: ApplicationContext, // Внедряем ApplicationContext
private val apiService: ApiService
) {
fun getUser(id: String): User {
// Repository может использовать context для кода, требующего Android,
// например, чтения SharedPreferences или работы с Room DB.
val preferences = appContext.getSharedPreferences("prefs", MODE_PRIVATE)
// Но сама логика "получить пользователя" абстрагирована в Repository.
return apiService.fetchUser(id)
}
}
// Domain Layer: Use Case не знает о Context, только о Repository
class CorrectUserUseCase @Inject constructor(
private val userRepository: CorrectUserRepository
) {
suspend fun execute(userId: String): UserResult {
// Здесь только бизнес-логика.
val user = userRepository.getUser(userId)
return if (user.isActive) {
UserResult.Success(user)
} else {
UserResult.Error("User inactive")
}
}
}
// Presentation Layer: ViewModel и UI (Compose) вызывают Use Case
@HiltViewModel
class UserViewModel @Inject constructor(
private val userUseCase: CorrectUserUseCase
) : ViewModel() {
fun loadUser(id: String) {
viewModelScope.launch {
val result = userUseCase.execute(id)
// Обновить UI State
}
}
}
// В Compose UI мы можем безопасно использовать LocalContext.current для UI-задач
@Composable
fun UserScreen(viewModel: UserViewModel) {
val uiContext = LocalContext.current // Для UI: показ Toast, запуск Activity итд.
// Но этот context НЕ передается в Use Case или Repository.
}
Заключение
Вызов LocalContext.current в Use Case или Repository является серьезной архитектурной ошибкой. Он нарушает инкапсуляцию, тестируемость и переносимость ключевых компонентов приложения. Вместо этого следует применять практики Clean Architecture и Dependency Injection, где платформо-специфичные ресурсы (как Context) предоставляются только тем слоям, которым они действительно нужны (Repository через DI), и никогда — слою бизнес-логики (Use Case). Это создает прочную основу для долгосрочной поддержки и развития проекта.