PATHWAY 2 Introduction to the Navigation component
1. 프래그먼트 및 프래그먼트 수명 주기
- 프래그먼트는 간단하게 말해 앱의 사용자 인터페이스에서 재사용 가능한 부분이다.
- 액티비티와 마찬가지로 프래그먼트는 수명 주기가 있고 사용자 입력에 응답할 수 있다.
- 프래그먼트는 액티비티가 화면에 표시될 때 액티비티의 뷰 계층 구조 내에 항상 포함된다.
- 재사용성과 모듈성을 강조하므로 단일 활동에서 여러 프래그먼트를 동시에 호스팅할 수도 있다.
2. 프래그먼트 구현
- LetterListFragment 와 WordListFragment를 만들어 각각의 xml 파일에 기존 activity xml 내용을 넣어준다.
- Activity와 마찬가지로 레이아웃을 확장하고 개별 뷰를 바인딩해야 한다.
(build.gradle 파일의 buildFeatures 섹션에서 viewBinding 속성을 사용해줘야 한다.)
- LetterListFragment에서 뷰 바인딩을 구현하려면 먼저 null을 허용하는 FragmentLetterListBinding 참조를 가져와야 한다. 그리고 밑줄 없이 바인딩이라는 새 속성을 만들고 _binding!!와 동일하게 설정한다.
class LetterListFragment : Fragment() {
private var _binding: FragmentLetterListBinding? = null
private val binding get() = _binding!!
...
}
- onCreate()를 구현하려면 setHasOptionsMenu()를 호출한다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setHasOptionsMenu(true)
}
- 프래그먼트에서는 레이아웃이 onCreateView()에서 확장된다. 뷰를 확장하고 _binding 값을 설정한 다음 루트 뷰를 반환하여 onCreateView()를 구현한다.
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = FragmentLetterListBinding.inflate(inflater, container, false)
val view = binding.root
return view
}
- recycler 뷰 속성을 만들고 설정해준 뒤 onDestroyView()에서 뷰가 더 이상 없으므로 _binding 속성을 null로 재설정한다.
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
- MainActivity에서 isLinearLayoutManager 속성을 복사하여 recyclerView 속성 선언 바로 아래에 배치한다.
- 모든 기능을 LetterListFragment로 이동했으므로 MainActivity 클래스에서 해야 할 작업은 프래그먼트가 뷰에 표시되도록 레이아웃을 확장하는 것뿐이다. 따라서 MainActivity에서 onCreate()를 제외하고 모든 항목을 삭제한다.
비슷한 과정을 거쳐 기존에 만들었던 각 activity들을 fragment로 변환할 수 있다.
3. Jetpack Navigation Component
Android Jetpack에서 제공하는 Navigation(탐색) component를 통해 앱에서 간단하거나 복잡한 탐색 구현을 처리할 수 있다. 탐색 구성요소에는 Words 앱에서 탐색을 구현하는 데 사용할 세 가지 주요 부분이 있다.
- Navigation Graph: 탐색 그래프는 앱에서 탐색을 시각적으로 보여주는 XML 파일이다. 파일은 개별 활동 및 프래그먼트에 상응하는 대상과 한 대상에서 다른 대상으로 이동하려고 코드에서 사용할 수 있는 대상 사이의 작업으로 구성된다. 레이아웃 파일과 마찬가지로 Android 스튜디오는 탐색 그래프에 대상과 작업을 추가하는 시각적 편집기를 제공한다.
- NavHost: NavHost는 활동 내에서 탐색 그래프의 대상을 표시하는 데 사용된다. 프래그먼트 간에 이동하면 NavHost에 표시되는 대상이 업데이트된다. MainActivity에서 NavHostFragment라는 기본 제공 구현을 사용한다.
- NavController: NavController 객체를 사용하면 NavHost에 표시되는 대상 간 탐색을 제어할 수 있다. 인텐트 대신 탐색 구성요소를 사용하면 NavController의 navigate() 메서드를 호출하여 표시되는 프래그먼트를 교체할 수 있다.
navigation 라이브러리를 사용하려면 Gradle 파일을 먼저 수정해줘야 한다.
- build.gradle 파일의 buildscript > ext에서 material_version 아래의 nav_version을 2.3.1로 설정한다.
- navigation dependencies 을 추가한다.
- buildscript > dependencies에서 클래스 경로를 추가한다.
- plugins 내에서 androidx.navigation.safeargs.kotlin을 추가한다.
4. Navigation graph 사용
탐색 그래프(또는 줄여서 NavGraph)는 앱 탐색의 가상 매핑이다. 각 화면(또는 이 경우의 프래그먼트)은 이동할 수 있는 가능한 '대상'이 되고, NavGraph는 각 대상이 서로 관련되는 방식을 보여주는 XML 파일로 나타낼 수 있다.
- activity_main.xml 파일이 프래그먼트의 NavHost 역할을 할 FragmentContainerView를 포함하도록 한다.
- 이 시점부터 앱의 모든 탐색은 FragmentContainerView 내에서 실행된다.
- FragmentContainerView에 app:defaultnavHost라는 속성을 추가하고 "true"로 설정하면 프래그먼트 컨테이너가 탐색 계층 구조와 상호작용할 수 있다.
- app:navGraph라는 속성을 추가하고 "@navigation/nav_graph"로 설정하면 앱의 프래그먼트가 서로 이동할 수 있는 방법을 정의하는 XML 파일을 가리게 된다.
- 탐색 그래프 파일(File > New > Android Resource File)을 추가하고 리소스 유형을 Navigation으로 설정한다.
- FragmentContainerView의 navGraph 속성에서 nav_graph를 이미 참조했으므로 새 대상을 추가하려면 화면 상단 왼쪽에서 새로 만들기 버튼을 클릭하여 각 프래그먼트의 대상을 만든다.
- 한 프래그먼트에 마우스 포인터를 놓으면 프래그먼트 오른쪽에 ConstraintLayout 제약 주듯이 원형 연결 지점이 나타나는데 이를 다른 프래그먼트쪽으로 끌어당기면 두 프래그먼트가 연결된다.
- 이 작업의 이름이 action_letterListFragment_to_wordListFragment와 같이 자동으로 설정되고 이는 코드에서 참조가 가능하다.
- 인수 지정, 시작 대상 설정까지 마치면 탐색 그래프에 기반한 코드가 생성되어 방금 만든 탐색 작업을 사용할 수 있다.
- LetterAdapter.kt를 열어 버튼의 onClickListener() 콘텐츠를 삭제하고 대신 방금 만든 탐색 작업을 넣어준다.
5. MainActivity 구성
- navController 속성을 만든다. onCreate에서 설정되므로 lateinit로 표시된다.
- onCreate()에서 setContentView()를 호출한 후 nav_host_fragment(FragmentContainerView의 ID임) 참조를 가져와서 navController 속성에 할당한다.
- onCreate()에서 setupActionBarWithNavController()를 호출하여 navController를 전달하면 LetterListFragment의 메뉴 옵션과 같은 작업 모음(앱 바) 버튼이 표시된다.
- onSupportNavigateUp()을 구현한다. XML에서 defaultNavHost를 true로 설정하는 것과 함께 이 메서드를 사용하면 위로 버튼을 처리할 수 있다.
6. 프래그먼트에서 인수 가져오기
- WordListFragment에서 letterId 속성을 만든다.
- onCreateView()나 onViewCreated()가 아닌 onCreate()를 재정의한다.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
letterId = it.getString(LETTER).toString()
}
}
- onViewCreated()의 activity?.intent?.extras?.getString(LETTER).toString()을 letterId로 바꾼다.
이러한 과정을 통해 인텐트 없이 하나의 Activity 내에서 두 화면 간에 이동을 할 수 있다.
PATHWAY 3 Architecture components
1. 앱 아키텍처와 ViewModel
The most common architectural principles are: separation of concerns and driving UI from a model.
- ViewModel은 뷰에 표시되는 앱 데이터의 모델이다.
- 모델은 앱의 데이터 처리를 담당하는 구성요소로, 아키텍처 원칙에 따라 모델에서 UI가 도출되는 앱을 만들 수 있다.
2. ViewModel 프래그먼트 연결 및 데이터 이동
MainActivity에 GameFragment가 포함되어 있으며, GameFragment는 GameViewModel에 있는 게임 관련 정보에 액세스한다.
- GameViewModel이라는 새 Kotlin 클래스 파일을 만들고 GameViewModel의 객체 인스턴스를 만든다.
private val viewModel: GameViewModel by viewModels()
- GameFragment 클래스에서 GameViewModel 클래스로 데이터 변수를 이동한다.
class GameViewModel : ViewModel() {
private var score = 0
private var currentWordCount = 0
private var currentScrambledWord = "test"
...
- 속성이 ViewModel에만 공개되며 UI 컨트롤러에서 액세스할 수 없기 때문에 오류가 난다.
-> GameViewModel에서 currentScrambledWord 선언을 변경하여 지원 속성을 추가한다.
private var _currentScrambledWord = "test"
val currentScrambledWord: String
get() = _currentScrambledWord
- GameFragment에서 읽기 전용 viewModel 속성인 currentScrambledWord를 사용하도록 updateNextWordOnScreen() 메서드를 업데이트한다.
private fun updateNextWordOnScreen() {
binding.textViewUnscrambledWord.text = viewModel.currentScrambledWord
}
주의사항) ViewModel에서 변경 가능한 데이터 입력란을 노출하면 안된다. 다른 클래스에서 이 데이터를 수정할 수 없도록 ViewModel 내부의 변경 가능한 데이터는 항상 private여야 한다.
3. ViewModel의 수명주기
4. 게임 앱 구현
- 지연 초기화: 변수를 선언할 때는 일반적으로 사전에 변수에 초깃값을 제공하지만 아직 값을 할당할 준비가 되지 ㅇ않은 경우 나중에 초기화할 수 있도록 지연 초기화를 지원한다. 이 때 lateinit 키워드를 사용하며, 변수가 초기화될 때까지는 변수에 메모리가 할당되지 않는다.
- 알림 대화상자
private fun showFinalScoreDialog() {
MaterialAlertDialogBuilder(requireContext())
.setTitle(getString(R.string.congratulations))
.setMessage(getString(R.string.you_scored, viewModel.score))
.setCancelable(false)
.setNegativeButton(getString(R.string.exit)) { _, _ ->
exitGame()
}
.setPositiveButton(getString(R.string.play_again)) { _, _ ->
restartGame()
}
.show()
}
- 텍스트 필드에 오류 표시: 머티리얼 텍스트 필드의 경우 TextInputLayout에 오류 메시지를 표시하는 기능이 내장되어 있다.
// Set error text passwordLayout.error = getString(R.string.error) // Clear error text passwordLayout.error = null --- private fun setErrorTextField(error: Boolean) { if (error) { binding.textField.isErrorEnabled = true binding.textField.error = getString(R.string.try_again) } else { binding.textField.isErrorEnabled = false binding.textInputEditText.text = null } }
5. Livedata
LiveData는 수명 주기를 인식하는 식별 가능한 데이터 홀더 클래스이다.
- LiveData는 데이터를 보유한다.
- LiveData는 식별 가능하다.
- LiveData는 수명 주기를 인식한다.
- MutableLiveData는 변경 가능한 버전의 LiveData이다.
6. 게임 앱 구현
- 글자가 뒤섞인 현재 단어에 LiveData 추가하기
private fun getNextWord() {
...
} else {
_currentScrambledWord.value = String(tempWord)
...
}
}
- LiveData 객체에 관찰자 연결하기
private fun onSubmitWord() {
val playerWord = binding.textInputEditText.text.toString()
if (viewModel.isUserWordCorrect(playerWord)) {
setErrorTextField(false)
if (!viewModel.nextWord()) {
showFinalScoreDialog()
}
} else {
setErrorTextField(true)
}
}
- 점수 및 단어 수에 observer 연결하기
LiveData로 점수 및 단어 수 래핑
관찰자를 점수 및 단어 수에 연결private fun showFinalScoreDialog() { MaterialAlertDialogBuilder(requireContext()) .setTitle(getString(R.string.congratulations)) .setMessage(getString(R.string.you_scored, viewModel.score.value)) ... .show() }
viewModel.currentWordCount.observe(viewLifecycleOwner, { newWordCount -> binding.wordCount.text = getString(R.string.word_count, newWordCount, MAX_NO_OF_WORDS) })
7. 데이터 결합
뷰 결합은 단방향이다. 뷰를 코드에 바인딩할 수는 있지만 코드를 뷰에 바인딩할 수는 없다. 따라서 뷰 결합을 사용하면 뷰에서 앱 데이터를 참조할 수 없다. 이때 데이터 결합을 사용하면 된다.
데이터 결합을 사용할 때 주요 이점은 활동에서 많은 UI 프레임워크 호출을 삭제할 수 있어 파일이 더욱 단순해지고 더 손쉬운 유지관리가 가능하다는 점이다.
또한 앱 성능이 향상되며 메모리 누수 및 null 포인터 예외를 방지할 수 있다.
binding.textViewUnscrambledWord.text = viewModel.currentScrambledWord //뷰결합
android:text="@{gameViewModel.currentScrambledWord}" //데이터결합
- 뷰 결합을 데이터 결합으로 변경
buildFeatures { dataBinding = true }
- 레이아웃 파일을 데이터 결합 레이아웃으로 변환
<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> <data> </data> <ScrollView android:layout_width="match_parent" android:layout_height="match_parent"> <androidx.constraintlayout.widget.ConstraintLayout ... </androidx.constraintlayout.widget.ConstraintLayout> </ScrollView> </layout>
- onCreateView() 메서드 시작 부분에서 데이터 결합을 사용하도록 binding 변수의 인스턴스화를 변경
binding = DataBindingUtil.inflate(inflater, R.layout.game_fragment, container, false)
데이터 결합 변수 추가 후 결합 표현식을 사용해 UI를 업데이트하도록 하면 결합 표현식과 함께 LiveData를 사용할 수 있다.
<data>
...
<variable
name="maxNoOfWords"
type="int" />
</data>
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.gameViewModel = viewModel
binding.maxNoOfWords = MAX_NO_OF_WORDS
...
}
<TextView
android:id="@+id/word_count"
...
android:text="@{@string/word_count(gameViewModel.currentWordCount, maxNoOfWords)}"
.../>
<TextView
android:id="@+id/score"
...
android:text="@{@string/score(gameViewModel.score)}"
... />
viewModel.score.observe(viewLifecycleOwner,
{ newScore ->
binding.score.text = getString(R.string.score, newScore)
})
viewModel.currentWordCount.observe(viewLifecycleOwner,
{ newWordCount ->
binding.wordCount.text =
getString(R.string.word_count, newWordCount, MAX_NO_OF_WORDS)
})
댓글