diff --git a/README.md b/README.md index 8c399042..353ac664 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,14 @@ # android-map-refactoring ### KakaoTechCampus 2기 Step2 - 5주차 과제 : 리팩토링 -### 0단계. 4주차 코드 반영하기 \ No newline at end of file +### 1단계. 의존성 주입 + +### 기능 구현 +- [ ] build 파일 의존성 추가 +- [ ] Hilt 의존성 적용한 MyApplication 구현 +- [ ] Room Database 정의 +- [ ] DAO 정의 +- [ ] Room Database와 DAO에 Hilt 모듈 설정 구현 +- [ ] 데이터 추상화 구현 +- [ ] ViewModel 및 UI hilt 의존성 및 room 적용 수정 + diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ae9d3282..a9e2ee66 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -9,6 +9,8 @@ fun getApiKey(key: String): String { plugins { id("com.android.application") id("org.jetbrains.kotlin.android") + id("org.jetbrains.kotlin.kapt") + id("dagger.hilt.android.plugin") } android { @@ -21,10 +23,9 @@ android { targetSdk = 34 versionCode = 1 versionName = "1.0" - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - //API 가져오기 + resValue("string", "kakao_api_key", getApiKey("KAKAO_API_KEY")) buildConfigField("String", "KAKAO_REST_API_KEY", "\"${getApiKey("KAKAO_REST_API_KEY")}\"") } @@ -65,9 +66,14 @@ dependencies { implementation("com.squareup.retrofit2:retrofit:2.9.0") implementation("com.squareup.retrofit2:converter-gson:2.9.0") implementation("com.squareup.okhttp3:logging-interceptor:4.9.0") - implementation("com.kakao.sdk:v2-user:2.10.0") // Kakao SDK 추가 + implementation("com.kakao.sdk:v2-user:2.10.0") implementation("com.kakao.maps.open:android:2.9.5") implementation("androidx.activity:activity:1.8.0") + implementation("androidx.room:room-runtime:2.5.0") + kapt("androidx.room:room-compiler:2.5.0") + implementation("androidx.room:room-ktx:2.5.0") + implementation("com.google.dagger:hilt-android:2.46.1") + kapt("com.google.dagger:hilt-compiler:2.46.1") testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") @@ -76,13 +82,16 @@ dependencies { implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1") implementation("com.google.android.gms:play-services-maps:19.0.0") implementation("com.google.android.material:material:1.11.0") - testImplementation ("junit:junit:4.13.2") - androidTestImplementation ("androidx.test.ext:junit:1.1.3") - androidTestImplementation ("androidx.test.espresso:espresso-core:3.4.0") - androidTestImplementation ("androidx.test:core-ktx:1.4.0") - androidTestImplementation ("androidx.test:runner:1.4.0") - androidTestImplementation ("androidx.test:rules:1.4.0") - androidTestImplementation ("org.mockito:mockito-core:3.11.2") - androidTestImplementation ("org.mockito:mockito-android:3.11.2") - androidTestImplementation ("androidx.arch.core:core-testing:2.1.0") + androidTestImplementation("androidx.test:core-ktx:1.4.0") + androidTestImplementation("androidx.test:runner:1.4.0") + androidTestImplementation("androidx.test:rules:1.4.0") + androidTestImplementation("org.mockito:mockito-core:3.11.2") + androidTestImplementation("org.mockito:mockito-android:3.11.2") + androidTestImplementation("androidx.arch.core:core-testing:2.1.0") +} + +afterEvaluate { + kapt { + correctErrorTypes = true + } } \ No newline at end of file diff --git a/app/src/androidTest/java/campus/tech/kakao/map/FunctionTest.kt b/app/src/androidTest/java/campus/tech/kakao/map/FunctionTest.kt index f3acdc81..a7300448 100644 --- a/app/src/androidTest/java/campus/tech/kakao/map/FunctionTest.kt +++ b/app/src/androidTest/java/campus/tech/kakao/map/FunctionTest.kt @@ -13,7 +13,6 @@ import androidx.test.platform.app.InstrumentationRegistry import campus.tech.kakao.map.model.MapItem import campus.tech.kakao.map.repository.MapRepository import campus.tech.kakao.map.viewmodel.MapViewModel -import campus.tech.kakao.map.viewmodel.MapViewModelFactory import campus.tech.kakao.map.R import campus.tech.kakao.map.ui.MainActivity import campus.tech.kakao.map.ui.SearchActivity diff --git a/app/src/main/java/campus/tech/kakao/MyApplication.kt b/app/src/main/java/campus/tech/kakao/MyApplication.kt index 853f4ccf..b399087c 100644 --- a/app/src/main/java/campus/tech/kakao/MyApplication.kt +++ b/app/src/main/java/campus/tech/kakao/MyApplication.kt @@ -3,6 +3,9 @@ package campus.tech.kakao import android.app.Application import campus.tech.kakao.map.R import com.kakao.vectormap.KakaoMapSdk +import dagger.hilt.android.HiltAndroidApp + +@HiltAndroidApp class MyApplication : Application() { override fun onCreate() { diff --git a/app/src/main/java/campus/tech/kakao/map/dao/MapItemDao.kt b/app/src/main/java/campus/tech/kakao/map/dao/MapItemDao.kt new file mode 100644 index 00000000..3a350a69 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/dao/MapItemDao.kt @@ -0,0 +1,16 @@ +package campus.tech.kakao.map.database + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.Query +import campus.tech.kakao.map.model.MapItem + +@Dao +interface MapItemDao { + + @Insert + suspend fun insert(mapItem: MapItem) + + @Query("SELECT * FROM MapItem WHERE place_name LIKE :query") + suspend fun searchItems(query: String): List +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/database/AppDatabase.kt b/app/src/main/java/campus/tech/kakao/map/database/AppDatabase.kt new file mode 100644 index 00000000..ed55835b --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/database/AppDatabase.kt @@ -0,0 +1,10 @@ +package campus.tech.kakao.map.database + +import androidx.room.Database +import androidx.room.RoomDatabase +import campus.tech.kakao.map.model.MapItem + +@Database(entities = [MapItem::class], version = 1, exportSchema = false) +abstract class AppDatabase : RoomDatabase() { + abstract fun mapItemDao(): MapItemDao +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/di/AppModule.kt b/app/src/main/java/campus/tech/kakao/map/di/AppModule.kt new file mode 100644 index 00000000..68b2c789 --- /dev/null +++ b/app/src/main/java/campus/tech/kakao/map/di/AppModule.kt @@ -0,0 +1,31 @@ +package campus.tech.kakao.map.di + +import android.content.Context +import androidx.room.Room +import campus.tech.kakao.map.database.AppDatabase +import campus.tech.kakao.map.database.MapItemDao +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object AppModule { + + @Provides + @Singleton + fun provideDatabase(appContext: Context): AppDatabase { + return Room.databaseBuilder( + appContext, + AppDatabase::class.java, + "map_database" + ).build() + } + + @Provides + fun provideMapItemDao(database: AppDatabase): MapItemDao { + return database.mapItemDao() + } +} \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/model/MapItem.kt b/app/src/main/java/campus/tech/kakao/map/model/MapItem.kt index 28ab31cf..8cdf03e5 100644 --- a/app/src/main/java/campus/tech/kakao/map/model/MapItem.kt +++ b/app/src/main/java/campus/tech/kakao/map/model/MapItem.kt @@ -1,12 +1,14 @@ package campus.tech.kakao.map.model -//이름 보다 알기 쉽게 변경 - api맞춰서 +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "MapItem") data class MapItem( - val id: String, + @PrimaryKey val id: Int, val place_name: String, val road_address_name: String, val category_group_name: String, val x: Double, val y: Double - ) \ No newline at end of file diff --git a/app/src/main/java/campus/tech/kakao/map/repository/MapRepository.kt b/app/src/main/java/campus/tech/kakao/map/repository/MapRepository.kt index 19addf1f..d9c8f904 100644 --- a/app/src/main/java/campus/tech/kakao/map/repository/MapRepository.kt +++ b/app/src/main/java/campus/tech/kakao/map/repository/MapRepository.kt @@ -1,17 +1,29 @@ package campus.tech.kakao.map.repository -import android.app.Application +import campus.tech.kakao.map.database.MapItemDao +import campus.tech.kakao.map.network.KakaoApiService import campus.tech.kakao.map.model.MapItem +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import javax.inject.Inject interface MapRepository { suspend fun searchItems(query: String): List } -class MapRepositoryImpl(private val application: Application) : MapRepository { - - private val mapAccess = MapAccess(application) +class MapRepositoryImpl @Inject constructor( + private val mapItemDao: MapItemDao, + private val apiService: KakaoApiService +) : MapRepository { override suspend fun searchItems(query: String): List { - return mapAccess.searchItems(query) + return withContext(Dispatchers.IO) { + val response = apiService.searchPlaces(Constants.KAKAO_API_KEY, query) + if (response.isSuccessful) { + response.body()?.documents ?: emptyList() + } else { + emptyList() + } + } } -} \ No newline at end of file +} diff --git a/app/src/main/java/campus/tech/kakao/map/ui/MainActivity.kt b/app/src/main/java/campus/tech/kakao/map/ui/MainActivity.kt index 82c89a85..6b9d3ac3 100644 --- a/app/src/main/java/campus/tech/kakao/map/ui/MainActivity.kt +++ b/app/src/main/java/campus/tech/kakao/map/ui/MainActivity.kt @@ -1,41 +1,34 @@ package campus.tech.kakao.map.ui - +import android.app.Activity import android.content.Context import android.content.Intent import android.os.Bundle import android.util.Log - -import android.widget.EditText -import androidx.appcompat.app.AppCompatActivity -import com.kakao.vectormap.MapView -import com.kakao.vectormap.MapLifeCycleCallback -import com.kakao.vectormap.KakaoMapReadyCallback -import com.kakao.vectormap.KakaoMap - import android.view.View +import android.widget.EditText +import android.widget.FrameLayout import android.widget.ImageButton import android.widget.RelativeLayout import android.widget.TextView -import android.app.Activity -import android.widget.FrameLayout -import campus.tech.kakao.map.viewmodel.MapViewModelFactory +import androidx.activity.viewModels +import androidx.appcompat.app.AppCompatActivity +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.kakao.vectormap.* import campus.tech.kakao.map.R import campus.tech.kakao.map.model.MapItem -import com.google.android.material.bottomsheet.BottomSheetBehavior -import com.kakao.vectormap.LatLng +import campus.tech.kakao.map.viewmodel.MapViewModel +import com.kakao.vectormap.camera.CameraAnimation +import com.kakao.vectormap.camera.CameraUpdateFactory import com.kakao.vectormap.label.LabelLayer import com.kakao.vectormap.label.LabelOptions import com.kakao.vectormap.label.LabelStyle import com.kakao.vectormap.label.LabelStyles import com.kakao.vectormap.label.LabelTextStyle -import com.kakao.vectormap.camera.CameraAnimation -import com.kakao.vectormap.camera.CameraUpdateFactory - +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class MainActivity : AppCompatActivity() { - - lateinit var viewModelFactory: MapViewModelFactory private lateinit var mapView: MapView private lateinit var errorLayout: RelativeLayout private lateinit var errorMessage: TextView @@ -48,8 +41,9 @@ class MainActivity : AppCompatActivity() { private lateinit var bottomSheetAddress: TextView private lateinit var bottomSheetLayout: FrameLayout private var selectedItems = mutableListOf() - - companion object { + private val viewModel: MapViewModel by viewModels() + + companion object { const val SEARCH_REQUEST_CODE = 1 const val PREFS_NAME = "LastMarkerPrefs" const val PREF_LATITUDE = "lastLatitude" @@ -58,7 +52,6 @@ class MainActivity : AppCompatActivity() { const val PREF_ROAD_ADDRESS_NAME = "lastRoadAddressName" } - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) @@ -68,10 +61,11 @@ class MainActivity : AppCompatActivity() { mapView.start(object : MapLifeCycleCallback() { override fun onMapDestroy() { // 지도 API가 정상적으로 종료될 때 호출됨 + Log.d("MainActivity", "Map destroyed") } override fun onMapError(error: Exception) { - + Log.e("MainActivity", "Map error: ${error.message}") showErrorScreen(error) } }, object : KakaoMapReadyCallback() { @@ -80,7 +74,6 @@ class MainActivity : AppCompatActivity() { labelLayer = kakaoMap.labelManager?.layer!! // 마지막 마커 위치 불러오기 loadLastMarkerPosition() - } }) @@ -88,7 +81,6 @@ class MainActivity : AppCompatActivity() { val searchEditText = findViewById(R.id.search_edit_text) searchEditText.setOnClickListener { val intent = Intent(this, SearchActivity::class.java) - intent.putExtra("selectedItemsSize", selectedItems.size) selectedItems.forEachIndexed { index, mapItem -> intent.putExtra("id_$index", mapItem.id) @@ -115,18 +107,6 @@ class MainActivity : AppCompatActivity() { bottomSheetLayout.visibility = View.GONE } - // 지도 -> 검색페이지 돌아갈 때 저장된 검색어 목록 그대로 저장 - private fun processIntentData() { - val placeName = intent.getStringExtra("place_name") - val roadAddressName = intent.getStringExtra("road_address_name") - val x = intent.getDoubleExtra("x", 0.0) - val y = intent.getDoubleExtra("y", 0.0) - if (placeName != null && roadAddressName != null) { - addLabel(placeName, roadAddressName, x, y) - - } - } - override fun onResume() { super.onResume() mapView.resume() // MapView의 resume 호출 @@ -135,7 +115,6 @@ class MainActivity : AppCompatActivity() { override fun onPause() { super.onPause() mapView.pause() // MapView의 pause 호출 - } fun showErrorScreen(error: Exception) { @@ -177,7 +156,7 @@ class MainActivity : AppCompatActivity() { selectedItems.clear() val selectedItemsSize = it.getIntExtra("selectedItemsSize", 0) for (i in 0 until selectedItemsSize) { - val id = it.getStringExtra("id_$i") ?: "" + val id = it.getIntExtra("id_$i",0) val place_name = it.getStringExtra("place_name_$i") ?: "" val road_address_name = it.getStringExtra("road_address_name_$i") ?: "" val category_group_name = it.getStringExtra("category_group_name_$i") ?: "" diff --git a/app/src/main/java/campus/tech/kakao/map/ui/SearchActivity.kt b/app/src/main/java/campus/tech/kakao/map/ui/SearchActivity.kt index 09c7fe77..e4de49f8 100644 --- a/app/src/main/java/campus/tech/kakao/map/ui/SearchActivity.kt +++ b/app/src/main/java/campus/tech/kakao/map/ui/SearchActivity.kt @@ -6,25 +6,22 @@ import android.text.Editable import android.text.TextWatcher import android.view.View import campus.tech.kakao.map.databinding.ActivitySearchBinding -import androidx.lifecycle.ViewModelProvider +import androidx.activity.viewModels import android.widget.Toast -import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import android.app.Activity import android.content.Intent import campus.tech.kakao.map.viewmodel.MapViewModel -import campus.tech.kakao.map.viewmodel.MapViewModelFactory import campus.tech.kakao.map.R -import campus.tech.kakao.map.util.SQLiteHelper import campus.tech.kakao.map.model.MapItem -import campus.tech.kakao.map.repository.MapRepositoryImpl +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class SearchActivity : AppCompatActivity() { private lateinit var binding: ActivitySearchBinding - private lateinit var sqLiteHelper: SQLiteHelper - lateinit var viewModel: MapViewModel + private val viewModel: MapViewModel by viewModels() private lateinit var searchAdapter: SearchAdapter private lateinit var selectedAdapter: SelectedAdapter @@ -33,13 +30,6 @@ class SearchActivity : AppCompatActivity() { binding = ActivitySearchBinding.inflate(layoutInflater) setContentView(binding.root) - sqLiteHelper = SQLiteHelper(this) - sqLiteHelper.writableDatabase - - val repository = MapRepositoryImpl(application) - val viewModelFactory = MapViewModelFactory(application, repository) - viewModel = ViewModelProvider(this, viewModelFactory).get(MapViewModel::class.java) - setupRecyclerViews() setupSearchEditText() setupClearTextButton() @@ -48,7 +38,7 @@ class SearchActivity : AppCompatActivity() { val selectedItemsSize = intent.getIntExtra("selectedItemsSize", 0) val selectedItems = mutableListOf() for (i in 0 until selectedItemsSize) { - val id = intent.getStringExtra("id_$i") ?: "" + val id = intent.getIntExtra("id_$i", 0) val place_name = intent.getStringExtra("place_name_$i") ?: "" val road_address_name = intent.getStringExtra("road_address_name_$i") ?: "" val category_group_name = intent.getStringExtra("category_group_name_$i") ?: "" @@ -109,16 +99,16 @@ class SearchActivity : AppCompatActivity() { } private fun observeViewModel() { - viewModel.searchResults.observe(this, Observer { results -> + viewModel.searchResults.observe(this) { results -> searchAdapter.submitList(results) binding.noResultsTextView.visibility = if (results.isEmpty() && !viewModel.searchQuery.value.isNullOrEmpty()) View.VISIBLE else View.GONE binding.searchResultsRecyclerView.visibility = if (results.isEmpty()) View.GONE else View.VISIBLE - }) + } - viewModel.selectedItems.observe(this, Observer { selectedItems -> + viewModel.selectedItems.observe(this) { selectedItems -> selectedAdapter.submitList(selectedItems) - }) + } } fun performSearch(query: String) { diff --git a/app/src/main/java/campus/tech/kakao/map/viewmodel/MapViewModel.kt b/app/src/main/java/campus/tech/kakao/map/viewmodel/MapViewModel.kt index a4a379b8..957ef520 100644 --- a/app/src/main/java/campus/tech/kakao/map/viewmodel/MapViewModel.kt +++ b/app/src/main/java/campus/tech/kakao/map/viewmodel/MapViewModel.kt @@ -7,9 +7,15 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.viewModelScope import campus.tech.kakao.map.model.MapItem import campus.tech.kakao.map.repository.MapRepository +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch +import javax.inject.Inject -class MapViewModel(application: Application, private val repository: MapRepository) : AndroidViewModel(application) { +@HiltViewModel +class MapViewModel @Inject constructor( + application: Application, + private val repository: MapRepository +) : AndroidViewModel(application) { val searchQuery = MutableLiveData() diff --git a/build.gradle.kts b/build.gradle.kts index ace33df3..a4a872c2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,8 +1,9 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { id("com.android.application") version "8.3.1" apply false id("org.jetbrains.kotlin.android") version "1.9.0" apply false id("org.jlleitschuh.gradle.ktlint") version "12.1.0" apply false + id("com.google.dagger.hilt.android") version "2.46.1" apply false + id("org.jetbrains.kotlin.kapt") version "1.9.0" apply false } allprojects { diff --git a/settings.gradle.kts b/settings.gradle.kts index fe9e1af2..5ae8ebc0 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,5 +18,4 @@ dependencyResolutionManagement { } rootProject.name = "android-map-search" -include(":app") include(":app") \ No newline at end of file