예제에서 컵케이크 주문 어플을 만든다.
이런 경우 4개의 프래그먼트가 ViewModel을 공유하는 것이 효과적일 것이다.
//sharedViewModel라는 변수는 OrderViewModel 타입이고 대리자 클래스 activityViewModels()
private val sharedViewModel: OrderViewModel by activityViewModels()
공유 뷰 모델을 사용하려면 4개의 프래그먼트에서 viewModels()
대리자 클래스 대신 activityViewModels()
를 사용하여 OrderViewModel
을 초기화한다.
viewModels()
는 현재 프래그먼트로 범위가 지정된 ViewModel
인스턴스를 제공한다. 따라서 인스턴스는 프래그먼트마다 다르다.
activityViewModels()
는 현재 액티비티로 범위가 지정된 ViewModel
인스턴스를 제공한다. 따라서 인스턴스는 동일한 액티비티의 여러 프래그먼트 간에 동일하게 유지된다.
<layout ...>
<data>
<variable
name="viewModel"
type="com.example.cupcake.model.OrderViewModel" />
<variable
name="flavorFragment"
type="com.example.cupcake.FlavorFragment" />
</data>
<ScrollView ...>
...
unit3-3에서 배운것과 마찬가지로 <data>
태그를 추가해준다.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
/*
apply는 범위함수.
binding.viewModel=sharedViemodel
...
이라고 보면 된다.
*/
binding?.apply {
viewModel = sharedViewModel //공유 뷰 모델 인스턴스와 결합
lifecycleOwner = viewLifecycleOwner //변수가 바뀌면 ui에 적용되도록...
flavorFragment = this@FlavorFragment //결합 인스턴스 참조
}
}
이후 onViewCreated()
내에서 뷰 모델 인스턴스를 레이아웃의 공유 뷰 모델 인스턴스와 결합한다.
리스너 결합
<RadioGroup
...>
...
<RadioButton
android:id="@+id/vanilla"
...
android:onClick="@{() -> viewModel.setFlavor(@string/vanilla)}"
.../>
<RadioButton
android:id="@+id/chocolate"
...
android:onClick="@{() -> viewModel.setFlavor(@string/chocolate)}"
.../>
...
</RadioGroup>
이런걸 리스너 결합이라고 한다. 라디오 버튼을 누르면 뷰모델의 setFlavor 함수를 호출해서 선택한 맛으로 set해준다.
Up 버튼(뒤로 버튼) 동작 구현
class MainActivity : AppCompatActivity(R.layout.activity_main) {
private lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//이부분..
val navHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
navController = navHostFragment.navController
setupActionBarWithNavController(navController)
}
}
MainActivity
에 탐색 컨트롤러를 사용하여 앱 바(작업 모음이라고도 함)를 설정하는 코드가 있다. navController
변수를 사용해서 up 버튼을 구현하자.
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp() || super.onSupportNavigateUp()
}
up버튼 누르면 뒤로가기 작업을 수행하도록 navController
에 요청한다. 이제 up 버튼이 잘 작동한다.
task와 back stack(백스택)
안드로이드에서 액티비티는 task 내에 존재한다. task는 사용자가 특정한 일을 할 때 상호작용하는 액티비티의 모음이다.
액티비티는 백 스택이라는 스택으로 배열되며, 사용자가 방문하는 각각의 새 액티비티는 작업의 백 스택으로 푸시된다. 스택 맨 위에 있는 액티비티는 사용자가 현재 상호작용하고 있는 액티비티이고, 스택에서 그 아래에 있는 액티비니는 백그라운드로 전환되었다가 중지되었다.
백 스택은 사용자가 뒤로 이동하고자 하는 경우 유용하다. Android는 스택 맨 위에 있는 현재 액티비티를 삭제하고 폐기한 후 그 아래에 있는 액티비티를 다시 시작할 수 있다. (이전 액티비티가 포그라운드로 이동)
※앱을 연 후에 홈버튼을 누르면 백스택에 있는 모든 액티비티가 백그라운드로 전환되고, 다시 앱을 열었을때 최상단의 액티비티를 보여준다.
이러한 백 스택은 Jetpack 탐색 구성요소의 도움으로 사용자가 방문한 프래그먼트 대상도 추적할 수 있다.
사용자가 summaryFragment에서 취소 버튼을 누르면 백스택에서 프래그먼트들이 한꺼번에 없애야 한다. 이렇게 동작하도록 수정해 보자.
먼저 탐색 그래프를 다음과 같이 수정한다.
fun cancelOrder() {
sharedViewModel.resetOrder()
//R.id.어쩌구는 각 프래그먼트에 맞게..
findNavController().navigate(R.id.action_flavorFragment_to_startFragment)
}
<Button
android:id="@+id/cancel_button"
android:onClick="@{() -> pickupFragment.cancelOrder()}" ... />
각 프래그먼트에 cancel 함수를 만들고 버튼에 연결시켜주면 된다.
여기까지 완료했을시... 백스택에서 아예 없어지는게 아니라 이렇게 쌓인다. 그러면 무지성 뒤로가기를 눌렀을때 초기값을 담고 있는 이전 주문 화면이 뜬다.
이를 고치기 위해 탐색 그래프로 가서 startFragment
들로 향하는 화살표들을 클린한 뒤 popUpTo
를 startFragment
로 설정해준다. startFragment
에 이르기까지 백 스택에 있는 모든 대상이 없어진다. 단 이러면 백스택에 startFragment
가 2개 쌓이게 되는데..
이 문제는 popUpToinclusive
를 true로 설정해주면 해결할 수 있다.
공유하기
fun sendOrder() {
//뭘 얼만큼 주문했고 가격은 얼만지 스트링으로 만듬
val orderSummary = getString(
R.string.order_details,
sharedViewModel.quantity.value.toString(),
sharedViewModel.flavor.value.toString(),
sharedViewModel.date.value.toString(),
sharedViewModel.price.value.toString()
)
//암시적 인텐트 생성(하단에서 반쯤 올라오는 공유 창)
val intent = Intent(Intent.ACTION_SEND)
.setType("text/plain")
.putExtra(Intent.EXTRA_SUBJECT, getString(R.string.new_cupcake_order))
.putExtra(Intent.EXTRA_TEXT, orderSummary)
//인텐트를 처리할 수 있는 앱(쥐메일 등..)이 있을때만 실행
if (activity?.packageManager?.resolveActivity(intent, 0) != null) {
startActivity(intent)
}
}