← Назад к вопросам

Как использовать часть функциональности одного feature модуля в другом?

2.3 Middle🔥 201 комментариев
#Многомодульность#Архитектура и паттерны

Комментарии (1)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Использование функциональности feature-модуля в другом модуле

В современной Android-архитектуре с многомодульной структурой часто возникает необходимость разделить функционал на изолированные feature-модули, но при этом обеспечить возможность их взаимодействия. Вот основные подходы для решения этой задачи:

1. Создание общего модуля зависимостей

Самый чистый подход — выделение общих компонентов в отдельный модуль (обычно называемый core, shared или base), который затем подключается как зависимость в оба feature-модуля.

// В shared модуле
interface UserRepository {
    suspend fun getUser(id: String): User
}

class UserRepositoryImpl @Inject constructor(
    private val apiService: ApiService
) : UserRepository {
    override suspend fun getUser(id: String): User {
        return apiService.getUser(id)
    }
}

2. Использование интерфейсов через Dagger/Hilt

Наиболее популярный подход в комбинации с DI-фреймворками:

// В feature-auth модуле
interface AuthProvider {
    fun isUserLoggedIn(): Boolean
    suspend fun getCurrentUser(): User?
}

// В feature-profile модуле
class ProfileViewModel @Inject constructor(
    private val authProvider: AuthProvider
) : ViewModel() {
    fun loadProfile() {
        if (authProvider.isUserLoggedIn()) {
            // Загружаем профиль
        }
    }
}

3. Dynamic Feature Modules с Play Core Library

Для динамических модулей используйте Play Feature Delivery:

// В базовом модуле
val featureInstallManager = FeatureInstallManagerFactory.create(context)

suspend fun isFeatureAvailable(moduleName: String): Boolean {
    return featureInstallManager
        .getFeatureModules()
        .any { it.name == moduleName && it.installationState == INSTALLED }
}

// Запрос установки модуля
val request = SplitInstallRequest.newBuilder()
    .addModule("feature_payments")
    .build()

splitInstallManager.startInstall(request)

4. Публикация API через :api модули

Распространенный паттерн — разделение каждого feature-модуля на:

  • :feature-auth (implementation)
  • :feature-auth-api (только интерфейсы)
// В feature-auth-api/build.gradle.kts
plugins {
    id("com.android.library")
}

dependencies {
    // Минимальные зависимости, только то, что нужно для API
}

// В feature-profile/build.gradle.kts
dependencies {
    implementation(project(":feature-auth-api"))
    // НЕ implementation(project(":feature-auth"))
}

5. Использование Navigation Component для межмодульной навигации

Для навигации между feature-модулями:

// В базовом модуле создаем граф навигации
<navigation 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    app:startDestination="@id/mainFragment">
    
    <include app:graph="@navigation/feature_auth_graph" />
    <include app:graph="@navigation/feature_profile_graph" />
</navigation>

// Динамическое включение графов
val navHostFragment = supportFragmentManager
    .findFragmentById(R.id.nav_host_fragment) as NavHostFragment

val navController = navHostFragment.navController
val graph = navController.navInflater.inflate(R.navigation.main_nav)
graph.addAll(navController.navInflater.inflate(R.navigation.feature_auth))
navController.graph = graph

6. Event Bus / Shared Flow для коммуникации

Для слабосвязанной коммуникации между модулями:

// В core модуле
object AppEventBus {
    private val _events = MutableSharedFlow<AppEvent>()
    val events = _events.asSharedFlow()
    
    suspend fun emit(event: AppEvent) {
        _events.emit(event)
    }
}

sealed class AppEvent {
    data class UserLoggedIn(val user: User) : AppEvent()
    object UserLoggedOut : AppEvent()
}

// В feature-auth (отправка)
AppEventBus.emit(AppEvent.UserLoggedIn(user))

// В feature-profile (получение)
AppEventBus.events
    .filterIsInstance<AppEvent.UserLoggedIn>()
    .collect { user ->
        updateProfile(user)
    }

7. Service Locator Pattern

Альтернатива DI для простых случаев:

object ServiceLocator {
    private val services = mutableMapOf<Class<*>, Any>()
    
    fun <T : Any> register(service: T, clazz: Class<T>) {
        services[clazz] = service
    }
    
    @Suppress("UNCHECKED_CAST")
    fun <T> get(clazz: Class<T>): T {
        return services[clazz] as T
    }
}

// Регистрация в feature-auth
ServiceLocator.register(AuthManager(), AuthManager::class.java)

// Использование в feature-profile
val authManager = ServiceLocator.get(AuthManager::class.java)

Критерии выбора подхода

  1. Степень связанности:

    • Для тесной интеграции → DI с интерфейсами
    • Для слабой связи → Event Bus / Shared Flow
  2. Динамичность:

    • Статические модули → Простые зависимости
    • Dynamic Features → Play Core Library
  3. Масштаб проекта:

    • Маленькие проекты → Service Locator
    • Крупные проекты → Четкое разделение на api/implementation модули
  4. Тестируемость:

    • DI подходы легче мокать и тестировать
    • Глобальные Event Bus сложнее тестировать изолированно

Рекомендация: Для большинства production-проектов оптимальным является комбинация Dagger/Hilt с выделением API модулей плюс Navigation Component для навигации. Это обеспечивает хороший баланс между изоляцией модулей и возможностью их взаимодействия, поддерживает тестируемость и соответствует принципам чистой архитектуры.