[심화] NaverMap API 사용하기 (9)

쓰리원·2022년 7월 6일
0

안드로이드 지도API

목록 보기
11/12
post-thumbnail

1. Bottom Navigation 추가 Fragment로 나누기

jetpack AAC navigtion을 활용해서 bottom bar를 만들어서 fragment로 메뉴에 맞는 창으로 이동되게 만들어 줍니다.

1. navigation_graph.xml

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/navigation_graph"
    app:startDestination="@id/home">

    <include app:graph="@navigation/map" />
    <include app:graph="@navigation/home" />
    <include app:graph="@navigation/chat" />

</navigation>

app:startDestination="@id/home" 코드로부터 home.xml이 처음 메인으로 활성화 되게 됩니다. 만약에 app:startDestination="@id/map"로 코드를 바꾸면 map.xml이 처음 메인으로 활성화 되게 됩니다.

2. home.xml

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/home"
    app:startDestination="@id/homeFragment">
    <fragment
        android:id="@+id/homeFragment"
        android:name="com.project.navermap.screen.home.HomeFragment"
        android:label="HomeFragment" />
</navigation>

3. chat.xml

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/chat"
    app:startDestination="@id/chatFragment">
    <fragment
        android:id="@+id/chatFragment"
        android:name="com.project.navermap.screen.chat.ChatFragment"
        android:label="ChatFragment" />
</navigation>

4. map.xml

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/map"
    app:startDestination="@id/mapFragment">
    <fragment
        android:id="@+id/mapFragment"
        android:name="com.project.navermap.screen.map.mapFragment.MapFragment"
        android:label="MapFragment" />
</navigation>

5. activity_main.xml

   <androidx.fragment.app.FragmentContainerView
        android:id="@+id/fragmentContainer"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toTopOf="@id/bottomNav"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/toolbar"
        app:navGraph="@navigation/navigation_graph" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottomNav"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        app:labelVisibilityMode="labeled"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:menu="@menu/bottom_navigation_menu" />

6. MainActivity.kt

class MainActivity : AppCompatActivity() {

	....
    
    private val navController by lazy {
        val hostContainer =
            supportFragmentManager
                .findFragmentById(R.id.fragmentContainer)
                    as NavHost

        hostContainer.navController
    }
    
	override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        binding.bottomNav.setupWithNavController(navController)
        
    }
        
    ....
}   

navController를 선언하고 안의 fragment를 위의 xml로 매칭시켜서 bottombar의 클릭된 메뉴에 따라 fragment가 이동하게 됩니다. 관련 내용의 추가 설명은 아래의 코드 리뷰를 참고하시면 됩니다.

팀-프로젝트-Yu-Market-두번째-코드-리뷰-3편

위와 같이 navigation을 설정해주면 아래의 빨간색 박스인 MainActivty를 제외한 각각의 fragment에 맞게 화면의 내용이 바뀌게 됩니다. 이로 인해 화면의 기능에 따른 코드 분할을 할 수 있게 됩니다.

2. ViewModel을 활용한 데이터 저장 및 전달

이해를 위한 기본 선수 지식 필요

1. AAC ViewModel이란?
2. ViewModel로 데이터 전달

ViewModel을 통해서 얻을 수 있는 기능은 여러가지가 있습니다. 그 중 하나로 ViewModel을 통해서 MainActivity에 종속되어 있는 Fragment는 MainActivity의 ViewModel data를 언제든지 접근해서 사용할 수 있습니다.

공통의 Fragment가 동일한 data를 사용해서 중복적인 작업을 한곳에서 처리 할 수 있으면 보일러 플레이트 코드 양산을 막고, data의 관리 또한 쉬워질 수 있습니다. 반대로 생기는 문제로는 종속성으로 인해 Unit test가 어려울 수도 있다는 것 입니다. 그러나 적절히 쓰면 유용한 기능입니다.

class MapFragment : Fragment() , OnMapReadyCallback {

	private val activityViewModel by activityViewModels<MainViewModel>()

	....
    
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
    
    ....
    
        binding.btnCurLocation.setOnClickListener {

            try {
                viewModel.getMap()?.cameraPosition = CameraPosition(
                    LatLng(activityViewModel.getCurrentLocation().latitude,
                        activityViewModel.getCurrentLocation().longitude),
                    15.0)
            } catch (ex: Exception) {
                Toast.makeText(context, "CurLocation 초기화 중", Toast.LENGTH_SHORT).show()
            }
        }

