안녕하세요! 허접 샴푸입니다.
오랜만에 다시 시작하고자 합니다.
처음부터 끝까지 한번 제대로 만들어보도록 하겠습니다.
이번 편은 먼저 스플래시 화면과 권한 체크를 하는 화면입니다.
[만들고자 하는 화면]



1. 인트로 화면
2. 권한 동의 여부 다이얼로그
3. 권한 체크 팝업
[필요 사전 지식]
1. 안드로이드
2. 코틀린
3. Android Jetpack
4. 코루틴
* 일단 사전 지식이 없더라도 따라 만들면서 부족한 점은, 인터넷 찾아보면서, 학습하시면 됩니다.
[목표]
1. Fragment 및 ResultListener
2. Jetpack Navigation
3. Permission check
[시작]
1) Gradle 설정
dependencies { | |
implementation 'androidx.core:core-ktx:1.9.0' | |
implementation 'androidx.appcompat:appcompat:1.5.1' | |
implementation 'com.google.android.material:material:1.7.0' | |
implementation 'androidx.constraintlayout:constraintlayout:2.1.4' | |
testImplementation 'junit:junit:4.13.2' | |
androidTestImplementation 'androidx.test.ext:junit:1.1.4' | |
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' | |
def lifecycle_version = "2.6.0-alpha03" | |
// ViewModel | |
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" | |
implementation 'androidx.fragment:fragment-ktx:1.5.4' | |
// Jetpack Navigation | |
def nav_version = "2.5.3" | |
implementation("androidx.navigation:navigation-fragment-ktx:$nav_version") | |
implementation("androidx.navigation:navigation-ui-ktx:$nav_version") | |
} |
- build.gradle (Module: app)에 위와 같이 ViewModel 및 Jetpack Navigation 종속 항목을 추가합니다.
- 이 프로젝트에서는 ViewModel과 Jetpack Navigation을 사용해서 화면을 탐색하고자 합니다.
2) 구조

- 안드로이드 앱 권장 아키텍처를 사용하기 위해 위와 같이 패키지 스트럭처를 가져갔습니다.
- 클린 아키텍처에 중점을 두어 프로젝트를 제작해 나가겠습니다.
- 간단하게 말씀드리자면, 권장 아키텍처 및 클린 아키텍처에서는 코드를 총 3개의 레이어인 data, domain, presentation 레이어로 나눌 수 있습니다.
- data 레이어: 비즈니스 로직을 담당하는 레이어(네트워크 통신, 로컬 데이터베이스 통신 등)
- domain 레이어: presentation 레이어와 data 레이어를 잇는 매개체 역할을 하는 레이어
- presentation 레이어: 말 그대로 화면에 보여주는 역할과 관련된 코드를 지닌 레이어입니다.
* 일단 레이어 분리부터, 이것저것 이해가 잘 안 가실 테지만 차근차근 배우면서 설명드리도록 하겠습니다.

- 일단 이번에 만들 것은, 위와 같습니다.
- Presentation 패키지 하위에 ui 패키지가 있고, ui 패키지에는 각 화면을 담당하는 Fragment 클래스가 있습니다.
- 그리고 utils 패키지는 ui에 필요한 파일들을 작성하는 곳입니다.
- 마지막으로 MainActivity가 있으며, 저희는 Single Activity 규칙을 사용합니다.
3) 네비게이션 설정
<?xml version="1.0" encoding="utf-8"?> | |
<androidx.constraintlayout.widget.ConstraintLayout 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" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
tools:context=".presentation.MainActivity"> | |
<androidx.fragment.app.FragmentContainerView | |
android:id="@+id/nav_host_fragment" | |
android:name="androidx.navigation.fragment.NavHostFragment" | |
android:layout_width="0dp" | |
android:layout_height="0dp" | |
app:defaultNavHost="true" | |
app:layout_constraintBottom_toBottomOf="parent" | |
app:layout_constraintLeft_toLeftOf="parent" | |
app:layout_constraintRight_toRightOf="parent" | |
app:layout_constraintTop_toTopOf="parent" | |
app:navGraph="@navigation/nav_graph" /> | |
</androidx.constraintlayout.widget.ConstraintLayout> |
- MainActivity는 단순히 FragmentContainerView를 가지고 있고, 저희는 모든 화면을 Fragment로 작성하여 FragmentContainerView가 모든 네비게이션을 담당하도록 설정하였습니다.
- FragmentContainerView는 네비게이션 그래프가 필요하며 아래와 같습니다.

- 네비게이션 그래프는 nav_graph.xml 파일이며, 시작 화면은 무엇이고, 각 화면에서 어느 화면으로 탐색을 할 수 있는지 규칙을 정하는 navigation 파일입니다.
- introSplashFragment가 startDestination(앱을 시작하였을 때 가장 먼저 보이는 화면)이 되며, 해당 화면에서는 checkPermissionsDialogFragment와 introTermAgreementFragment로 탐색(화면 전환)을 할 수 있게 설정하였습니다.

