diff --git a/app/build.gradle b/app/build.gradle index 0e043fc..038d9f5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -14,6 +14,9 @@ apply from: 'jacoco.gradle' apply from: 'lint.gradle' apply from: 'build-scripts.gradle' +apply plugin: 'kotlin-kapt' +apply plugin: 'dagger.hilt.android.plugin' + repositories { mavenCentral() maven { url 'https://maven.google.com' } @@ -171,12 +174,13 @@ ext { lottie_version = "4.2.0" core_version = "1.6.0" desugar_jdk_version = "1.1.5" + hilt_version = "2.38.1" + // Compose activity_compose_version = "1.3.1" constraintlayout_compose_version = "1.0.0-rc01" viewmodel_compose_version = "1.0.0-alpha07" } - jacoco { toolVersion = "0.8.7" } @@ -208,6 +212,8 @@ dependencies { implementation "com.squareup.retrofit2:retrofit:$retrofit_version" implementation "com.squareup.retrofit2:converter-moshi:$retrofit_version" + implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" + // Http Logging Interceptor implementation "com.squareup.okhttp3:okhttp:$ok_http_version" implementation "com.squareup.okhttp3:logging-interceptor:$ok_http_version" @@ -292,6 +298,10 @@ dependencies { implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$viewmodel_compose_version" implementation "com.google.android.material:compose-theme-adapter:$compose_version" + // Hilt: Official Google Dependency Injection Library + implementation "com.google.dagger:hilt-android:$hilt_version" + kapt "com.google.dagger:hilt-android-compiler:$hilt_version" + // Test // Unit testing framework androidTestImplementation "com.android.support.test:rules:$test_rule_version" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 04b2829..9a52df3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -32,11 +32,14 @@ android:networkSecurityConfig="@xml/network_security_config" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" - android:theme="@style/Theme.AndroidBase.Main"> - + android:theme="@style/Theme.AndroidBase.Main" > + - + diff --git a/app/src/main/java/com/rocketinsights/android/RocketApplication.kt b/app/src/main/java/com/rocketinsights/android/RocketApplication.kt index 3de415c..adb280a 100644 --- a/app/src/main/java/com/rocketinsights/android/RocketApplication.kt +++ b/app/src/main/java/com/rocketinsights/android/RocketApplication.kt @@ -1,14 +1,17 @@ package com.rocketinsights.android import android.app.Application -import com.rocketinsights.android.di.initKoin +import androidx.viewbinding.BuildConfig +import com.rocketinsights.android.di.koin.initKoin import com.rocketinsights.android.work.messages.MessagesUpdateScheduler +import dagger.hilt.android.HiltAndroidApp import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.koin.android.ext.android.inject import timber.log.Timber +@HiltAndroidApp class RocketApplication : Application() { private val applicationScope = CoroutineScope(Dispatchers.Default) diff --git a/app/src/main/java/com/rocketinsights/android/di/hilt/AppModule.kt b/app/src/main/java/com/rocketinsights/android/di/hilt/AppModule.kt new file mode 100644 index 0000000..50eef66 --- /dev/null +++ b/app/src/main/java/com/rocketinsights/android/di/hilt/AppModule.kt @@ -0,0 +1,19 @@ +package com.rocketinsights.android.di.hilt + +import android.content.Context +import com.rocketinsights.android.RocketApplication +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object AppModule { + @Singleton + @Provides + fun provideApplication(@ApplicationContext app: Context): RocketApplication = + app as RocketApplication +} diff --git a/app/src/main/java/com/rocketinsights/android/di/hilt/HiltNetworkModule.kt b/app/src/main/java/com/rocketinsights/android/di/hilt/HiltNetworkModule.kt new file mode 100644 index 0000000..4e13cad --- /dev/null +++ b/app/src/main/java/com/rocketinsights/android/di/hilt/HiltNetworkModule.kt @@ -0,0 +1,57 @@ +package com.rocketinsights.android.di.hilt + +import com.google.gson.GsonBuilder +import com.rocketinsights.android.models.recipe_hilt.RecipeDtoMapper +import com.rocketinsights.android.network.hiltexample.RecipeService +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import retrofit2.Retrofit +import retrofit2.converter.gson.GsonConverterFactory +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object HiltNetworkModule { + + private var okHttpClient: OkHttpClient? = null + + @Singleton + @Provides + fun providesRecipeMapper(): RecipeDtoMapper = RecipeDtoMapper() + + @Singleton + @Provides + fun provideRecipeService(): RecipeService { + return Retrofit.Builder() + .baseUrl("https://food2fork.ca/api/recipe/") + .addConverterFactory(GsonConverterFactory.create(GsonBuilder().create())) + .client(buildClientHttp()) + .build() + .create(RecipeService::class.java) + } + + private fun buildClientHttp(): OkHttpClient? { + okHttpClient = OkHttpClient.Builder() + .addInterceptor(buildInterceptor()) + .build() + + return okHttpClient + } + + private fun buildInterceptor() = Interceptor { chain -> + var request = chain.request() + + request = request.newBuilder() + .addHeader( + "Authorization", + "Token 9c8b06d329136da358c2d00e76946b0111ce2c48" + ) + .build() + + chain.proceed(request = request) + } +} diff --git a/app/src/main/java/com/rocketinsights/android/di/hilt/RepositoryModule.kt b/app/src/main/java/com/rocketinsights/android/di/hilt/RepositoryModule.kt new file mode 100644 index 0000000..e533985 --- /dev/null +++ b/app/src/main/java/com/rocketinsights/android/di/hilt/RepositoryModule.kt @@ -0,0 +1,28 @@ +package com.rocketinsights.android.di.hilt + +import com.rocketinsights.android.models.recipe_hilt.RecipeDtoMapper +import com.rocketinsights.android.network.hiltexample.RecipeService +import com.rocketinsights.android.repos.RecipeRepository +import com.rocketinsights.android.repos.RecipeRepositoryImplementation +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object RepositoryModule { + + @Singleton + @Provides + fun provideRecipeRepository( + recipeService: RecipeService, + recipeMapper: RecipeDtoMapper, + ): RecipeRepository { + return RecipeRepositoryImplementation( + recipeService = recipeService, + mapper = recipeMapper + ) + } +} diff --git a/app/src/main/java/com/rocketinsights/android/di/KoinUtils.kt b/app/src/main/java/com/rocketinsights/android/di/koin/KoinUtils.kt similarity index 99% rename from app/src/main/java/com/rocketinsights/android/di/KoinUtils.kt rename to app/src/main/java/com/rocketinsights/android/di/koin/KoinUtils.kt index fb0c482..bfe2917 100644 --- a/app/src/main/java/com/rocketinsights/android/di/KoinUtils.kt +++ b/app/src/main/java/com/rocketinsights/android/di/koin/KoinUtils.kt @@ -1,4 +1,4 @@ -package com.rocketinsights.android.di +package com.rocketinsights.android.di.koin import android.app.Application import android.app.NotificationManager diff --git a/app/src/main/java/com/rocketinsights/android/models/recipe_hilt/Recipe.kt b/app/src/main/java/com/rocketinsights/android/models/recipe_hilt/Recipe.kt new file mode 100644 index 0000000..2c212ed --- /dev/null +++ b/app/src/main/java/com/rocketinsights/android/models/recipe_hilt/Recipe.kt @@ -0,0 +1,13 @@ +package com.rocketinsights.android.models.recipe_hilt + +data class Recipe( + val id: Int, + val title: String, + val publisher: String, + val featuredImage: String, + val rating: Int = 0, + val sourceUrl: String, + val ingredients: List, + val dateAdded: String, + val dateUpdated: String +) diff --git a/app/src/main/java/com/rocketinsights/android/models/recipe_hilt/RecipeDto.kt b/app/src/main/java/com/rocketinsights/android/models/recipe_hilt/RecipeDto.kt new file mode 100644 index 0000000..b4bfbcb --- /dev/null +++ b/app/src/main/java/com/rocketinsights/android/models/recipe_hilt/RecipeDto.kt @@ -0,0 +1,19 @@ +package com.rocketinsights.android.models.recipe_hilt + +import com.google.gson.annotations.SerializedName + +data class RecipeDto( + var pk: Int, + var title: String, + var publisher: String, + @SerializedName("featured_image") + var featuredImage: String, + var rating: Int = 0, + @SerializedName("source_url") + var sourceUrl: String, + var ingredients: List = emptyList(), + @SerializedName("date_added") + var dateAdded: String, + @SerializedName("date_updated") + var dateUpdated: String, +) diff --git a/app/src/main/java/com/rocketinsights/android/models/recipe_hilt/RecipeDtoMapper.kt b/app/src/main/java/com/rocketinsights/android/models/recipe_hilt/RecipeDtoMapper.kt new file mode 100644 index 0000000..fe59dc9 --- /dev/null +++ b/app/src/main/java/com/rocketinsights/android/models/recipe_hilt/RecipeDtoMapper.kt @@ -0,0 +1,19 @@ +package com.rocketinsights.android.models.recipe_hilt + +class RecipeDtoMapper { + fun mapToDomainModel(model: RecipeDto): Recipe { + return Recipe( + id = model.pk, + title = model.title, + featuredImage = model.featuredImage, + rating = model.rating, + publisher = model.publisher, + sourceUrl = model.sourceUrl, + ingredients = model.ingredients, + dateAdded = model.dateAdded, + dateUpdated = model.dateUpdated, + ) + } + + fun toDomainList(initial: List): List = initial.map { mapToDomainModel(it) } +} diff --git a/app/src/main/java/com/rocketinsights/android/network/hiltexample/RecipeSearchResponse.kt b/app/src/main/java/com/rocketinsights/android/network/hiltexample/RecipeSearchResponse.kt new file mode 100644 index 0000000..de99856 --- /dev/null +++ b/app/src/main/java/com/rocketinsights/android/network/hiltexample/RecipeSearchResponse.kt @@ -0,0 +1,8 @@ +package com.rocketinsights.android.network.hiltexample + +import com.rocketinsights.android.models.recipe_hilt.RecipeDto + +data class RecipeSearchResponse( + val count: Int, + val results: List, +) diff --git a/app/src/main/java/com/rocketinsights/android/network/hiltexample/RecipeService.kt b/app/src/main/java/com/rocketinsights/android/network/hiltexample/RecipeService.kt new file mode 100644 index 0000000..9660347 --- /dev/null +++ b/app/src/main/java/com/rocketinsights/android/network/hiltexample/RecipeService.kt @@ -0,0 +1,13 @@ +package com.rocketinsights.android.network.hiltexample + +import retrofit2.http.GET +import retrofit2.http.Query + +interface RecipeService { + + @GET("search") + suspend fun searchRecipe( + @Query("page") page: Int, + @Query("query") query: String + ): RecipeSearchResponse +} diff --git a/app/src/main/java/com/rocketinsights/android/repos/RecipeRepository.kt b/app/src/main/java/com/rocketinsights/android/repos/RecipeRepository.kt new file mode 100644 index 0000000..d1deecd --- /dev/null +++ b/app/src/main/java/com/rocketinsights/android/repos/RecipeRepository.kt @@ -0,0 +1,7 @@ +package com.rocketinsights.android.repos + +import com.rocketinsights.android.models.recipe_hilt.Recipe + +interface RecipeRepository { + suspend fun search(page: Int, query: String): List? +} diff --git a/app/src/main/java/com/rocketinsights/android/repos/RecipeRepositoryImplementation.kt b/app/src/main/java/com/rocketinsights/android/repos/RecipeRepositoryImplementation.kt new file mode 100644 index 0000000..df73112 --- /dev/null +++ b/app/src/main/java/com/rocketinsights/android/repos/RecipeRepositoryImplementation.kt @@ -0,0 +1,14 @@ +package com.rocketinsights.android.repos + +import com.rocketinsights.android.models.recipe_hilt.Recipe +import com.rocketinsights.android.models.recipe_hilt.RecipeDtoMapper +import com.rocketinsights.android.network.hiltexample.RecipeService + +class RecipeRepositoryImplementation( + private val recipeService: RecipeService, + private val mapper: RecipeDtoMapper, +) : RecipeRepository { + + override suspend fun search(page: Int, query: String): List = + mapper.toDomainList(recipeService.searchRecipe(page = page, query = query).results) +} diff --git a/app/src/main/java/com/rocketinsights/android/ui/HiltActivity.kt b/app/src/main/java/com/rocketinsights/android/ui/HiltActivity.kt new file mode 100644 index 0000000..63d3852 --- /dev/null +++ b/app/src/main/java/com/rocketinsights/android/ui/HiltActivity.kt @@ -0,0 +1,22 @@ +package com.rocketinsights.android.ui + +import android.os.Bundle +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import com.rocketinsights.android.viewmodels.HiltListViewModel +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class HiltActivity : AppCompatActivity() { + + private val viewModel: HiltListViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + // TODO: Show data with JetpackCompose in next PR + viewModel.liveRecipe.observe(this) { recipe -> + // do nothing for now + } + } +} diff --git a/app/src/main/java/com/rocketinsights/android/ui/MainFragment.kt b/app/src/main/java/com/rocketinsights/android/ui/MainFragment.kt index c1898ea..2eaf86c 100644 --- a/app/src/main/java/com/rocketinsights/android/ui/MainFragment.kt +++ b/app/src/main/java/com/rocketinsights/android/ui/MainFragment.kt @@ -1,5 +1,6 @@ package com.rocketinsights.android.ui +import android.content.Intent import android.content.pm.PackageManager import android.net.Uri import android.os.Bundle @@ -126,6 +127,11 @@ class MainFragment : BaseFragment(R.layout.fragment_main) { setFadeThroughTransition() item.onNavDestinationSelected(findNavController()) } + R.id.hilt_activity -> { + setFadeThroughTransition() + Intent(this.context, HiltActivity::class.java).also { startActivity(it) } + true + } else -> super.onOptionsItemSelected(item) } } diff --git a/app/src/main/java/com/rocketinsights/android/viewmodels/HiltListViewModel.kt b/app/src/main/java/com/rocketinsights/android/viewmodels/HiltListViewModel.kt new file mode 100644 index 0000000..84daba1 --- /dev/null +++ b/app/src/main/java/com/rocketinsights/android/viewmodels/HiltListViewModel.kt @@ -0,0 +1,35 @@ +package com.rocketinsights.android.viewmodels + +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.rocketinsights.android.models.recipe_hilt.Recipe +import com.rocketinsights.android.repos.RecipeRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class HiltListViewModel +@Inject +constructor( + private val repository: RecipeRepository +) : ViewModel() { + + private val recipes: MutableLiveData> = MutableLiveData() + val liveRecipe: LiveData> = recipes + + init { + viewModelScope.launch { + val result = repository.search( + page = 1, + query = "beef carrot potato onion" + ) + + result?.apply { + recipes.value = this + } + } + } +} diff --git a/app/src/main/res/menu/main_menu.xml b/app/src/main/res/menu/main_menu.xml index 764025a..8d3f4c2 100644 --- a/app/src/main/res/menu/main_menu.xml +++ b/app/src/main/res/menu/main_menu.xml @@ -29,6 +29,9 @@ + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b36d68a..9d62f0c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -109,4 +109,7 @@ Contacts Read contacts permission not granted + + + Hilt example diff --git a/build.gradle b/build.gradle index f23d4ed..c1eda9d 100644 --- a/build.gradle +++ b/build.gradle @@ -9,6 +9,7 @@ buildscript { navigation_version = "2.3.5" google_services_version = "4.3.10" compose_version = "1.0.3" + hilt_version = "2.38.1" } repositories { gradlePluginPortal() @@ -22,6 +23,7 @@ buildscript { classpath "com.google.gms:google-services:$google_services_version" classpath "org.owasp:dependency-check-gradle:$dependency_check_version" classpath "com.github.ben-manes:gradle-versions-plugin:$gradle_versions_version" + classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files }