        binding.btnDestLocation.setOnClickListener {

            try {
                viewModel.getMap()?.cameraPosition = CameraPosition(
                    LatLng(activityViewModel.getDestinationLocation().latitude,
                        activityViewModel.getDestinationLocation().longitude),
                    15.0)

                viewModel.updateLocation(activityViewModel.getDestinationLocation())

            } catch (ex: Exception) {
                Toast.makeText(context, "DestLocation 초기화 중", Toast.LENGTH_SHORT).show()
            }
        }
        
        ....
    }
}    

activityViewModel.getCurrentLocation().latitude (현재 위치의 위도) activityViewModel.getCurrentLocation().longitude (현재 위치의 경도)
activityViewModel.getDestinationLocation().latitude (주소지의 위도)
activityViewModel.getDestinationLocation().longitude (주소지의 경도)

이렇게 activity의 ViewModel에서 변경된 위도 경도 값을 MapFragment에서 접근해서 사용 할 수 있게 됩니다. 그런데 왜 이런식으로 사용하게 되는지 의문이 들 수 있습니다.

그 이유는 현재 위치를 바꾸게 되는 것은 MainActivty의 맨 위의 주소창을 클릭시 열리는 MyLocationActivity를 통해서 값을 반환 받게 됩니다. 그 이후 위 값은 MainActivity에서 다시 MainViewModel로 이동해서 저장하게 되는 것 입니다. 그래서 앱 구조상 MainViewModel에 저장이 될 수 밖에 없습니다. 아래의 코드로 다시 한번 설명해 보겠습니다.

class MainActivity : AppCompatActivity() {

	private val viewModel: MainViewModel by viewModels()
    
	....
    
    private val changeLocationLauncher =
        registerForActivityResult(ActivityResultContracts.StartActivityForResult())
        { results ->
            results.data?.getParcelableExtra<MapSearchInfoEntity>(MyLocationActivity.MY_LOCATION_KEY)?.let {
                    mapSearchInfoEntity ->
                    getReverseGeoInformation(mapSearchInfoEntity.locationLatLng)
                    viewModel.setDestinationLocation(mapSearchInfoEntity.locationLatLng)
            }
        }
        
        
    @SuppressLint("MissingPermission")
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        ....

        binding.locationTitleTextView.setOnClickListener {

            try {
                changeLocationLauncher.launch(
                    MyLocationActivity.newIntent(this, mapSearchInfoEntity)
                )
            } catch (ex: Exception) {
                Toast.makeText(this, "myLocation 초기화 중", Toast.LENGTH_SHORT).show()
            }
            
        }
    }
    
    ....
}

class MainViewModel() : ViewModel() {

    private lateinit var destLocation: LocationEntity
    lateinit var curLocation: Location

    fun setCurrentLocation(loc: Location) { curLocation = loc }

    fun getCurrentLocation() : Location { return curLocation }

    fun setDestinationLocation(loc: LocationEntity) { destLocation = loc }

    fun getDestinationLocation(): LocationEntity { return destLocation }
    
}

changeLocationLauncher는 registerForActivityResult입니다. 그래서 MyLocationActivity에서 보낸 값을results.data?.getParcelableExtra를 통해서 MainActivity는 data값을 반환 받을 수 있습니다. 반환 받는 data값은 mapSearchInfoEntity가 되는데, 그 중 viewModel.setDestinationLocation(mapSearchInfoEntity.locationLatLng) 를 통해 MainViewModel에 저장하는 data값은 위도 경도 위치 값인 mapSearchInfoEntity.locationLatLng이 됩니다.

그런데 MapFragment에서는 이렇게 바뀐 주소지 위치의 위도 경도가 필요한 상황입니다. 그러다보니 activityViewModels()를 통해서 MainViewModel을 접근해서 data를 사용하는 것 입니다. 이것은 data를 저장하고 활용하는 하나의 방법입니다.


다른 방법으로는 SharedPreferences를 사용해서 어디에서든지 접근할 수 있게도 할 수 있을 것 입니다. 이렇게 되면 MainActivty에 한정짓지 않고 앱의 어느 Activity에서든지 간에 사용이 가능 할 수도 있습니다.

profile
가장 아름다운 정답은 서로의 협업안에 있다.

0개의 댓글