diff --git a/app/src/main/java/com/otus/dihomework/MainNavigation.kt b/app/src/main/java/com/otus/dihomework/MainNavigation.kt index 6350f6e..56b6066 100644 --- a/app/src/main/java/com/otus/dihomework/MainNavigation.kt +++ b/app/src/main/java/com/otus/dihomework/MainNavigation.kt @@ -26,11 +26,12 @@ sealed class Screen(val route: String, val titleRes: Int, val icon: ImageVector) @OptIn(ExperimentalMaterial3Api::class) @Composable -fun MainNavigation() { +fun MainNavigation(modifier: Modifier = Modifier) { val navController = rememberNavController() val screens = listOf(Screen.Products, Screen.Favorites) Scaffold( + modifier = modifier, topBar = { val navBackStackEntry by navController.currentBackStackEntryAsState() val currentDestination = navBackStackEntry?.destination diff --git a/app/src/main/java/com/otus/dihomework/ProductsApplication.kt b/app/src/main/java/com/otus/dihomework/ProductsApplication.kt index 2e3799d..34628ec 100644 --- a/app/src/main/java/com/otus/dihomework/ProductsApplication.kt +++ b/app/src/main/java/com/otus/dihomework/ProductsApplication.kt @@ -1,10 +1,25 @@ package com.otus.dihomework import android.app.Application +import com.otus.dihomework.common.di.Dependencies +import com.otus.dihomework.common.di.DependenciesProvider +import com.otus.dihomework.di.AppComponent +import com.otus.dihomework.di.DaggerAppComponent + +/** + * Класс приложения, инициализирующий Dagger граф. + */ +class ProductsApplication : Application(), DependenciesProvider { + + lateinit var appComponent: AppComponent + private set -class ProductsApplication : Application() { override fun onCreate() { super.onCreate() - ServiceLocator.init(this) + appComponent = DaggerAppComponent.factory().create(this) + } + + override fun getDependencies(): Dependencies { + return appComponent } } diff --git a/app/src/main/java/com/otus/dihomework/ServiceLocator.kt b/app/src/main/java/com/otus/dihomework/ServiceLocator.kt deleted file mode 100644 index 1a56c01..0000000 --- a/app/src/main/java/com/otus/dihomework/ServiceLocator.kt +++ /dev/null @@ -1,106 +0,0 @@ -package com.otus.dihomework - -import android.content.Context -import com.google.gson.GsonBuilder -import com.otus.dihomework.common.data.FavoritesRepositoryImpl -import com.otus.dihomework.common.data.ProductApiService -import com.otus.dihomework.common.data.ProductDomainMapper -import com.otus.dihomework.common.data.ProductRemoteDataSource -import com.otus.dihomework.common.data.ProductRepositoryImpl -import com.otus.dihomework.common.domain_api.ConsumeFavoritesUseCase -import com.otus.dihomework.common.domain_api.ConsumeProductsUseCase -import com.otus.dihomework.common.domain_api.ToggleFavoriteUseCase -import com.otus.dihomework.common.domain_impl.ConsumeFavoritesUseCaseImpl -import com.otus.dihomework.common.domain_impl.ConsumeProductsUseCaseImpl -import com.otus.dihomework.common.domain_impl.FavoritesRepository -import com.otus.dihomework.common.domain_impl.ProductRepository -import com.otus.dihomework.common.domain_impl.ToggleFavoriteUseCaseImpl -import com.otus.dihomework.common.util.PriceFormatter -import com.otus.dihomework.features.favorites.FavoritesStateFactory -import com.otus.dihomework.features.products.ProductsStateFactory -import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor -import retrofit2.Retrofit -import retrofit2.converter.gson.GsonConverterFactory -import java.util.concurrent.TimeUnit - -object ServiceLocator { - - private var applicationContext: Context? = null - - fun init(context: Context) { - applicationContext = context.applicationContext - } - - private val httpLoggingInterceptor: HttpLoggingInterceptor by lazy { - HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.BODY - } - } - - private val okHttpClient: OkHttpClient by lazy { - OkHttpClient.Builder() - .addInterceptor(httpLoggingInterceptor) - .connectTimeout(30, TimeUnit.SECONDS) - .readTimeout(30, TimeUnit.SECONDS) - .build() - } - - private val retrofit: Retrofit by lazy { - Retrofit.Builder() - .baseUrl("https://otus-android.github.io/") - .client(okHttpClient) - .addConverterFactory(GsonConverterFactory.create(GsonBuilder().create())) - .build() - } - - private val _productApiService: ProductApiService by lazy { - retrofit.create(ProductApiService::class.java) - } - - fun getProductApiService(): ProductApiService { - return _productApiService - } - - fun getProductDomainMapper(): ProductDomainMapper { - return ProductDomainMapper() - } - - fun getProductRemoteDataSource(): ProductRemoteDataSource { - return ProductRemoteDataSource() - } - - fun getFavoritesRepository(): FavoritesRepository { - val context = applicationContext - checkNotNull(context) { "ServiceLocator not initialized! Call init() first." } - return FavoritesRepositoryImpl(context) - } - - fun getProductRepository(): ProductRepository { - return ProductRepositoryImpl() - } - - fun getConsumeProductsUseCase(): ConsumeProductsUseCase { - return ConsumeProductsUseCaseImpl() - } - - fun getConsumeFavoritesUseCase(): ConsumeFavoritesUseCase { - return ConsumeFavoritesUseCaseImpl() - } - - fun getToggleFavoriteUseCase(): ToggleFavoriteUseCase { - return ToggleFavoriteUseCaseImpl() - } - - fun getPriceFormatter(): PriceFormatter { - return PriceFormatter() - } - - fun getProductsStateFactory(): ProductsStateFactory { - return ProductsStateFactory() - } - - fun getFavoritesStateFactory(): FavoritesStateFactory { - return FavoritesStateFactory() - } -} diff --git a/app/src/main/java/com/otus/dihomework/common/data/FavoritesRepositoryImpl.kt b/app/src/main/java/com/otus/dihomework/common/data/FavoritesRepositoryImpl.kt index ad70211..ccf7336 100644 --- a/app/src/main/java/com/otus/dihomework/common/data/FavoritesRepositoryImpl.kt +++ b/app/src/main/java/com/otus/dihomework/common/data/FavoritesRepositoryImpl.kt @@ -9,10 +9,14 @@ import androidx.datastore.preferences.preferencesDataStore import com.otus.dihomework.common.domain_impl.FavoritesRepository import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import javax.inject.Inject private val Context.dataStore: DataStore by preferencesDataStore(name = "favorites") -class FavoritesRepositoryImpl( +/** + * Реализация репозитория избранного с использованием DataStore. + */ +class FavoritesRepositoryImpl @Inject constructor( private val context: Context ) : FavoritesRepository { diff --git a/app/src/main/java/com/otus/dihomework/common/data/ProductDomainMapper.kt b/app/src/main/java/com/otus/dihomework/common/data/ProductDomainMapper.kt index 762959d..9e032b4 100644 --- a/app/src/main/java/com/otus/dihomework/common/data/ProductDomainMapper.kt +++ b/app/src/main/java/com/otus/dihomework/common/data/ProductDomainMapper.kt @@ -1,8 +1,12 @@ package com.otus.dihomework.common.data import com.otus.dihomework.common.domain_api.Product +import javax.inject.Inject -class ProductDomainMapper() { +/** + * Маппер для преобразования DTO продуктов в доменные модели. + */ +class ProductDomainMapper @Inject constructor() { fun fromDto(dto: ProductDto): Product { return Product( diff --git a/app/src/main/java/com/otus/dihomework/common/data/ProductRemoteDataSource.kt b/app/src/main/java/com/otus/dihomework/common/data/ProductRemoteDataSource.kt index ae9935f..00829dc 100644 --- a/app/src/main/java/com/otus/dihomework/common/data/ProductRemoteDataSource.kt +++ b/app/src/main/java/com/otus/dihomework/common/data/ProductRemoteDataSource.kt @@ -1,11 +1,15 @@ package com.otus.dihomework.common.data -import com.otus.dihomework.ServiceLocator import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow +import javax.inject.Inject -class ProductRemoteDataSource() { - private val apiService = ServiceLocator.getProductApiService() +/** + * Удаленный источник данных для получения продуктов из API. + */ +class ProductRemoteDataSource @Inject constructor( + private val apiService: ProductApiService +) { fun consumeProducts(): Flow> = flow { emit(apiService.getProducts()) diff --git a/app/src/main/java/com/otus/dihomework/common/data/ProductRepositoryImpl.kt b/app/src/main/java/com/otus/dihomework/common/data/ProductRepositoryImpl.kt index 7027dca..b048035 100644 --- a/app/src/main/java/com/otus/dihomework/common/data/ProductRepositoryImpl.kt +++ b/app/src/main/java/com/otus/dihomework/common/data/ProductRepositoryImpl.kt @@ -1,15 +1,18 @@ package com.otus.dihomework.common.data -import com.otus.dihomework.ServiceLocator import com.otus.dihomework.common.domain_impl.ProductRepository import com.otus.dihomework.common.domain_api.Product import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import javax.inject.Inject -class ProductRepositoryImpl() : ProductRepository { - - private val remoteDataSource = ServiceLocator.getProductRemoteDataSource() - private val mapper = ServiceLocator.getProductDomainMapper() +/** + * Реализация репозитория продуктов. + */ +class ProductRepositoryImpl @Inject constructor( + private val remoteDataSource: ProductRemoteDataSource, + private val mapper: ProductDomainMapper +) : ProductRepository { override fun consumeProducts(): Flow> { return remoteDataSource.consumeProducts() diff --git a/app/src/main/java/com/otus/dihomework/common/domain_impl/ConsumeFavoritesUseCaseImpl.kt b/app/src/main/java/com/otus/dihomework/common/domain_impl/ConsumeFavoritesUseCaseImpl.kt index bc9b71f..d272479 100644 --- a/app/src/main/java/com/otus/dihomework/common/domain_impl/ConsumeFavoritesUseCaseImpl.kt +++ b/app/src/main/java/com/otus/dihomework/common/domain_impl/ConsumeFavoritesUseCaseImpl.kt @@ -1,15 +1,18 @@ package com.otus.dihomework.common.domain_impl -import com.otus.dihomework.ServiceLocator import com.otus.dihomework.common.domain_api.ConsumeFavoritesUseCase import com.otus.dihomework.common.domain_api.ProductWithFavorite import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import javax.inject.Inject -class ConsumeFavoritesUseCaseImpl() : ConsumeFavoritesUseCase { - - private val productRepository = ServiceLocator.getProductRepository() - private val favoritesRepository = ServiceLocator.getFavoritesRepository() +/** + * Реализация сценария получения списка избранных продуктов. + */ +class ConsumeFavoritesUseCaseImpl @Inject constructor( + private val productRepository: ProductRepository, + private val favoritesRepository: FavoritesRepository +) : ConsumeFavoritesUseCase { override fun invoke(): Flow> { return combine( diff --git a/app/src/main/java/com/otus/dihomework/common/domain_impl/ConsumeProductsUseCaseImpl.kt b/app/src/main/java/com/otus/dihomework/common/domain_impl/ConsumeProductsUseCaseImpl.kt index 6314f50..7d31c8b 100644 --- a/app/src/main/java/com/otus/dihomework/common/domain_impl/ConsumeProductsUseCaseImpl.kt +++ b/app/src/main/java/com/otus/dihomework/common/domain_impl/ConsumeProductsUseCaseImpl.kt @@ -1,16 +1,19 @@ package com.otus.dihomework.common.domain_impl -import com.otus.dihomework.ServiceLocator import com.otus.dihomework.common.domain_api.ConsumeProductsUseCase import com.otus.dihomework.common.domain_api.ProductWithFavorite import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map +import javax.inject.Inject -class ConsumeProductsUseCaseImpl() : ConsumeProductsUseCase { - - private val productRepository = ServiceLocator.getProductRepository() - private val favoritesRepository = ServiceLocator.getFavoritesRepository() +/** + * Реализация сценария получения списка продуктов с информацией об избранном. + */ +class ConsumeProductsUseCaseImpl @Inject constructor( + private val productRepository: ProductRepository, + private val favoritesRepository: FavoritesRepository +) : ConsumeProductsUseCase { override fun invoke(): Flow> { return combine( diff --git a/app/src/main/java/com/otus/dihomework/common/domain_impl/ToggleFavoriteUseCaseImpl.kt b/app/src/main/java/com/otus/dihomework/common/domain_impl/ToggleFavoriteUseCaseImpl.kt index 4b5c8b7..bd687d3 100644 --- a/app/src/main/java/com/otus/dihomework/common/domain_impl/ToggleFavoriteUseCaseImpl.kt +++ b/app/src/main/java/com/otus/dihomework/common/domain_impl/ToggleFavoriteUseCaseImpl.kt @@ -1,11 +1,14 @@ package com.otus.dihomework.common.domain_impl -import com.otus.dihomework.ServiceLocator import com.otus.dihomework.common.domain_api.ToggleFavoriteUseCase +import javax.inject.Inject -class ToggleFavoriteUseCaseImpl() : ToggleFavoriteUseCase { - - private val favoritesRepository = ServiceLocator.getFavoritesRepository() +/** + * Реализация сценария добавления/удаления продукта из избранного. + */ +class ToggleFavoriteUseCaseImpl @Inject constructor( + private val favoritesRepository: FavoritesRepository +) : ToggleFavoriteUseCase { override suspend fun invoke(productId: String, isFavorite: Boolean) { if (isFavorite) { diff --git a/app/src/main/java/com/otus/dihomework/common/util/PriceFormatter.kt b/app/src/main/java/com/otus/dihomework/common/util/PriceFormatter.kt index d13a12f..2fec066 100644 --- a/app/src/main/java/com/otus/dihomework/common/util/PriceFormatter.kt +++ b/app/src/main/java/com/otus/dihomework/common/util/PriceFormatter.kt @@ -1,6 +1,11 @@ package com.otus.dihomework.common.util -class PriceFormatter() { +import javax.inject.Inject + +/** + * Утилита для форматирования цены продукта. + */ +class PriceFormatter @Inject constructor() { fun format(price: Double): String { return "${price.toInt()} ₽" diff --git a/app/src/main/java/com/otus/dihomework/di/AppComponent.kt b/app/src/main/java/com/otus/dihomework/di/AppComponent.kt new file mode 100644 index 0000000..6e967c9 --- /dev/null +++ b/app/src/main/java/com/otus/dihomework/di/AppComponent.kt @@ -0,0 +1,39 @@ +package com.otus.dihomework.di + +import android.content.Context +import com.otus.dihomework.features.favorites.FavoritesComponent +import dagger.BindsInstance +import dagger.Component +import javax.inject.Singleton + +/** + * Основной компонент приложения. + * Управляет жизненным циклом зависимостей, которые должны существовать в единственном экземпляре. + */ +@Singleton +@Component( + modules = [ + NetworkModule::class, + DataModule::class, + DomainModule::class, + SubcomponentsModule::class + ] +) +interface AppComponent : ProductsDependencies { + + /** + * Предоставляет фабрику для создания сабкомпонента избранного. + */ + fun favoritesComponent(): FavoritesComponent.Factory + + /** + * Фабрика для создания экземпляра AppComponent. + */ + @Component.Factory + interface Factory { + /** + * Создает экземпляр AppComponent, передавая контекст приложения. + */ + fun create(@BindsInstance context: Context): AppComponent + } +} diff --git a/app/src/main/java/com/otus/dihomework/di/DataModule.kt b/app/src/main/java/com/otus/dihomework/di/DataModule.kt new file mode 100644 index 0000000..c28ee39 --- /dev/null +++ b/app/src/main/java/com/otus/dihomework/di/DataModule.kt @@ -0,0 +1,30 @@ +package com.otus.dihomework.di + +import com.otus.dihomework.common.data.FavoritesRepositoryImpl +import com.otus.dihomework.common.data.ProductRepositoryImpl +import com.otus.dihomework.common.domain_impl.FavoritesRepository +import com.otus.dihomework.common.domain_impl.ProductRepository +import dagger.Binds +import dagger.Module +import javax.inject.Singleton + +/** + * Модуль для предоставления зависимостей уровня данных. + */ +@Module +interface DataModule { + + /** + * Связывает реализацию репозитория продуктов с его интерфейсом. + */ + @Binds + @Singleton + fun bindProductRepository(impl: ProductRepositoryImpl): ProductRepository + + /** + * Связывает реализацию репозитория избранного с его интерфейсом. + */ + @Binds + @Singleton + fun bindFavoritesRepository(impl: FavoritesRepositoryImpl): FavoritesRepository +} diff --git a/app/src/main/java/com/otus/dihomework/di/DomainModule.kt b/app/src/main/java/com/otus/dihomework/di/DomainModule.kt new file mode 100644 index 0000000..bbc2736 --- /dev/null +++ b/app/src/main/java/com/otus/dihomework/di/DomainModule.kt @@ -0,0 +1,35 @@ +package com.otus.dihomework.di + +import com.otus.dihomework.common.domain_api.ConsumeFavoritesUseCase +import com.otus.dihomework.common.domain_api.ConsumeProductsUseCase +import com.otus.dihomework.common.domain_api.ToggleFavoriteUseCase +import com.otus.dihomework.common.domain_impl.ConsumeFavoritesUseCaseImpl +import com.otus.dihomework.common.domain_impl.ConsumeProductsUseCaseImpl +import com.otus.dihomework.common.domain_impl.ToggleFavoriteUseCaseImpl +import dagger.Binds +import dagger.Module + +/** + * Модуль для предоставления зависимостей уровня домена (бизнес-логики). + */ +@Module +interface DomainModule { + + /** + * Связывает реализацию сценария получения продуктов с его интерфейсом. + */ + @Binds + fun bindConsumeProductsUseCase(impl: ConsumeProductsUseCaseImpl): ConsumeProductsUseCase + + /** + * Связывает реализацию сценария получения избранного с его интерфейсом. + */ + @Binds + fun bindConsumeFavoritesUseCase(impl: ConsumeFavoritesUseCaseImpl): ConsumeFavoritesUseCase + + /** + * Связывает реализацию сценария переключения избранного с его интерфейсом. + */ + @Binds + fun bindToggleFavoriteUseCase(impl: ToggleFavoriteUseCaseImpl): ToggleFavoriteUseCase +} diff --git a/app/src/main/java/com/otus/dihomework/di/NetworkModule.kt b/app/src/main/java/com/otus/dihomework/di/NetworkModule.kt new file mode 100644 index 0000000..3fb21a9 --- /dev/null +++ b/app/src/main/java/com/otus/dihomework/di/NetworkModule.kt @@ -0,0 +1,75 @@ +package com.otus.dihomework.di + +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import com.otus.dihomework.common.data.ProductApiService +import dagger.Module +import dagger.Provides +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import java.util.concurrent.TimeUnit +import javax.inject.Singleton + +/** + * Модуль для предоставления сетевых зависимостей. + */ +@Module +class NetworkModule { + + /** + * Предоставляет интерцептор для логирования HTTP запросов. + */ + @Provides + @Singleton + fun provideLoggingInterceptor(): HttpLoggingInterceptor { + return HttpLoggingInterceptor().apply { + level = HttpLoggingInterceptor.Level.BODY + } + } + + /** + * Предоставляет OkHttpClient для выполнения сетевых запросов. + */ + @Provides + @Singleton + fun provideOkHttpClient(loggingInterceptor: HttpLoggingInterceptor): OkHttpClient { + return OkHttpClient.Builder() + .addInterceptor(loggingInterceptor) + .connectTimeout(30, TimeUnit.SECONDS) + .readTimeout(30, TimeUnit.SECONDS) + .build() + } + + /** + * Предоставляет Gson для сериализации/десериализации JSON. + */ + @Provides + @Singleton + fun provideGson(): Gson { + return GsonBuilder().create() + } + + /** + * Предоставляет Retrofit для создания API сервисов. + */ + @Provides + @Singleton + fun provideRetrofit(okHttpClient: OkHttpClient, gson: Gson): Retrofit { + return Retrofit.Builder() + .baseUrl("https://otus-android.github.io/") + .client(okHttpClient) + .addConverterFactory(GsonConverterFactory.create(gson)) + .build() + } + + /** + * Предоставляет ProductApiService для работы с продуктами через сеть. + */ + @Provides + @Singleton + fun provideProductApiService(retrofit: Retrofit): ProductApiService { + return retrofit.create(ProductApiService::class.java) + } +} diff --git a/app/src/main/java/com/otus/dihomework/di/ProductsDependencies.kt b/app/src/main/java/com/otus/dihomework/di/ProductsDependencies.kt new file mode 100644 index 0000000..4c9e41d --- /dev/null +++ b/app/src/main/java/com/otus/dihomework/di/ProductsDependencies.kt @@ -0,0 +1,26 @@ +package com.otus.dihomework.di + +import com.otus.dihomework.common.di.Dependencies +import com.otus.dihomework.common.domain_api.ConsumeProductsUseCase +import com.otus.dihomework.common.domain_api.ToggleFavoriteUseCase +import com.otus.dihomework.features.products.ProductsStateFactory + +/** + * Зависимости, необходимые для работы фичи Products. + */ +interface ProductsDependencies : Dependencies { + /** + * Сценарий получения продуктов. + */ + fun consumeProductsUseCase(): ConsumeProductsUseCase + + /** + * Сценарий переключения избранного. + */ + fun toggleFavoriteUseCase(): ToggleFavoriteUseCase + + /** + * Фабрика состояния для экрана продуктов. + */ + fun productsStateFactory(): ProductsStateFactory +} diff --git a/app/src/main/java/com/otus/dihomework/di/SubcomponentsModule.kt b/app/src/main/java/com/otus/dihomework/di/SubcomponentsModule.kt new file mode 100644 index 0000000..7fe10c0 --- /dev/null +++ b/app/src/main/java/com/otus/dihomework/di/SubcomponentsModule.kt @@ -0,0 +1,10 @@ +package com.otus.dihomework.di + +import com.otus.dihomework.features.favorites.FavoritesComponent +import dagger.Module + +/** + * Модуль для подключения сабкомпонентов к основному графу AppComponent. + */ +@Module(subcomponents = [FavoritesComponent::class]) +class SubcomponentsModule diff --git a/app/src/main/java/com/otus/dihomework/features/favorites/FavoritesComponent.kt b/app/src/main/java/com/otus/dihomework/features/favorites/FavoritesComponent.kt new file mode 100644 index 0000000..89975d0 --- /dev/null +++ b/app/src/main/java/com/otus/dihomework/features/favorites/FavoritesComponent.kt @@ -0,0 +1,28 @@ +package com.otus.dihomework.features.favorites + +import com.otus.dihomework.common.di.FeatureScope +import dagger.Subcomponent + +/** + * Сабкомпонент для фичи Favorites. + */ +@FeatureScope +@Subcomponent +interface FavoritesComponent { + + /** + * Предоставляет фабрику для создания FavoritesViewModel. + */ + fun viewModelFactory(): FavoritesViewModelFactory + + /** + * Фабрика для создания экземпляра FavoritesComponent. + */ + @Subcomponent.Factory + interface Factory { + /** + * Создает экземпляр FavoritesComponent. + */ + fun create(): FavoritesComponent + } +} diff --git a/app/src/main/java/com/otus/dihomework/features/favorites/FavoritesStateFactory.kt b/app/src/main/java/com/otus/dihomework/features/favorites/FavoritesStateFactory.kt index d2569a4..2fc420f 100644 --- a/app/src/main/java/com/otus/dihomework/features/favorites/FavoritesStateFactory.kt +++ b/app/src/main/java/com/otus/dihomework/features/favorites/FavoritesStateFactory.kt @@ -1,10 +1,15 @@ package com.otus.dihomework.features.favorites -import com.otus.dihomework.ServiceLocator import com.otus.dihomework.common.domain_api.ProductWithFavorite +import com.otus.dihomework.common.util.PriceFormatter +import javax.inject.Inject -class FavoritesStateFactory() { - private val priceFormatter = ServiceLocator.getPriceFormatter() +/** + * Фабрика для создания состояния экрана избранного. + */ +class FavoritesStateFactory @Inject constructor( + private val priceFormatter: PriceFormatter +) { fun create(favorites: List): List { return favorites.map { createFavoriteState(it) } diff --git a/app/src/main/java/com/otus/dihomework/features/favorites/FavoritesViewModel.kt b/app/src/main/java/com/otus/dihomework/features/favorites/FavoritesViewModel.kt index 9d41c4b..8b065f5 100644 --- a/app/src/main/java/com/otus/dihomework/features/favorites/FavoritesViewModel.kt +++ b/app/src/main/java/com/otus/dihomework/features/favorites/FavoritesViewModel.kt @@ -2,7 +2,8 @@ package com.otus.dihomework.features.favorites import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.otus.dihomework.ServiceLocator +import com.otus.dihomework.common.domain_api.ConsumeFavoritesUseCase +import com.otus.dihomework.common.domain_api.ToggleFavoriteUseCase import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -12,12 +13,16 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import javax.inject.Inject -class FavoritesViewModel() : ViewModel() { - - private val consumeFavoritesUseCase = ServiceLocator.getConsumeFavoritesUseCase() - private val toggleFavoriteUseCase = ServiceLocator.getToggleFavoriteUseCase() - private val favoritesStateFactory = ServiceLocator.getFavoritesStateFactory() +/** + * ViewModel для экрана списка избранного. + */ +class FavoritesViewModel @Inject constructor( + private val consumeFavoritesUseCase: ConsumeFavoritesUseCase, + private val toggleFavoriteUseCase: ToggleFavoriteUseCase, + private val favoritesStateFactory: FavoritesStateFactory +) : ViewModel() { private val _state = MutableStateFlow(FavoritesScreenState()) val state: StateFlow = _state.asStateFlow() diff --git a/app/src/main/java/com/otus/dihomework/features/favorites/FavoritesViewModelFactory.kt b/app/src/main/java/com/otus/dihomework/features/favorites/FavoritesViewModelFactory.kt index a30f8fc..b87c786 100644 --- a/app/src/main/java/com/otus/dihomework/features/favorites/FavoritesViewModelFactory.kt +++ b/app/src/main/java/com/otus/dihomework/features/favorites/FavoritesViewModelFactory.kt @@ -2,12 +2,19 @@ package com.otus.dihomework.features.favorites import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import javax.inject.Inject +import javax.inject.Provider -class FavoritesViewModelFactory() : ViewModelProvider.Factory { +/** + * Фабрика для создания FavoritesViewModel с внедрением зависимостей через Provider. + */ +class FavoritesViewModelFactory @Inject constructor( + private val viewModelProvider: Provider +) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T { require(modelClass == FavoritesViewModel::class.java) - return FavoritesViewModel() as T + return viewModelProvider.get() as T } } diff --git a/app/src/main/java/com/otus/dihomework/features/favorites/ui/FavoritesScreen.kt b/app/src/main/java/com/otus/dihomework/features/favorites/ui/FavoritesScreen.kt index 6c9c79b..4af3d48 100644 --- a/app/src/main/java/com/otus/dihomework/features/favorites/ui/FavoritesScreen.kt +++ b/app/src/main/java/com/otus/dihomework/features/favorites/ui/FavoritesScreen.kt @@ -10,27 +10,39 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import com.otus.dihomework.ProductsApplication import com.otus.dihomework.R import com.otus.dihomework.features.favorites.FavoritesScreenState import com.otus.dihomework.features.favorites.FavoritesViewModel -import com.otus.dihomework.features.favorites.FavoritesViewModelFactory +/** + * Основной Composable-компонент экрана избранного, отвечающий за получение сабкомпонента + * из AppComponent и инициализацию ViewModel. + */ @Composable fun FavoritesScreenContent( modifier: Modifier = Modifier ) { + val context = LocalContext.current + val favoritesComponent = remember { + (context.applicationContext as ProductsApplication).appComponent.favoritesComponent() + .create() + } + val viewModel: FavoritesViewModel = viewModel( - factory = FavoritesViewModelFactory() + factory = favoritesComponent.viewModelFactory() ) - val state by viewModel.state.collectAsState() + val state by viewModel.state.collectAsStateWithLifecycle() FavoritesScreen( state = state, diff --git a/app/src/main/java/com/otus/dihomework/features/products/ProductsComponent.kt b/app/src/main/java/com/otus/dihomework/features/products/ProductsComponent.kt new file mode 100644 index 0000000..e1880e9 --- /dev/null +++ b/app/src/main/java/com/otus/dihomework/features/products/ProductsComponent.kt @@ -0,0 +1,30 @@ +package com.otus.dihomework.features.products + +import com.otus.dihomework.common.di.FeatureScope +import com.otus.dihomework.di.ProductsDependencies +import dagger.Component + +/** + * Компонент для фичи Products. + * Использует Component Dependencies для получения необходимых зависимостей. + */ +@FeatureScope +@Component(dependencies = [ProductsDependencies::class]) +interface ProductsComponent { + + /** + * Предоставляет фабрику для создания ProductsViewModel. + */ + fun viewModelFactory(): ProductsViewModelFactory + + /** + * Фабрика для создания экземпляра ProductsComponent. + */ + @Component.Factory + interface Factory { + /** + * Создает экземпляр ProductsComponent на основе переданных зависимостей. + */ + fun create(dependencies: ProductsDependencies): ProductsComponent + } +} diff --git a/app/src/main/java/com/otus/dihomework/features/products/ProductsStateFactory.kt b/app/src/main/java/com/otus/dihomework/features/products/ProductsStateFactory.kt index d78f6e7..c0bbf6f 100644 --- a/app/src/main/java/com/otus/dihomework/features/products/ProductsStateFactory.kt +++ b/app/src/main/java/com/otus/dihomework/features/products/ProductsStateFactory.kt @@ -1,10 +1,15 @@ package com.otus.dihomework.features.products -import com.otus.dihomework.ServiceLocator import com.otus.dihomework.common.domain_api.ProductWithFavorite +import com.otus.dihomework.common.util.PriceFormatter +import javax.inject.Inject -class ProductsStateFactory() { - private val priceFormatter = ServiceLocator.getPriceFormatter() +/** + * Фабрика для создания состояния экрана списка продуктов. + */ +class ProductsStateFactory @Inject constructor( + private val priceFormatter: PriceFormatter +) { fun create(products: List): List { return products diff --git a/app/src/main/java/com/otus/dihomework/features/products/ProductsViewModel.kt b/app/src/main/java/com/otus/dihomework/features/products/ProductsViewModel.kt index ad2ce7d..bdbd24f 100644 --- a/app/src/main/java/com/otus/dihomework/features/products/ProductsViewModel.kt +++ b/app/src/main/java/com/otus/dihomework/features/products/ProductsViewModel.kt @@ -2,7 +2,8 @@ package com.otus.dihomework.features.products import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.otus.dihomework.ServiceLocator +import com.otus.dihomework.common.domain_api.ConsumeProductsUseCase +import com.otus.dihomework.common.domain_api.ToggleFavoriteUseCase import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow @@ -12,12 +13,16 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import javax.inject.Inject -class ProductsViewModel() : ViewModel() { - - private val consumeProductsUseCase = ServiceLocator.getConsumeProductsUseCase() - private val toggleFavoriteUseCase = ServiceLocator.getToggleFavoriteUseCase() - private val productsStateFactory = ServiceLocator.getProductsStateFactory() +/** + * ViewModel для экрана списка продуктов. + */ +class ProductsViewModel @Inject constructor( + private val consumeProductsUseCase: ConsumeProductsUseCase, + private val toggleFavoriteUseCase: ToggleFavoriteUseCase, + private val productsStateFactory: ProductsStateFactory +) : ViewModel() { private val _state = MutableStateFlow(ProductsScreenState()) val state: StateFlow = _state.asStateFlow() diff --git a/app/src/main/java/com/otus/dihomework/features/products/ProductsViewModelFactory.kt b/app/src/main/java/com/otus/dihomework/features/products/ProductsViewModelFactory.kt index 82defc9..daaf5c3 100644 --- a/app/src/main/java/com/otus/dihomework/features/products/ProductsViewModelFactory.kt +++ b/app/src/main/java/com/otus/dihomework/features/products/ProductsViewModelFactory.kt @@ -2,12 +2,19 @@ package com.otus.dihomework.features.products import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import javax.inject.Inject +import javax.inject.Provider -class ProductsViewModelFactory() : ViewModelProvider.Factory { +/** + * Фабрика для создания ProductsViewModel с внедрением зависимостей через Provider. + */ +class ProductsViewModelFactory @Inject constructor( + private val viewModelProvider: Provider +) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T { require(modelClass == ProductsViewModel::class.java) - return ProductsViewModel() as T + return viewModelProvider.get() as T } } diff --git a/app/src/main/java/com/otus/dihomework/features/products/ui/ProductsScreen.kt b/app/src/main/java/com/otus/dihomework/features/products/ui/ProductsScreen.kt index 0072b10..a9fba2d 100644 --- a/app/src/main/java/com/otus/dihomework/features/products/ui/ProductsScreen.kt +++ b/app/src/main/java/com/otus/dihomework/features/products/ui/ProductsScreen.kt @@ -11,25 +11,37 @@ import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import com.otus.dihomework.common.di.findDependencies +import com.otus.dihomework.features.products.DaggerProductsComponent import com.otus.dihomework.features.products.ProductsScreenState import com.otus.dihomework.features.products.ProductsViewModel -import com.otus.dihomework.features.products.ProductsViewModelFactory +/** + * Основной Composable-компонент экрана продуктов, отвечающий за создание DI-графа + * и инициализацию ViewModel. + */ @Composable fun ProductsScreenContent( modifier: Modifier = Modifier ) { + val context = LocalContext.current + val productsComponent = remember { + DaggerProductsComponent.factory().create(context.findDependencies()) + } + val viewModel: ProductsViewModel = viewModel( - factory = ProductsViewModelFactory() + factory = productsComponent.viewModelFactory() ) - val state by viewModel.state.collectAsState() + val state by viewModel.state.collectAsStateWithLifecycle() ProductsScreen( state = state, diff --git a/app/src/main/java/com/otus/dihomework/ui/theme/Theme.kt b/app/src/main/java/com/otus/dihomework/ui/theme/Theme.kt index b49e3e2..1910600 100644 --- a/app/src/main/java/com/otus/dihomework/ui/theme/Theme.kt +++ b/app/src/main/java/com/otus/dihomework/ui/theme/Theme.kt @@ -3,12 +3,14 @@ package com.otus.dihomework.ui.theme import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.darkColorScheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.Modifier private val DarkColorScheme = darkColorScheme( primary = Purple80, @@ -24,6 +26,7 @@ private val LightColorScheme = lightColorScheme( @Composable fun DIHomeworkTheme( + modifier: Modifier = Modifier, darkTheme: Boolean = isSystemInDarkTheme(), dynamicColor: Boolean = true, content: @Composable () -> Unit @@ -41,6 +44,11 @@ fun DIHomeworkTheme( MaterialTheme( colorScheme = colorScheme, typography = Typography, - content = content + content = { + Surface( + modifier = modifier, + content = content + ) + } ) } diff --git a/common/di/src/main/java/com/otus/dihomework/common/di/Dependencies.kt b/common/di/src/main/java/com/otus/dihomework/common/di/Dependencies.kt new file mode 100644 index 0000000..e8a3569 --- /dev/null +++ b/common/di/src/main/java/com/otus/dihomework/common/di/Dependencies.kt @@ -0,0 +1,27 @@ +package com.otus.dihomework.common.di + +import android.content.Context + +/** + * Базовый интерфейс для зависимостей компонентов. + * Используется как маркерный интерфейс для передачи зависимостей между компонентами. + */ +interface Dependencies + +/** + * Интерфейс, который должен реализовать класс Application для предоставления зависимостей. + */ +interface DependenciesProvider { + /** + * Возвращает зависимости для указанного типа. + */ + fun getDependencies(): Dependencies +} + +/** + * Расширение для Context для поиска зависимостей в приложении. + * Используется для получения зависимостей в UI слое (Activity/Fragment/Compose). + */ +inline fun Context.findDependencies(): T { + return (applicationContext as DependenciesProvider).getDependencies() as T +} diff --git a/di_graph.mmd b/di_graph.mmd new file mode 100644 index 0000000..7de0a6c --- /dev/null +++ b/di_graph.mmd @@ -0,0 +1,25 @@ +graph TD + App[ProductsApplication] --> AppComponent + + subgraph AppComponent [AppComponent Singleton] + NM[NetworkModule] + DM[DataModule] + DomM[DomainModule] + SM[SubcomponentsModule] + end + + AppComponent --> ProductsDependencies + + subgraph ProductsFeature [Products Feature] + ProductsDependencies -.-> ProductsComponent["ProductsComponent FeatureScope"] + ProductsComponent --> PVF[ProductsViewModelFactory] + PVF --> PVM[ProductsViewModel] + end + + subgraph FavoritesFeature [Favorites Feature] + AppComponent -- subcomponent --> FavoritesComponent["FavoritesComponent FeatureScope"] + FavoritesComponent --> FVF[FavoritesViewModelFactory] + FVF --> FVM[FavoritesViewModel] + end + + AppComponent -.-> FavoritesFeature diff --git a/img/di_graph.png b/img/di_graph.png new file mode 100644 index 0000000..0b82d23 Binary files /dev/null and b/img/di_graph.png differ diff --git a/task_result.md b/task_result.md new file mode 100644 index 0000000..3f31dad --- /dev/null +++ b/task_result.md @@ -0,0 +1,42 @@ +# Результат выполнения задания: Внедрение Dagger 2 + +Приложение было переведено с паттерна Service Locator на Dependency Injection с использованием Dagger 2. + +### Структура DI-графа: + +![DI Graph](img/di_graph.png) + +[Посмотреть диаграмму в текстовом формате](di_graph.mmd) + +### Выполненные работы: + +1. **Создан AppComponent (Singleton):** + * Является основным компонентом приложения. + * Предоставляет сетевые зависимости (Retrofit, OkHttp, ProductApiService), репозитории и Use Case'ы. + * Реализует интерфейс `ProductsDependencies`. + * Включает `SubcomponentsModule` для работы с сабкомпонентами. + * Инициализируется в `ProductsApplication`. + +2. **Реализована фича Products (Component Dependencies):** + * Создан `ProductsComponent` с областью видимости `@FeatureScope`. + * Использует `ProductsDependencies` как внешнюю зависимость. + * Предоставляет `ProductsViewModelFactory`. + +3. **Реализована фича Favorites (Subcomponent):** + * Создан `FavoritesComponent` как сабкомпонент `AppComponent`. + * Имеет область видимости `@FeatureScope`. + * Предоставляет `FavoritesViewModelFactory`. + +4. **Рефакторинг кода:** + * `ServiceLocator` полностью удален. + * Все репозитории, Use Case'ы и фабрики состояний переведены на использование конструкторной инъекции (`@Inject`). + * ViewModels теперь получают свои зависимости через конструктор. + * Компоненты в Compose-экранах создаются с использованием `remember` и вспомогательных функций поиска зависимостей. + * Сбор состояния во ViewModels в Composable-функциях переведен на использование `collectAsStateWithLifecycle()` для безопасной работы с жизненным циклом. + * Исправлено использование `modifier` в Composable-функциях для корректной передачи параметров оформления. + +5. **Инфраструктура DI:** + * В модуле `common:di` добавлены интерфейсы `Dependencies`, `DependenciesProvider` и расширение `findDependencies`. + * Используется аннотация `@FeatureScope` для управления временем жизни компонентов фич. + +Все новые классы и функции снабжены документацией на русском языке. Проект успешно собирается.