- popupTo와 popUpToInclusive: introSplashFragment에서 introTermAgreementFragment로 화면 전환을 하고 나면, introTermAgreementFragment에서 백버튼을 눌러 뒤로 가기를 할 경우, nav_graph로 pop을 하도록 설정하였습니다.
- popupToInclusive 옵션에 true를 설정하여 nav_graph 또한 네비게이션 백스택에서 제거되도록 설정하였습니다. 그래서 introTermAgreementFragment에서 뒤로가기를 누르면 네비게이션 백스택에 남아있는 화면이 없으므로 앱은 꺼지게 됩니다.
- 만약 "false"로 설정하였다면, nav_graph는 startDestination인 introTermAgreementFragment를 다시 화면에 표시하고 앱이 진행될 것입니다.

- 네비에기션 파일은 res > navigation 디렉터리에 위치해야 합니다.
4) Presentation Layer 코딩
<IntroSplashFragment.kt>
import android.Manifest | |
import android.content.pm.PackageManager | |
import android.os.Bundle | |
import android.view.View | |
import androidx.core.content.ContextCompat | |
import androidx.fragment.app.Fragment | |
import androidx.fragment.app.setFragmentResultListener | |
import androidx.lifecycle.lifecycleScope | |
import androidx.navigation.fragment.findNavController | |
import com.dj.baeminpractice2.R | |
import com.dj.baeminpractice2.presentation.utils.REQUEST_KEY_PERMISSION_CHECK_READ_EXTERNAL_STORAGE | |
import kotlinx.coroutines.delay | |
import kotlinx.coroutines.launch | |
class IntroSplashFragment : Fragment(R.layout.fragment_intro_splash) { | |
private val neededPermission = Manifest.permission.READ_EXTERNAL_STORAGE | |
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | |
super.onViewCreated(view, savedInstanceState) | |
lifecycleScope.launch { | |
delay(1000) | |
checkPermissions() | |
listenToFragmentResultListeners() | |
} | |
} | |
private fun checkPermissions() { | |
when (PackageManager.PERMISSION_GRANTED) { | |
ContextCompat.checkSelfPermission( | |
requireContext(), | |
neededPermission | |
) -> navigateToIntroTermAgreementScreen() | |
else -> navigateToCheckPermissionDialogScreen() | |
} | |
} | |
private fun listenToFragmentResultListeners() { | |
setFragmentResultListener(requestKey = REQUEST_KEY_PERMISSION_CHECK_READ_EXTERNAL_STORAGE) { requestKey, _ -> | |
require(requestKey == REQUEST_KEY_PERMISSION_CHECK_READ_EXTERNAL_STORAGE) | |
navigateToIntroTermAgreementScreen() | |
} | |
} | |
private fun navigateToIntroTermAgreementScreen() { | |
findNavController().navigate(resId = R.id.action_introSplashFragment_to_introTermAgreementFragment) | |
} | |
private fun navigateToCheckPermissionDialogScreen() { | |
findNavController().navigate(resId = R.id.action_introSplashFragment_to_checkPermissionsDialogFragment) | |
} | |
} |
1. delay 옵션

- 1000ms = 1초
- 1초를 딜레이 한 뒤 함수들을 호출하도록 하였습니다.
2. 권한 체크

- 사용자가 READ_EXTERNAL_STORAGE 권한을 허용했는지 체크를 합니다.
- 권한이 있을 경우 navigateToIntroTermAgreementScreen()를 호출하여 그다음 화면으로 이동하도록 하였습니다.
- 권한이 없을 경우 navigateToCheckPermissionDialogScreen()를 호출하여 권한 동의 여부 다이얼로그 화면으로 이동하도록 하였습니다.

- findNavController()는 MainActivity에서 관리되는 Navigation Controller 객체를 반환받으며, 해당 객체의 navigate 함수를 이용하여 저희가 네비게이션 그래프인 nav_graph.xml에 선언한 action id를 인자로 넘겨주어 화면을 탐색할 수 있습니다.

- setFragmentResultListener: 다른 Fragment의 결과 값을 받을 수 있는 리스너입니다.
- Request Key를 사용하여 결괏값을 준 화면과 받는 화면 간의 계약을 체결할 수 있습니다. 만약 Request Key가 다르다면 결과 값을 받을 수 없습니다.
- 권한 체크 동의 여부 다이얼로그 화면인 checkPermissionDialogFragment에서 동의를 의미하는 "확인" 버튼을 클릭했을 때 해당 화면에서 결과 값을 현재 introSplashFragment에게 리턴합니다. 즉, 동의를 했다는 결과를 받은 introSplashFragment 화면에서는 navigateToIntroTermAgreementScreen()을 호출하여 그다음 화면으로 이동하게 됩니다.
<CheckPermissionsDialogFragment.kt>
* 먼저 권한 체크에 앞서, 아래 코드를 AndroidManifest.xml에 추가해주셔야 합니다.

