diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7cc1f25..0810011 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,5 +1,6 @@ import com.zucchini.buildsrc.Constants + plugins { id("com.android.application") kotlin("android") @@ -21,36 +22,6 @@ android { versionName = Constants.versionName testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - -// buildConfigField( -// "String", -// "NATIVE_APP_KEY", -// gradleLocalProperties(rootDir).getProperty("native.app.key"), -// ) -// manifestPlaceholders["NATIVE_APP_KEY"] = -// gradleLocalProperties(rootDir).getProperty("nativeAppKey") - } - - buildTypes { - debug { -// buildConfigField( -// "String", -// "BASE_URL", -// gradleLocalProperties(rootDir).getProperty("test.base.url") -// ) - } - release { -// buildConfigField( -// "String", -// "BASE_URL", -// gradleLocalProperties(rootDir).getProperty("base.url") -// ) - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro", - ) - } } compileOptions { @@ -75,6 +46,7 @@ dependencies { implementation(project(":domain")) implementation(project(":core:designsystem")) implementation(project(":core:common")) + implementation(project(":core:network")) implementation(project(":feature:projects")) KotlinDependencies.run { @@ -95,6 +67,7 @@ dependencies { KaptDependencies.run { kapt(hiltCompiler) kapt(hiltWorkManagerCompiler) + kapt(hiltAndroidCompiler) } TestDependencies.run { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 14de5f0..1d8dcf0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -14,7 +14,8 @@ android:roundIcon="@drawable/logo_olive_rotational" android:supportsRtl="true" android:theme="@style/Theme.SSUPlectorAndroid" - tools:targetApi="31"> + tools:targetApi="31" + android:usesCleartextTraffic="true"> , + val developerList: List, @SerialName("shortIntro") val shortIntro: String, @SerialName("longIntro") @@ -37,7 +37,7 @@ data class DevelopersDetailResponse( val techStackList: List, ) { @Serializable - data class Developer( + data class DeveloperInfoInDevelopersDetail( @SerialName("id") val id: Int, @SerialName("name") diff --git a/core/network/src/main/java/com/sample/network/reponse/DevlopersListReponse.kt b/core/network/src/main/java/com/sample/network/reponse/DevelopersListResponse.kt similarity index 82% rename from core/network/src/main/java/com/sample/network/reponse/DevlopersListReponse.kt rename to core/network/src/main/java/com/sample/network/reponse/DevelopersListResponse.kt index 9ae6b60..1d3f465 100644 --- a/core/network/src/main/java/com/sample/network/reponse/DevlopersListReponse.kt +++ b/core/network/src/main/java/com/sample/network/reponse/DevelopersListResponse.kt @@ -4,7 +4,7 @@ import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable -data class DevlopersListReponse( +data class DevelopersListResponse( @SerialName("currentElement") val currentElement: Int, @SerialName("totalElement") @@ -12,10 +12,10 @@ data class DevlopersListReponse( @SerialName("totalPage") val totalPage: Int, @SerialName("developerResponseDTOList") - val developerResponseDTOList: List, + val developerResponseDTOListInList: List, ) { @Serializable - data class DeveloperDetailInfo( + data class DeveloperDetailInProjectInfo( @SerialName("id") val id: Int, @SerialName("name") diff --git a/core/network/src/main/java/com/sample/network/service/DevelopersService.kt b/core/network/src/main/java/com/sample/network/service/DevelopersService.kt new file mode 100644 index 0000000..620ed67 --- /dev/null +++ b/core/network/src/main/java/com/sample/network/service/DevelopersService.kt @@ -0,0 +1,22 @@ +package com.sample.network.service + +import com.sample.network.model.BaseResponse +import com.sample.network.reponse.DevelopersDetailResponse +import com.sample.network.reponse.DevelopersListResponse +import retrofit2.http.GET +import retrofit2.http.Query + +interface DevelopersService { + + @GET("/api/developers/list") + suspend fun getDevelopersListData( + @Query("sortType") sortType: String? = null, + @Query("part") part: String? = null, + @Query("page") page: Int = 0, + ): BaseResponse + + @GET("/api/developers/{developerId}") + suspend fun getDevelopersDetailData( + developerId: Int, + ): BaseResponse +} diff --git a/core/network/src/main/java/com/sample/network/service/ProjectsService.kt b/core/network/src/main/java/com/sample/network/service/ProjectsService.kt new file mode 100644 index 0000000..f1b85b8 --- /dev/null +++ b/core/network/src/main/java/com/sample/network/service/ProjectsService.kt @@ -0,0 +1,18 @@ +package com.sample.network.service + +import com.sample.network.model.BaseResponse +import com.sample.network.reponse.ProjectsDetailResponse +import com.sample.network.reponse.ProjectsListReponse +import com.sample.network.request.ProjectsListRequest +import retrofit2.http.Body +import retrofit2.http.GET + +interface ProjectsService { + @GET("api/projects/list") + suspend fun getProjectsListData(@Body request: ProjectsListRequest): BaseResponse + + @GET("api/projects/{projectId}") + suspend fun getProjectsDetailData( + projectId: Int, + ): BaseResponse +} diff --git a/data/build.gradle.kts b/data/build.gradle.kts index 695f7d6..9cf27e8 100644 --- a/data/build.gradle.kts +++ b/data/build.gradle.kts @@ -34,6 +34,7 @@ android { dependencies { implementation(project(":domain")) + implementation(project(":core:network")) AndroidXDependencies.run { implementation(hilt) @@ -62,4 +63,10 @@ dependencies { androidTestImplementation(androidTest) androidTestImplementation(espresso) } + + KaptDependencies.run { + kapt(hiltCompiler) + kapt(hiltWorkManagerCompiler) + kapt(hiltAndroidCompiler) + } } diff --git a/data/src/main/java/com/zucchini/data/DevelopersRepositoryImpl.kt b/data/src/main/java/com/zucchini/data/DevelopersRepositoryImpl.kt new file mode 100644 index 0000000..a96c9e8 --- /dev/null +++ b/data/src/main/java/com/zucchini/data/DevelopersRepositoryImpl.kt @@ -0,0 +1,31 @@ +package com.zucchini.data + +import com.sample.network.service.DevelopersService +import com.zucchini.domain.model.DevelopersDetailModel +import com.zucchini.domain.model.DevelopersListModel +import com.zucchini.domain.repository.DevelopersRepository +import com.zucchini.mapper.toDevelopersDetailModel +import com.zucchini.mapper.toDevelopersListModel +import javax.inject.Inject + +class DevelopersRepositoryImpl @Inject constructor( + private val developersService: DevelopersService, +) : DevelopersRepository { + override suspend fun getDevelopersListData(): Result { + return runCatching { + developersService.getDevelopersListData( + sortType = null, + part = null, + page = 0, + ).data.toDevelopersListModel() + } + } + + override suspend fun getDevelopersDetailData( + developerId: Int, + ): Result { + return runCatching { + developersService.getDevelopersDetailData(developerId).data.toDevelopersDetailModel() + } + } +} diff --git a/data/src/main/java/com/zucchini/data/ProjectsRepositoryImpl.kt b/data/src/main/java/com/zucchini/data/ProjectsRepositoryImpl.kt new file mode 100644 index 0000000..04797aa --- /dev/null +++ b/data/src/main/java/com/zucchini/data/ProjectsRepositoryImpl.kt @@ -0,0 +1,8 @@ +package com.zucchini.data + +import com.sample.network.service.ProjectsService +import javax.inject.Inject + +class ProjectsRepositoryImpl @Inject constructor( + private val projectsService: ProjectsService, +) diff --git a/data/src/main/java/com/zucchini/di/DevelopersBinderModule.kt b/data/src/main/java/com/zucchini/di/DevelopersBinderModule.kt new file mode 100644 index 0000000..9e49f0f --- /dev/null +++ b/data/src/main/java/com/zucchini/di/DevelopersBinderModule.kt @@ -0,0 +1,17 @@ +package com.zucchini.di + +import com.zucchini.data.DevelopersRepositoryImpl +import com.zucchini.domain.repository.DevelopersRepository +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +interface DevelopersBinderModule { + @Binds + @Singleton + fun provideDevelopersRepository(developersRepository: DevelopersRepositoryImpl): DevelopersRepository +} diff --git a/data/src/main/java/com/zucchini/mapper/DevelopersDetail.kt b/data/src/main/java/com/zucchini/mapper/DevelopersDetail.kt new file mode 100644 index 0000000..359664c --- /dev/null +++ b/data/src/main/java/com/zucchini/mapper/DevelopersDetail.kt @@ -0,0 +1,33 @@ +package com.zucchini.mapper + +import com.sample.network.reponse.DevelopersDetailResponse +import com.zucchini.domain.model.DeveloperInfoInDetailModel +import com.zucchini.domain.model.DevelopersDetailModel + +internal fun DevelopersDetailResponse.toDevelopersDetailModel(): DevelopersDetailModel { + val developerList = developerList.map { developerInfo -> + DeveloperInfoInDetailModel( + id = developerInfo.id, + name = developerInfo.name, + partList = developerInfo.partList, + ) + } + + return DevelopersDetailModel( + id = id, + name = name, + imageLink = imageLink, + developerList = developerList, + shortIntro = shortIntro, + longIntro = longIntro, + category = category, + hits = hits, + githubLink = githubLink, + infoPageLink = infoPageLink, + webLink = webLink, + appLink = appLink, + languageList = languageList, + devToolList = devToolList, + techStackList = techStackList, + ) +} diff --git a/data/src/main/java/com/zucchini/mapper/DevelopersList.kt b/data/src/main/java/com/zucchini/mapper/DevelopersList.kt new file mode 100644 index 0000000..6a60307 --- /dev/null +++ b/data/src/main/java/com/zucchini/mapper/DevelopersList.kt @@ -0,0 +1,25 @@ +package com.zucchini.mapper + +import com.sample.network.reponse.DevelopersListResponse +import com.zucchini.domain.model.DeveloperDetailInfoInListModel +import com.zucchini.domain.model.DevelopersListModel + +internal fun DevelopersListResponse.toDevelopersListModel(): DevelopersListModel { + val developerDetailListInList = developerResponseDTOListInList.map { dto -> + DeveloperDetailInfoInListModel( + id = dto.id, + name = dto.name, + part1 = dto.part1, + part2 = dto.part2, + githubLink = dto.githubLink, + hits = dto.hits, + ) + } + + return DevelopersListModel( + currentElement = currentElement, + totalElement = totalElement, + totalPage = totalPage, + developerDetailList = developerDetailListInList, + ) +} diff --git a/domain/src/main/java/com/zucchini/domain/model/DevelopersDetailModel.kt b/domain/src/main/java/com/zucchini/domain/model/DevelopersDetailModel.kt new file mode 100644 index 0000000..ed1ef4a --- /dev/null +++ b/domain/src/main/java/com/zucchini/domain/model/DevelopersDetailModel.kt @@ -0,0 +1,26 @@ +package com.zucchini.domain.model + +data class DevelopersDetailModel( + val id: Int, + val name: String, + val imageLink: String, + val developerList: List, + val shortIntro: String, + val longIntro: String, + val category: String, + val hits: Int, + val githubLink: String, + val infoPageLink: String, + val webLink: String, + val appLink: String, + val languageList: List, + val devToolList: List, + val techStackList: List +) + +data class DeveloperInfoInDetailModel( + val id: Int, + val name: String, + val partList: List +) + diff --git a/domain/src/main/java/com/zucchini/domain/model/DevelopersListModel.kt b/domain/src/main/java/com/zucchini/domain/model/DevelopersListModel.kt new file mode 100644 index 0000000..63dc99b --- /dev/null +++ b/domain/src/main/java/com/zucchini/domain/model/DevelopersListModel.kt @@ -0,0 +1,17 @@ +package com.zucchini.domain.model + +data class DevelopersListModel( + val currentElement: Int, + val totalElement: Int, + val totalPage: Int, + val developerDetailList: List, +) + +data class DeveloperDetailInfoInListModel( + val id: Int, + val name: String, + val part1: String, + val part2: String, + val githubLink: String, + val hits: Int, +) diff --git a/domain/src/main/java/com/zucchini/domain/repository/DevelopersRepository.kt b/domain/src/main/java/com/zucchini/domain/repository/DevelopersRepository.kt new file mode 100644 index 0000000..564ea1f --- /dev/null +++ b/domain/src/main/java/com/zucchini/domain/repository/DevelopersRepository.kt @@ -0,0 +1,9 @@ +package com.zucchini.domain.repository + +import com.zucchini.domain.model.DevelopersDetailModel +import com.zucchini.domain.model.DevelopersListModel + +interface DevelopersRepository { + suspend fun getDevelopersListData(): Result + suspend fun getDevelopersDetailData(developerId: Int): Result +} diff --git a/feature/projects/build.gradle.kts b/feature/projects/build.gradle.kts index aa90c0b..d180858 100644 --- a/feature/projects/build.gradle.kts +++ b/feature/projects/build.gradle.kts @@ -40,6 +40,7 @@ dependencies { implementation(project(":domain")) implementation(project(":core:designsystem")) implementation(project(":core:common")) + implementation(project(":core:network")) KotlinDependencies.run { implementation(kotlin) @@ -70,6 +71,7 @@ dependencies { KaptDependencies.run { kapt(hiltCompiler) kapt(hiltWorkManagerCompiler) + kapt(hiltAndroidCompiler) } implementation(MaterialDesignDependencies.materialDesign) diff --git a/feature/projects/src/main/java/com/zucchini/projects/IntroduceActivity.kt b/feature/projects/src/main/java/com/zucchini/projects/IntroduceActivity.kt index 477fde2..d778d65 100644 --- a/feature/projects/src/main/java/com/zucchini/projects/IntroduceActivity.kt +++ b/feature/projects/src/main/java/com/zucchini/projects/IntroduceActivity.kt @@ -7,11 +7,13 @@ import androidx.lifecycle.lifecycleScope import com.zucchini.feature.projects.R import com.zucchini.feature.projects.databinding.ActivityIntroduceBinding import com.zucchini.projects.adapter.IntroducePagerAdapter +import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch +@AndroidEntryPoint class IntroduceActivity : AppCompatActivity() { private lateinit var binding: ActivityIntroduceBinding diff --git a/feature/projects/src/main/java/com/zucchini/projects/MainActivity.kt b/feature/projects/src/main/java/com/zucchini/projects/MainActivity.kt index 5f8979a..f8ea3a1 100644 --- a/feature/projects/src/main/java/com/zucchini/projects/MainActivity.kt +++ b/feature/projects/src/main/java/com/zucchini/projects/MainActivity.kt @@ -4,9 +4,10 @@ import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.setupWithNavController -import com.zucchini.feature.projects.R import com.zucchini.feature.projects.databinding.ActivityMainBinding +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding diff --git a/feature/projects/src/main/java/com/zucchini/projects/developer/DevDetailActivity.kt b/feature/projects/src/main/java/com/zucchini/projects/developer/DevDetailActivity.kt index 89a8674..60e8cb6 100644 --- a/feature/projects/src/main/java/com/zucchini/projects/developer/DevDetailActivity.kt +++ b/feature/projects/src/main/java/com/zucchini/projects/developer/DevDetailActivity.kt @@ -1,19 +1,28 @@ package com.zucchini.projects.developer import android.os.Bundle +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.zucchini.feature.projects.databinding.ActivityDevDetailBinding import com.zucchini.projects.developer.adapter.DevDetailProjectAdapter import com.zucchini.projects.dummy.DevProjectsDummy +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +@AndroidEntryPoint class DevDetailActivity : AppCompatActivity() { private lateinit var binding: ActivityDevDetailBinding + private val devDetailViewModel: DevDetailViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { binding = ActivityDevDetailBinding.inflate(layoutInflater) super.onCreate(savedInstanceState) setContentView(binding.root) + collectDevelopersDetail() initProjectAdapter() } @@ -23,4 +32,13 @@ class DevDetailActivity : AppCompatActivity() { binding.rvDevProject.adapter = projectAdapter projectAdapter.submitList(DevProjectsDummy.devProjectsInfoList) } + + private fun collectDevelopersDetail() { + devDetailViewModel.developersDetail + .flowWithLifecycle(lifecycle) + .onEach { + // developerInfoAdapter.submitList(it.developersList) + } + .launchIn(lifecycleScope) + } } diff --git a/feature/projects/src/main/java/com/zucchini/projects/developer/DevDetailViewModel.kt b/feature/projects/src/main/java/com/zucchini/projects/developer/DevDetailViewModel.kt new file mode 100644 index 0000000..b9f8845 --- /dev/null +++ b/feature/projects/src/main/java/com/zucchini/projects/developer/DevDetailViewModel.kt @@ -0,0 +1,35 @@ +package com.zucchini.projects.developer + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.zucchini.domain.model.DevelopersDetailModel +import com.zucchini.domain.repository.DevelopersRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class DevDetailViewModel @Inject constructor( + private val developersRepository: DevelopersRepository, +) : ViewModel() { + + private val _developersDetail = MutableStateFlow(null) + val developersDetail = _developersDetail.asStateFlow() + + init { + loadDevelopersDetail(1) + } + fun loadDevelopersDetail(developerId: Int) { + viewModelScope.launch { + developersRepository.getDevelopersDetailData(developerId).onSuccess { + _developersDetail.value = it + Timber.tag("DevDetailViewModel").d(it.toString()) + }.onFailure { + // handle error + } + } + } +} \ No newline at end of file diff --git a/feature/projects/src/main/java/com/zucchini/projects/developer/DevInfoFragment.kt b/feature/projects/src/main/java/com/zucchini/projects/developer/DevInfoFragment.kt index ca9a2ff..d15334b 100644 --- a/feature/projects/src/main/java/com/zucchini/projects/developer/DevInfoFragment.kt +++ b/feature/projects/src/main/java/com/zucchini/projects/developer/DevInfoFragment.kt @@ -3,18 +3,29 @@ package com.zucchini.projects.developer import android.content.Intent import android.net.Uri import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.flowWithLifecycle +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.LinearLayoutManager +import com.zucchini.domain.model.Keyword +import com.zucchini.domain.model.KeywordList import com.zucchini.feature.projects.R import com.zucchini.feature.projects.databinding.FragmentDevInfoBinding import com.zucchini.projects.adapter.PageIndicatorAdapter import com.zucchini.projects.developer.adapter.DeveloperInfoAdapter import com.zucchini.projects.dummy.DeveloperInfoDummy +import com.zucchini.projects.projects.adapter.SearchKeywordAdapter +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +@AndroidEntryPoint class DevInfoFragment : Fragment() { private var _binding: FragmentDevInfoBinding? = null private val binding: FragmentDevInfoBinding get() = _binding!! @@ -24,6 +35,8 @@ class DevInfoFragment : Fragment() { private val totalPage = 4 + private val viewModel by viewModels() + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -31,13 +44,33 @@ class DevInfoFragment : Fragment() { ): View { _binding = FragmentDevInfoBinding.inflate(inflater, container, false) + initKeywordAdapter() initDeveloperAdapter() initPageIndicator() navigateToSubmitForms() + collectDevelopersList() return binding.root } + private fun collectDevelopersList() { + viewModel.developersList + .flowWithLifecycle(viewLifecycleOwner.lifecycle) + .onEach { + // developerInfoAdapter.submitList(it.developersList) + Log.d("DevInfoFragment", it.toString()) + } + .launchIn(viewLifecycleOwner.lifecycleScope) + } + + private fun initKeywordAdapter() { + val searchKeywordAdapter = SearchKeywordAdapter() + binding.rvSearchKeyword.layoutManager = + LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) + binding.rvSearchKeyword.adapter = searchKeywordAdapter + searchKeywordAdapter.submitList(KeywordList.searchKeyword.map { Keyword(it) }) + } + private fun initDeveloperAdapter() { developerInfoAdapter = DeveloperInfoAdapter() binding.rvDevinfo.adapter = developerInfoAdapter diff --git a/feature/projects/src/main/java/com/zucchini/projects/developer/DevInfoViewModel.kt b/feature/projects/src/main/java/com/zucchini/projects/developer/DevInfoViewModel.kt new file mode 100644 index 0000000..f82a827 --- /dev/null +++ b/feature/projects/src/main/java/com/zucchini/projects/developer/DevInfoViewModel.kt @@ -0,0 +1,47 @@ +package com.zucchini.projects.developer + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.zucchini.domain.model.DevelopersListModel +import com.zucchini.domain.repository.DevelopersRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import timber.log.Timber +import javax.inject.Inject + +@HiltViewModel +class DevInfoViewModel @Inject constructor( + private val developersRepository: DevelopersRepository, +) : ViewModel() { + private val _developersList = MutableStateFlow(null) + val developersList = _developersList.asStateFlow() + + init { + getDevelopersListData() + } + + fun getDevelopersListData() { + viewModelScope.launch { + developersRepository.getDevelopersListData().onSuccess { + _developersList.value = it + Timber.tag("DevInfoViewModel Success").d(it.toString()) + }.onFailure { + Timber.tag("DevInfoViewModel Timber").d(it.toString()) + } + } + } + +// fun getDevelopersDetailData(developerId: Int) { +// viewModelScope.launch { +// developersRepository.getDevelopersDetailData(developerId).onSuccess { +// _developersDetail.value = it +// Timber.tag("DevInfoViewModel").d(it.toString()) +// }.onFailure { +// Timber.tag("DevInfoViewModel").d(it.toString()) +// } +// } +// } +} diff --git a/feature/projects/src/main/res/layout/fragment_dev_info.xml b/feature/projects/src/main/res/layout/fragment_dev_info.xml index d039d64..75583e5 100644 --- a/feature/projects/src/main/res/layout/fragment_dev_info.xml +++ b/feature/projects/src/main/res/layout/fragment_dev_info.xml @@ -30,15 +30,28 @@ app:layout_constraintTop_toTopOf="parent" /> + + + app:layout_constraintTop_toBottomOf="@id/rv_search_keyword" + tools:listitem="@layout/item_developer"/>