SW 공부노트
[Android Basics in Kotlin] Navigation: 공유 ViewModel / 데이터결합, 리스너결합 / 백스택 본문
[Android Basics in Kotlin] Navigation: 공유 ViewModel / 데이터결합, 리스너결합 / 백스택
요빈 2023. 3. 18. 14:18https://developer.android.com/courses/android-basics-kotlin/unit-3
Android Kotlin Basics in Kotlin | Android Basics in Kotlin - Navigation | Android Developers
Enhance your users’ ability to navigate across, into and back out from the various screens within your app for a consistent and predictable user experience.
developer.android.com
해당 글은 위 사이트의 PATHWAY 4 과정인 Advanced navigation app examples를 공부하며 작성한 글입니다.
이 과정에서는 공유 ViewModel을 사용해 동일한 활동 내 프래그먼트 간에 데이터를 공유하는 방법 및 LiveData 변환과 같은 개념을 배웁니다.
탐색 그래프
탐색 그래프 설정
nav_graph.xml 파일에 들어가면 Navigation Editor 내에 4개의 프래그먼트가 있는 걸 확인할 수 있다.
프래그먼트 대상을 연결해 화살표가 생기면 성공적으로 연결된 것으로 두 화면간 이동이 가능함을 나타낸다.
탐색 그래프를 정의할 때 시작 대상을 지정할 수도 있다.
화면들을 보면 특정 한 화면 옆에 작은 집 아이콘이 있는 것을 볼 수 있다.
이는 해당 프래그먼트가 NavHost에 표시될 첫 번째 프래그먼트임을 나타낸다.
프래그먼트를 마우스 오른쪽 버튼으로 클릭하고 Set as Start Destination 옵션을 선택해 시작 대상을 변경할 수 있다.
프래그먼트 간 이동
프래그먼트 코틀린 파일에서 위에서 설정한 탐색 작업을 이용해 화면 간 이동 코드를 추가한다.
findNavController() 메서드를 통해 NavController를 가져오고, 거기에서 navigate()를 호출해 작업 ID를 인수로 전달한다.
그러면 해당 작업 ID를 갖는 작업(Navigation)이 행해진다. 즉, 화면 간 이동이 발생한다.
fun orderCupcake(quantity: Int) {
findNavController().navigate(R.id.action_startFragment_to_flavorFragment)
}
앱 바에서 제목 업데이트
프래그먼트의 기능에 따라 앱 바에 관련성이 높은 제목을 제공하는 것이 좋다.
NavController를 이용해 프래그먼트의 앱 바에 있는 제목을 변경하고 위로 버튼을 표시한다.
MainActivity.kt의 onCreate() 메서드를 재정의해 NavController를 설정한다.
setupActionBarWithNavController 메서드를 통해 라벨 속성을 기반으로 앱 바에 프래그먼트 제목이 표시되고, 최상위 대상에 있지 않은 경우 항상 위로(<-) 버튼이 표시된다.
class MainActivity : AppCompatActivity(R.layout.activity_main) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// NavHostFragment에서 NavController의 인스턴스를 가져온다.
// 아래 프래그먼트 id는 activity_main의 FragmentContainerView의 id이다.
val navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
val navController = navHostFragment.navController
// NavController을 이용한 액션바 설정
// 이를 통해 라벨(label 속성)을 기반으로 앱 바에 제목이 표시되고,
// 최상위 대상이 아닌 경우 항상 위로 버튼이 표시된다.
setupActionBarWithNavController(navController)
}
}
공유 ViewModel
공유 ViewModel을 사용해 앱 데이터를 단일 ViewModel에 저장하고, 앱의 여러 프래그먼트는 활동 범위를 사용해 공유 ViewModel에 액세스할 수 있다.
대부분의 프로덕션 앱에서 프래그먼트 간 데이터를 공유하는 것은 일반적이다.
ViewModel 클래스 만들기
ViewModel에서 뷰 모델 데이터는 public 변수로 노출하지 않는 것이 좋다.
공개 변수로 노출되면 앱 데이터가 외부 클래스에 의해 예상치 못한 방식으로 수정될 수 있으며, 앱에서 처리하지 못할 케이스가 발생할 수도 있다.
따라서 이러한 속성을 private으로 만들고, 지원 속성을 구현해 필요한 경우 각 속성의 public 변수에 접근하면 된다. 이러한 경우 네이밍 컨벤션 규칙은 변경 가능한 private 속성의 이름 앞에 밑줄(_)을 붙이는 것이다.
ViewModel 사용해 UI 업데이트
공유 ViewModel 구현의 주요 차이점은 UI 컨트롤러에서 뷰 모델에 액세스하는 방식이다.
프래그먼트 인스턴스 대신 활동 인스턴스를 사용하여 뷰 모델에 접근하게 된다.
즉, 뷰 모델을 여러 프래그먼트 간에 공유할 수 있다.
뷰 모델을 사용하기 위해선 뷰 모델을 사용할 활동/프래그먼트 파일에서 뷰 모델 객체를 선언한다.
공유 ViewModel을 사용하려면 viewModels() 대리자 클래스 대신 activityViewModels() 클래스를 사용해 뷰 모델을 초기화한다.
activityViewModels()는 fragment-ktx 라이브러리에서 사용 가능하며, viewModels()와 activityViewModels()의 차이점은 다음과 같다.
여기서는 코틀린의 속성위임이 사용된다. 속성 위임을 사용하면 getter-setter 책임을 대리자 클래스에게 넘길 수 있다.
이 대리자 클래스는 속성의 getter 및 setter 함수를 제공하고 변경사항을 처리한다.
대리자 속성은 다음과 같이 by 절 및 대리자 클래스 인스턴스를 사용해 정의된다.
private val sharedViewModel: OrderViewModel by activityViewModels()
위 코드는 이 공유 ViewModel를 사용할 모든 프래그먼트 파일에서 선언하면 된다.
데이터 결합(data binding)
데이터 결합은 쉽게 말해 레이아웃 파일에서 데이터를 변경하는 것을 말한다.
데이터 결합을 통해 업데이트를 자동으로 설정하면 코드에서 UI를 직접 업데이트하는 것을 잊은 경우에 오류 발생률이 줄어든다.
1. 레이아웃 파일의 최상단 태그를 <layout>으로 변경
2. <layout> 태그 내 <data> 태그 추가 후 <variable> 태그에 사용할 레이아웃 변수 추가(name, type 작성)
3. 프래그먼트 코틀린 파일에서 변수 인스턴스를 레이아웃의 인스턴스와 결합
-> 레이아웃 파일에 선언한 데이터를 코틀린 파일에서 연결
아래 코드는 공유 뷰 모델을 예로 들고 있다.
binding?.apply {
viewModel = sharedViewModel
...
}
// binding?.viewModel = sharedViewModel과 같은 의미
* apply 함수
apply는 코틀린 표준 라이브러리의 범위함수로, 객체의 컨텍스트 내에서 코드 블록을 실행하며, 임시 범위를 형성한다. 그러면 이 범위에서는 이름을 사용하지 않고 객체에 액세스할 수 있다.
apply의 일반적인 사용 사례는 객체를 구성하는 것이다.
clark.apply {
firstName = "Clark"
lastName = "James"
age = 18
}
// The equivalent code without apply scope function would look like the following.
clark.firstName = "Clark"
clark.lastName = "James"
clark.age = 18
리스너 결합
리스너 결합은 onClick 이벤트와 같은 이벤트가 발생할 때 실행되는 람다 표현식이다.
즉, 리스너 결합을 사용하면 레이아웃 파일에서 버튼에 이벤트 리스너를 추가할 수 있다.
아래 예시에서 onClick에 리스너 결합이 사용되었고, checked에 데이터 결합이 사용되었다.
<RadioButton
android:id="@+id/coffee"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/coffee"
android:onClick="@{()-> viewModel.setFlavor(@string/coffee)}"
android:checked="@{viewModel.flavor.equals(@string/coffee)}"/>
라디오 버튼에서의 리스너 결합 외에도 프래그먼트 클래스의 버튼 클릭 리스너를 레이아웃에 결합하는 경우도 있다.
1. 레이아웃 파일에 프래그먼트 변수 추가
name: "startFragment", type="com.example.cupcake.StartFragment"
2. 프래그먼트의 onViewCreated() 메서드 내에서 새 데이터 변수를 프래그먼트 인스턴스에 결합
데이터 바인딩 변수가 1개일 경우 apply 함수가 필요 없지만 여러 개인 경우 apply 함수로 한번에 처리한다.
apply 함수를 사용하지 않을 경우 Fragment값은 this 키워드를 사용해도 되지만, 현재는 apply 블록 내에 있어 this 키워드가 결합 인스턴스를 참조하기 때문에 @를 사용해 프래그먼트 클래스 이름을 명시적으로 지정한다.
binding?.apply {
lifecycleOwner = viewLifecycleOwner
viewModel = sharedViewModel
flavorFragment = this@FlavorFragment
}
3. 레이아웃 파일의 버튼에서 onClick 속성에 결합 표현식 지정
* SimpleDateFormat() : 날짜 형식을 지정(날짜 -> 텍스트)하고 파싱(텍스트 -> 날짜)하는 클래스
* Locale 객체 : 특정한 지리적, 정치적 또는 문화적 지역을 나타냄
-> Locale.getDefault() 메서드를 통해 사용자 기기에 설정된 언어정보를 가져옴
private fun getPickupOptions():List<String>{
// 픽업 가능한 날자 리스트 반환
val option = mutableListOf<String>()
// 형식 지정 문자열 -> 패턴 문자열 및 언어 전달
val formatter = SimpleDateFormat("E MMM d",Locale.getDefault())
// 현재 시간 및 날짜 포함
val calendar = Calendar.getInstance()
repeat(4){
option.add(formatter.format(calendar.time))
calendar.add(Calendar.DATE, 1)
}
return option
}
LiveData 관찰하도록 수명 주기 소유자 설정
데이터 결합을 사용하면 관찰 가능한 값이 변경되는 경우 결합된 UI 요소가 자동으로 업데이트 된다.
즉, UI 요소 속성값에 LiveData가 포함된 결합 표현식이 있을 경우 해당 값이 자동으로 업데이트 된다.
UI 요소가 자동으로 업데이트되도록 하려면 binding.lifecycleOwner를 앱의 수명 주기 소유자와 연결해야 한다.
관찰할 LiveData가 존재하는 프래그먼트 파일 내에서 결합 객체에 수명 주기 소유자 설정을 해주면 된다.
lifecycleOwner는 ViewDataBinding 클래스 값이고, viewLifecycleOwner는 Fragment 클래스 내 설정값이다.
binding?.apply {
lifecycleOwner = viewLifecycleOwner
...
}
LiveData 변환을 통한 가격 형식 지정
LiveData 변환 메서드는 LiveData 소스에서 데이터 조작을 실행하고 LiveData 객체를 반환한다.
간단히 말해, LiveData 값을 다른 값으로 변환한다. 관찰자가 LiveData 객체를 관찰하고 있지 않다면 이러한 변환은 계산되지 않는다.
LiveData 변환을 사용하는 몇 가지 실시간 예는 다음과 같다.
Transformations.map() 은 변환 함수 중 하나로, LiveData 소스를 조작하고 관찰된 업데이트 값을 반환한다.
요약
탐색 및 백 스택
Up 버튼 동작 구현
Up 버튼은 상위 계층에 있는 화면으로 이동하는 버튼이다.
이전에 방문했던 화면으로 되돌아가는 Back 버튼과는 차이가 있음을 명심해야 한다.
Up 버튼 동작 구현은 프래그먼트를 화면에 출력하는 FragmentContainerView를 가진 액티비티 파일에 추가된다.
탐색 그래프를 다루는 navController가 위로 동작을 처리하도록 아래 함수 재정의 코드를 추가한다.
override fun onSupportNavigateUp(): Boolean {
return navController.navigateUp() || super.onSupportNavigateUp()
}
태스크 및 백 스택
Android에서 활동은 태스크 내에 존재한다.
태스크는 사용자가 이메일 확인, 사진 촬영 등 특정한 일을 할 때 상호작용하는 액티비티의 모음이다.
액티비티(활동)는 백 스택이라는 스택으로 배열되며, 사용자가 방문하는 각각의 새 활동은 작업의 백 스택으로 푸시된다.
맨 위 스택 액티비티는 현재 사용자가 상호작용할 수 있도록 생성, 시작 및 재개되었음을 의미하고,
그 아래 스택은 백그라운드로 전환되었다가 중지된 활동을 의미한다.
백 스택은 사용자가 뒤로 이동하고자 하는 경우 유용하게 사용된다.
Android는 스택 맨 위에 있는 현재 액티비티는 삭제하고 폐기한 후 그 아래 있는 액티비티를 다시 시작한다.
백 스택은 사용자가 열어본 액티비티를 추적하는 것과 같은 방식으로 Jetpack Navigation 컴포넌트를 사용해 사용자가 방문한 프래그먼트 대상도 추적할 수 있다. 네비게이션 라이브러리를 사용하면 사용자가 Back 버튼을 누를 때마다 백 스택에서 프래그먼트 대상을 없앨 수 있다.
취소 버튼
사용자가 주문 취소 버튼을 누르면 다시 startFragment로 돌아가게 하는 취소 버튼을 만들려고 한다.
취소 버튼 생성 과정은 다음과 같다.
1. 탐색 작업 추가: nav_graph 파일에서 다른 프래그먼트에서 시작 프래그먼트로 이동하는 작업 추가
2. 리스너 결합
2단계까지 진행했을 경우, summaryFragment -> cancel -> startFragment 인 상태에서 Back 버튼을 누르면,
유효하지 않는 데이터가 담긴 summaryFragment로 돌아가는 버그가 발생한다.
따라서 이 탐색 버그를 수정하기 위해 탐색 작업을 사용해 탐색 구성요소가 추가 대상을 백 스택에서 없애도록 해야한다.
3. 백 스택에서 추가 대상 없애기
- popUpTo
탐색 그래프의 탐색 작업에 app:popUpTo 속성을 포함하면 지정된 대상에 도달할 때까지 백 스택에서 pop한다.
예를 들어 app:popUpTo="@id/startFragment"를 지정하면 스택에 StartFragment에 도달할 때까지 백 스택에 있는 대상이 없어진다.
즉, 왼쪽 백 스택 상태에서 제일 하단에 위치한 startFragment에 도달할 때까지 pop 작업이 이루어진다는 의미이다. 결과적으로는 두 개의 StartFragment가 쌓여있는 모습이 된다.
- popUpToInclusive
위 단계까지 진행된 경우 startFragment에서 Back 버튼을 눌러 앱에서 나가려고 해도 백 스택에 StartFragment 인스턴스가 두 개 쌓여있기 때문에 Back 버튼을 두 번 눌러야 앱에서 빠져나갈 수 있다.
popUpToInclusive 속성은 지정된 대상에 이르기까지 해당 대상을 포함한 모든 대상을 백 스택에서 없애도록 요청한다. 이렇게 하면 백 스택에 새 프래그먼트 하나만 남아 Back 버튼을 한 번만 눌러도 앱이 종료된다.
즉, popUpTo 속성과 popUpToInclusive 속성을 지정하면 올바른 취소 작업이 이루진다.
추가 사항
- 암시적 인텐트
- Android 수량 문자열 : string 파일에서 plurals 리소스를 사용하면 수량에 따라 사용할 다른 문자열 리소스(단수 및 복수)를 지정할 수 있음
ViewModel 테스트
LiveData가 변경되면 이에 맞춰 UI가 업데이트 된다. UI 변경은 '기본 스레드'에서 실행된다.
그러나 LiveData 객체는 기본 스레드에 액세스할 수 없으므로 LiveData 객체가 기본 스레드를 호출하면 안된다고 명시적으로 지정해야 한다.
이를 위해서 LiveData 객체를 테스트할 때마다 특정 테스트 규칙(Rule)을 제공해야 한다.
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()
또한 관찰할 LiveData를 관찰하기 위한 코드를 작성한다.
orderViewModel.quantity.observeForever{}
'안드로이드 > 안드로이드 Kotlin' 카테고리의 다른 글
[Android Basics in Kotlin] Internet: Retrofit / BindingAdapter (0) | 2023.03.22 |
---|---|
[Android Basics in Kotlin] Navigation: 적응형 레이아웃(Adaptable Layout) (0) | 2023.03.19 |
[Android Basics in Kotlin] Navigation: 아키텍처 구성요소 / 뷰모델 / LiveData / dataBinding (0) | 2023.03.15 |
[Android Basics in Kotlin] Navigation: 프래그먼트 / 네이게이션 컴포넌트 (0) | 2023.03.15 |
[코틀린-Kotlin Bootcamp] 4. Object-oriented programming (0) | 2023.03.08 |