import android.Manifest | |
import android.os.Bundle | |
import android.view.View | |
import androidx.activity.result.contract.ActivityResultContracts | |
import androidx.core.os.bundleOf | |
import androidx.fragment.app.DialogFragment | |
import androidx.fragment.app.setFragmentResult | |
import androidx.navigation.fragment.findNavController | |
import com.dj.baeminpractice2.R | |
import com.dj.baeminpractice2.databinding.DialogFragmentCheckPermissionsBinding | |
import com.dj.baeminpractice2.presentation.utils.REQUEST_KEY_PERMISSION_CHECK_READ_EXTERNAL_STORAGE | |
class CheckPermissionsDialogFragment : DialogFragment(R.layout.dialog_fragment_check_permissions) { | |
private val neededPermission = Manifest.permission.READ_EXTERNAL_STORAGE | |
private val requestPermissionLauncher = | |
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> | |
setFragmentResultAndPopBackStack() | |
} | |
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { | |
super.onViewCreated(view, savedInstanceState) | |
val binding = DialogFragmentCheckPermissionsBinding.bind(view) | |
initViews(binding = binding) | |
} | |
private fun initViews(binding: DialogFragmentCheckPermissionsBinding) { | |
binding.btnConfirm.setOnClickListener { | |
requestPermissionLauncher.launch(neededPermission) | |
} | |
} | |
private fun setFragmentResultAndPopBackStack() { | |
findNavController().popBackStack() | |
setFragmentResult(REQUEST_KEY_PERMISSION_CHECK_READ_EXTERNAL_STORAGE, bundleOf()) | |
} | |
} |
- 권한 체크 여부를 묻는 다이얼로그 화면입니다.


- "확인" 버튼인 btnConfirm를 클릭하였을 때 reqeustPermissionLauncher는 neededPermission 권한이 있는지 확인을 합니다.

- requestPermissionLauncher는 권한이 있는지 없는지 결과 값을 받게 됩니다. isGranted 변수를 사용하여 권한을 허용했는지 거절했는지 판단할 수 있습니다.
- 단 배달의 민족 어플을 보시면, 권한에 동의하지 않아도 앱이 진행되기에, 위에서 if else 조건문을 사용하진 않았습니다.
- 권한에 동의하지 않아도 앱이 진행될 수 있도록 조건문 없이 setFragmentResultAndPopBackStack() 함수를 호출하도록 하였습니다.

- findNavController().popBackStack() 호출하여 현재 화면을 네비게
이션 백스택에서 제거합니다. 즉, 지금 보여주고 있는 화면을 더 이상 보여주지 말라는 뜻입니다.
- 그다음 setFragmentResult(REQUEST_KEY_PERMISSION_CHECK_READ_EXTERNAL_STORAGE, bundleOf()) 를 이용하여, introSplashFragment에게 "확인" 버튼을 눌렀다는 것을 알려줍니다.
- bundleOf()를 이용하여 더 많은 값을 넘겨줄 수 있지만, 저희 앱에서는 딱히 넘겨줄 값이 없기에 빈 번들 객체만을 넘겨주게 하였습니다.
* 중요한 점
(IntroSplashFragment.kt)

(CheckPermissionsDialogFragment.kt)

- 위 두 코드를 보시면 REQUEST_KEY_PERMISSION_CHECK_READ_EXTERNAL_STORAGE를 key 값으로 이용하여 Fragment 간의 통신 계약을 체결하였습니다.
- 즉, key가 같아야 원하는 Fragment로부터 결괏값을 받을 수 있습니다.
[마무리]
- Jetpack Navigation에 대해 간단히 알아보았습니다.
- FragmentResultListener에 대해 간단히 알아보았습니다.
- 권한 체크에 대해서 간단히 알아보았습니다.
[다음 목표]
- 마케팅 동의 여부 화면을 만들고, ViewModel을 사용하여 Ui 상태를 보존하는 방법 및 왜 ViewModel을 사용해야 하는지 등에 대해서 알아보도록 하겠습니다.
[Github] 코드를 다운로드하여서 실행해보시기 바랍니다.
https://github.com/DJDrama/BaeminPractice2/tree/1_splashscreen
GitHub - DJDrama/BaeminPractice2
Contribute to DJDrama/BaeminPractice2 development by creating an account on GitHub.
github.com
'[Android] > App UI 따라 만들기' 카테고리의 다른 글
[배달의민족2] 클론코딩 - 3. 약관 상세 화면(Data 레이어) (5) | 2022.11.20 |
---|---|
[배달의민족2] 클론코딩 - 2. 약관 동의 화면 (4) | 2022.11.13 |
[배달의 민족] 따라만들기 11-1편(주문내역 화면) (5) | 2021.03.23 |
[배달의 민족] 따라만들기 10-3편 (Dagger-Hilt) (1) | 2021.03.17 |
[배달의 민족] 따라만들기 10-2편 (Dagger-Hilt) (4) | 2021.02.25 |