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가 이동하게 됩니다. 관련 내용의 추가 설명은 아래의 코드 리뷰를 참고하시면 됩니다.
위와 같이 navigation을 설정해주면 아래의 빨간색 박스인 MainActivty를 제외한 각각의 fragment에 맞게 화면의 내용이 바뀌게 됩니다. 이로 인해 화면의 기능에 따른 코드 분할을 할 수 있게 됩니다.
이해를 위한 기본 선수 지식 필요
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에서든지 간에 사용이 가능 할 수도 있습니다.