diff --git a/app/build.gradle b/app/build.gradle index e454aec..d16c4bb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,6 +2,7 @@ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' id 'io.michaelrocks.paranoid' + id 'kotlin-kapt' } android { @@ -87,4 +88,7 @@ dependencies { testImplementation 'app.cash.turbine:turbine:0.8.0' testImplementation "com.google.truth:truth:1.1.3" testImplementation 'org.robolectric:robolectric:4.8' + + implementation 'com.google.dagger:dagger:2.42' + kapt 'com.google.dagger:dagger-compiler:2.42' } \ No newline at end of file diff --git a/app/src/main/java/com/example/githubuserfinder/app/GithubUserFinderApp.kt b/app/src/main/java/com/example/githubuserfinder/app/GithubUserFinderApp.kt index ea480cf..1660b93 100644 --- a/app/src/main/java/com/example/githubuserfinder/app/GithubUserFinderApp.kt +++ b/app/src/main/java/com/example/githubuserfinder/app/GithubUserFinderApp.kt @@ -8,18 +8,11 @@ import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import com.example.githubuserfinder.app.navigation.NavArgs import com.example.githubuserfinder.app.navigation.NavigationDestination -import com.example.githubuserfinder.core.data.NetworkRequester -import com.example.githubuserfinder.user_detail.data.adapter.GithubUserDetailJsonAdapter -import com.example.githubuserfinder.user_detail.data.datasource.UsersRemoteDataSource -import com.example.githubuserfinder.user_detail.data.datasource.UsersRemoteDataSourceImpl +import com.example.githubuserfinder.core.di.daggerViewModel +import com.example.githubuserfinder.user_detail.di.DaggerUserDetailComponent import com.example.githubuserfinder.user_detail.presentation.screen.UserDetailScreen -import com.example.githubuserfinder.user_detail.presentation.viewmodel.UserDetailViewModel -import com.example.githubuserfinder.user_finder.data.adapter.GithubSearchItemJsonAdapter -import com.example.githubuserfinder.user_finder.data.adapter.GithubSearchResponseJsonAdapter -import com.example.githubuserfinder.user_finder.data.datasource.SearchRemoteDataSource -import com.example.githubuserfinder.user_finder.data.datasource.SearchRemoteDataSourceImpl +import com.example.githubuserfinder.user_finder.di.DaggerUserFinderComponent import com.example.githubuserfinder.user_finder.presentation.screen.UserFinderScreen -import com.example.githubuserfinder.user_finder.presentation.viewmodel.UserFinderViewModel // As this is a small application, a dependency injection framework isn't used. // Dependencies are created in the root of the UI manually and injected to different features. @@ -28,29 +21,6 @@ fun GithubUserFinderApp() { val navController = rememberNavController() - // Dependencies - val networkRequester = NetworkRequester() - val githubSearchItemJsonAdapter = GithubSearchItemJsonAdapter() - val githubSearchResponseJsonAdapter = - GithubSearchResponseJsonAdapter(githubSearchItemJsonAdapter = githubSearchItemJsonAdapter) - val githubUserDetailJsonAdapter = GithubUserDetailJsonAdapter() - val searchRemoteDataSource: SearchRemoteDataSource = - SearchRemoteDataSourceImpl( - networkRequester = networkRequester, - githubSearchResponseJsonAdapter = githubSearchResponseJsonAdapter, - ) - val usersRemoteDataSource: UsersRemoteDataSource = - UsersRemoteDataSourceImpl( - networkRequester = networkRequester, - githubUserDetailJsonAdapter = githubUserDetailJsonAdapter, - ) - val userFinderViewModel = UserFinderViewModel( - searchRemoteDataSource = searchRemoteDataSource, - ) - val userDetailViewModel = UserDetailViewModel( - usersRemoteDataSource = usersRemoteDataSource, - ) - /** * This callback must be passed to [UserFinderScreen] so that * it would be able to navigate to the detail screen @@ -76,6 +46,11 @@ fun GithubUserFinderApp() { composable( route = NavigationDestination.UserFinderNavigationDestination.routeTemplate, ) { + val userFinderComponent = DaggerUserFinderComponent.builder().build() + val userFinderViewModel = daggerViewModel { + userFinderComponent.getViewModel() + } + UserFinderScreen( viewModel = userFinderViewModel, onNavigateToUserDetail = onNavigateToUserDetail, @@ -91,6 +66,11 @@ fun GithubUserFinderApp() { } ), ) { entry -> + val userDetailComponent = DaggerUserDetailComponent.builder().build() + val userDetailViewModel = daggerViewModel { + userDetailComponent.getViewModel() + } + val username = entry.arguments?.getString(NavArgs.Username) UserDetailScreen( viewModel = userDetailViewModel, diff --git a/app/src/main/java/com/example/githubuserfinder/app/di/AppComponent.kt b/app/src/main/java/com/example/githubuserfinder/app/di/AppComponent.kt new file mode 100644 index 0000000..b6e867f --- /dev/null +++ b/app/src/main/java/com/example/githubuserfinder/app/di/AppComponent.kt @@ -0,0 +1,6 @@ +package com.example.githubuserfinder.app.di + +import dagger.Component + +@Component +interface AppComponent diff --git a/app/src/main/java/com/example/githubuserfinder/core/data/NetworkRequester.kt b/app/src/main/java/com/example/githubuserfinder/core/data/NetworkRequester.kt index de24159..685c6ac 100644 --- a/app/src/main/java/com/example/githubuserfinder/core/data/NetworkRequester.kt +++ b/app/src/main/java/com/example/githubuserfinder/core/data/NetworkRequester.kt @@ -1,7 +1,5 @@ package com.example.githubuserfinder.core.data -import android.util.Log -import com.example.githubuserfinder.BuildConfig import com.example.githubuserfinder.core.presentation.CoreString import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers @@ -15,6 +13,7 @@ import java.io.InputStream import java.net.MalformedURLException import java.net.URL import java.nio.charset.StandardCharsets +import javax.inject.Inject import javax.net.ssl.HttpsURLConnection /** @@ -23,7 +22,7 @@ import javax.net.ssl.HttpsURLConnection * - Handles exceptions * - Returns a [DataResult] of type [JSONObject] * */ -class NetworkRequester { +class NetworkRequester @Inject constructor() { suspend fun invoke( url: URL, diff --git a/app/src/main/java/com/example/githubuserfinder/core/di/ViewModel.kt b/app/src/main/java/com/example/githubuserfinder/core/di/ViewModel.kt new file mode 100644 index 0000000..3f6850f --- /dev/null +++ b/app/src/main/java/com/example/githubuserfinder/core/di/ViewModel.kt @@ -0,0 +1,22 @@ +package com.example.githubuserfinder.core.di + +import androidx.compose.runtime.Composable +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.compose.viewModel + +@Composable +inline fun daggerViewModel( + key: String? = null, + crossinline viewModelInstanceCreator: () -> T +): T { + return viewModel( + modelClass = T::class.java, + key = key, + factory = object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return viewModelInstanceCreator() as T + } + } + ) +} diff --git a/app/src/main/java/com/example/githubuserfinder/user_detail/data/adapter/GithubUserDetailJsonAdapter.kt b/app/src/main/java/com/example/githubuserfinder/user_detail/data/adapter/GithubUserDetailJsonAdapter.kt index 07d23ab..0053d9e 100644 --- a/app/src/main/java/com/example/githubuserfinder/user_detail/data/adapter/GithubUserDetailJsonAdapter.kt +++ b/app/src/main/java/com/example/githubuserfinder/user_detail/data/adapter/GithubUserDetailJsonAdapter.kt @@ -5,13 +5,14 @@ import com.example.githubuserfinder.core.data.getStringNullable import com.example.githubuserfinder.user_detail.data.datasource.UsersRemoteDataSource import com.example.githubuserfinder.user_detail.data.model.GithubUserDetail import com.example.githubuserfinder.user_detail.presentation.UserDetailString +import javax.inject.Inject import org.json.JSONObject /** * This class is used in [UsersRemoteDataSource] and handles * converting a [JSONObject] to a [GithubUserDetail] model. * */ -class GithubUserDetailJsonAdapter : JsonAdapter { +class GithubUserDetailJsonAdapter @Inject constructor() : JsonAdapter { override fun createEntityFromJson(json: JSONObject): GithubUserDetail { return GithubUserDetail( login = json.getString(UserDetailString.UserDetailLoginKey), diff --git a/app/src/main/java/com/example/githubuserfinder/user_detail/data/datasource/UsersRemoteDataSourceImpl.kt b/app/src/main/java/com/example/githubuserfinder/user_detail/data/datasource/UsersRemoteDataSourceImpl.kt index 5c55d07..021df56 100644 --- a/app/src/main/java/com/example/githubuserfinder/user_detail/data/datasource/UsersRemoteDataSourceImpl.kt +++ b/app/src/main/java/com/example/githubuserfinder/user_detail/data/datasource/UsersRemoteDataSourceImpl.kt @@ -6,8 +6,9 @@ import com.example.githubuserfinder.user_detail.data.adapter.GithubUserDetailJso import com.example.githubuserfinder.user_detail.data.model.GithubUserDetail import com.example.githubuserfinder.user_detail.presentation.UserDetailString import java.net.URL +import javax.inject.Inject -class UsersRemoteDataSourceImpl( +class UsersRemoteDataSourceImpl @Inject constructor( private val networkRequester: NetworkRequester, private val githubUserDetailJsonAdapter: GithubUserDetailJsonAdapter, ) : UsersRemoteDataSource { diff --git a/app/src/main/java/com/example/githubuserfinder/user_detail/di/UserDetailComponent.kt b/app/src/main/java/com/example/githubuserfinder/user_detail/di/UserDetailComponent.kt new file mode 100644 index 0000000..e26454e --- /dev/null +++ b/app/src/main/java/com/example/githubuserfinder/user_detail/di/UserDetailComponent.kt @@ -0,0 +1,15 @@ +package com.example.githubuserfinder.user_detail.di + +import com.example.githubuserfinder.user_detail.presentation.viewmodel.UserDetailViewModel +import dagger.Component + +@UserDetailScope +@Component(modules = [UserDetailModule::class]) +interface UserDetailComponent { + @Component.Builder + interface Builder { + fun build(): UserDetailComponent + } + + fun getViewModel(): UserDetailViewModel +} \ No newline at end of file diff --git a/app/src/main/java/com/example/githubuserfinder/user_detail/di/UserDetailModule.kt b/app/src/main/java/com/example/githubuserfinder/user_detail/di/UserDetailModule.kt new file mode 100644 index 0000000..6481675 --- /dev/null +++ b/app/src/main/java/com/example/githubuserfinder/user_detail/di/UserDetailModule.kt @@ -0,0 +1,14 @@ +package com.example.githubuserfinder.user_detail.di + +import com.example.githubuserfinder.user_detail.data.datasource.UsersRemoteDataSource +import com.example.githubuserfinder.user_detail.data.datasource.UsersRemoteDataSourceImpl +import dagger.Binds +import dagger.Module + +@Module +abstract class UserDetailModule { + + @UserDetailScope + @Binds + abstract fun bindUserRemoteDataSource(usersRemoteDataSourceImpl: UsersRemoteDataSourceImpl): UsersRemoteDataSource +} diff --git a/app/src/main/java/com/example/githubuserfinder/user_detail/di/UserDetailScope.kt b/app/src/main/java/com/example/githubuserfinder/user_detail/di/UserDetailScope.kt new file mode 100644 index 0000000..0704804 --- /dev/null +++ b/app/src/main/java/com/example/githubuserfinder/user_detail/di/UserDetailScope.kt @@ -0,0 +1,7 @@ +package com.example.githubuserfinder.user_detail.di + +import javax.inject.Scope + +@Scope +@Retention +annotation class UserDetailScope diff --git a/app/src/main/java/com/example/githubuserfinder/user_detail/presentation/viewmodel/UserDetailViewModel.kt b/app/src/main/java/com/example/githubuserfinder/user_detail/presentation/viewmodel/UserDetailViewModel.kt index 01d0aea..7fe6098 100644 --- a/app/src/main/java/com/example/githubuserfinder/user_detail/presentation/viewmodel/UserDetailViewModel.kt +++ b/app/src/main/java/com/example/githubuserfinder/user_detail/presentation/viewmodel/UserDetailViewModel.kt @@ -6,6 +6,7 @@ import com.example.githubuserfinder.core.data.DataResult import com.example.githubuserfinder.user_detail.data.datasource.UsersRemoteDataSource import com.example.githubuserfinder.user_detail.data.model.GithubUserDetail import com.example.githubuserfinder.user_detail.presentation.screen.UserDetailScreen +import javax.inject.Inject import kotlinx.coroutines.Job import kotlinx.coroutines.ensureActive import kotlinx.coroutines.flow.MutableStateFlow @@ -23,7 +24,7 @@ data class UserDetailUiState( /** * This class contains all of the [UserDetailScreen] functionalities * */ -class UserDetailViewModel( +class UserDetailViewModel @Inject constructor( private val usersRemoteDataSource: UsersRemoteDataSource ) : ViewModel() { diff --git a/app/src/main/java/com/example/githubuserfinder/user_finder/data/adapter/GithubSearchItemJsonAdapter.kt b/app/src/main/java/com/example/githubuserfinder/user_finder/data/adapter/GithubSearchItemJsonAdapter.kt index 6a40e38..47ad9ea 100644 --- a/app/src/main/java/com/example/githubuserfinder/user_finder/data/adapter/GithubSearchItemJsonAdapter.kt +++ b/app/src/main/java/com/example/githubuserfinder/user_finder/data/adapter/GithubSearchItemJsonAdapter.kt @@ -3,12 +3,13 @@ package com.example.githubuserfinder.user_finder.data.adapter import com.example.githubuserfinder.core.data.JsonAdapter import com.example.githubuserfinder.user_finder.data.model.GithubSearchItemModel import com.example.githubuserfinder.user_finder.presentation.UserFinderString +import javax.inject.Inject import org.json.JSONObject /** * This class will handle converting a [JSONObject] to a [GithubSearchItemModel] model * */ -class GithubSearchItemJsonAdapter : JsonAdapter { +class GithubSearchItemJsonAdapter @Inject constructor() : JsonAdapter { override fun createEntityFromJson(json: JSONObject): GithubSearchItemModel { return GithubSearchItemModel( id = json.getInt(UserFinderString.GithubSearchItemIdKey), diff --git a/app/src/main/java/com/example/githubuserfinder/user_finder/data/adapter/GithubSearchResponseJsonAdapter.kt b/app/src/main/java/com/example/githubuserfinder/user_finder/data/adapter/GithubSearchResponseJsonAdapter.kt index 120ab93..71e2b74 100644 --- a/app/src/main/java/com/example/githubuserfinder/user_finder/data/adapter/GithubSearchResponseJsonAdapter.kt +++ b/app/src/main/java/com/example/githubuserfinder/user_finder/data/adapter/GithubSearchResponseJsonAdapter.kt @@ -3,12 +3,13 @@ package com.example.githubuserfinder.user_finder.data.adapter import com.example.githubuserfinder.core.data.JsonAdapter import com.example.githubuserfinder.user_finder.data.model.GithubSearchItemModel import com.example.githubuserfinder.user_finder.data.model.GithubSearchResponseModel +import javax.inject.Inject import org.json.JSONObject /** * This class will handle converting a [JSONObject] to a [GithubSearchResponseModel] model * */ -class GithubSearchResponseJsonAdapter( +class GithubSearchResponseJsonAdapter @Inject constructor( private val githubSearchItemJsonAdapter: GithubSearchItemJsonAdapter, ) : JsonAdapter { override fun createEntityFromJson(json: JSONObject): GithubSearchResponseModel { diff --git a/app/src/main/java/com/example/githubuserfinder/user_finder/data/datasource/SearchRemoteDataSourceImpl.kt b/app/src/main/java/com/example/githubuserfinder/user_finder/data/datasource/SearchRemoteDataSourceImpl.kt index c838d09..7dc6c13 100644 --- a/app/src/main/java/com/example/githubuserfinder/user_finder/data/datasource/SearchRemoteDataSourceImpl.kt +++ b/app/src/main/java/com/example/githubuserfinder/user_finder/data/datasource/SearchRemoteDataSourceImpl.kt @@ -6,8 +6,9 @@ import com.example.githubuserfinder.user_finder.data.adapter.GithubSearchRespons import com.example.githubuserfinder.user_finder.data.model.GithubSearchResponseModel import com.example.githubuserfinder.user_finder.presentation.UserFinderString import java.net.URL +import javax.inject.Inject -class SearchRemoteDataSourceImpl( +class SearchRemoteDataSourceImpl @Inject constructor( private val networkRequester: NetworkRequester, private val githubSearchResponseJsonAdapter: GithubSearchResponseJsonAdapter, ) : SearchRemoteDataSource { diff --git a/app/src/main/java/com/example/githubuserfinder/user_finder/di/UserFinderComponent.kt b/app/src/main/java/com/example/githubuserfinder/user_finder/di/UserFinderComponent.kt new file mode 100644 index 0000000..02edcc2 --- /dev/null +++ b/app/src/main/java/com/example/githubuserfinder/user_finder/di/UserFinderComponent.kt @@ -0,0 +1,15 @@ +package com.example.githubuserfinder.user_finder.di + +import com.example.githubuserfinder.user_finder.presentation.viewmodel.UserFinderViewModel +import dagger.Component + +@UserFinderScope +@Component(modules = [UserFinderModule::class]) +interface UserFinderComponent { + @Component.Builder + interface Builder { + fun build(): UserFinderComponent + } + + fun getViewModel(): UserFinderViewModel +} diff --git a/app/src/main/java/com/example/githubuserfinder/user_finder/di/UserFinderModule.kt b/app/src/main/java/com/example/githubuserfinder/user_finder/di/UserFinderModule.kt new file mode 100644 index 0000000..4f32cb8 --- /dev/null +++ b/app/src/main/java/com/example/githubuserfinder/user_finder/di/UserFinderModule.kt @@ -0,0 +1,14 @@ +package com.example.githubuserfinder.user_finder.di + +import com.example.githubuserfinder.user_finder.data.datasource.SearchRemoteDataSource +import com.example.githubuserfinder.user_finder.data.datasource.SearchRemoteDataSourceImpl +import dagger.Binds +import dagger.Module + +@Module +abstract class UserFinderModule { + + @UserFinderScope + @Binds + abstract fun bindSearchRemoteDataSource(searchRemoteDataSourceImpl: SearchRemoteDataSourceImpl): SearchRemoteDataSource +} diff --git a/app/src/main/java/com/example/githubuserfinder/user_finder/di/UserFinderScope.kt b/app/src/main/java/com/example/githubuserfinder/user_finder/di/UserFinderScope.kt new file mode 100644 index 0000000..5d8f2a8 --- /dev/null +++ b/app/src/main/java/com/example/githubuserfinder/user_finder/di/UserFinderScope.kt @@ -0,0 +1,7 @@ +package com.example.githubuserfinder.user_finder.di + +import javax.inject.Scope + +@Scope +@Retention +annotation class UserFinderScope diff --git a/app/src/main/java/com/example/githubuserfinder/user_finder/presentation/viewmodel/UserFinderViewModel.kt b/app/src/main/java/com/example/githubuserfinder/user_finder/presentation/viewmodel/UserFinderViewModel.kt index f1a2b3d..9f28266 100644 --- a/app/src/main/java/com/example/githubuserfinder/user_finder/presentation/viewmodel/UserFinderViewModel.kt +++ b/app/src/main/java/com/example/githubuserfinder/user_finder/presentation/viewmodel/UserFinderViewModel.kt @@ -7,6 +7,7 @@ import com.example.githubuserfinder.core.data.DataResult import com.example.githubuserfinder.user_finder.data.datasource.SearchRemoteDataSource import com.example.githubuserfinder.user_finder.data.model.GithubSearchResponseModel import com.example.githubuserfinder.user_finder.presentation.screen.UserFinderScreen +import javax.inject.Inject import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.ensureActive @@ -28,7 +29,7 @@ data class UserFinderUiState( /** * This class contains all of the [UserFinderScreen] functionalities * */ -class UserFinderViewModel( +class UserFinderViewModel @Inject constructor( private val searchRemoteDataSource: SearchRemoteDataSource, ) : ViewModel() {