안드로이드를 6년정도 해오다가 최근 2년정도는 Flutter에 집중했었다.
이번에 좋은 기회가 생긴 덕분에, 꽤나 오랜만에 안드로이드를 다시, 제대로 재시작하고있다.
천리길도 한걸음부터!
세 개의 화면을 갖는 앱에 대한 구현을 진행한다.
Activity를 세개 만드는 것이 아니라, Fragment를 이용하여 구현할 것이고
Jetpack의 Navigation을 이용하여 화면 이동들을 구현할 것이다.
Fragment는 독자적으로 존재할 순 없고, 액티비티 위에서 보여진다.
따라서 이번 구현에서는 액티비티 1개, 프래그먼트 3개가 필요하다.
구글에서는 각 이동되는 화면들 마다 Activity를 구현하는 것이 아니라
하나의 액티비티, 그리고 여러개의 Fragment로 구현하는 것을 권장하고 있고
그 기반에서 화면이동을 효율적으로 활용할 수 있는 Jetpack의 Navigation 이용을 권장하고 있다.
프로젝트를 생성한다.
Navigation 사용을 위해
build.gradle (Module)을 열고, dependencies 안에 다음과 같은 내용을 추가한다.
// Kotlin
def nav_version = "2.5.3"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
res 아래에 navigation 이름을 갖는 resource directory를 생성한다.
이름과 설정은 이렇게 한다.
그리고 이 폴더 안에 nav_graph.xml 리소스 파일을 생성한다.
nav_graph는 프래그먼트 간 이동 관계도를 그려서 설계하는 역할을 해 줄 것이다.
activity_main.xml을 연다.
ConstraintLayout을 사용한다. Design 과 Code를 연결하는데 유용하다.
<?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=".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:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>
이렇게 작성해준다.
이제 fragment들을 만들어 줄 것이다.
적당한 이름을 설계해준다.
같은 방식으로 총 3개의 프레그먼트를 생성해주는데
요즘에는 Fragment 생성해주면 xml도 자동으로 생성해준다.
프래그먼트명을 낙타표기법 (Camel case)으로 작성해주면
레이아웃명은 뱀 표기법 (Snake case)으로 만들어진다.
fragment_main.xml을 손봐보자!
ConstraintLayout은, 안드로이드 스튜디오 Split을 이용해 화면 분할한 뒤에 직접 끌어당기면서 배치하면 된다.
ConstraintLayout은 이렇게 직접 배치할 때 매우 편하다.
(한창 안드로이드를 했던 몇 년 전에는 Linear, Relative로 열심히 코딩했었는데..!)
최근 Flutter에 익숙해왔다보니까, 예전엔 못 느꼈던 안드로이드 layout 설계 방식인 xml에 불편함을 느낀다.
Flutter처럼 style이면 TextStyle() 안에서 계층 구조로 Widget을 관리하면 가독성이 좋을 것 같은데..
xml은 textSize, texStyle 등등 속성들이 모여져 있지 않고 구분되어서 매우 불편하다고 느낀다.
역시 사람은 넓게 배워야하는 것 같다!
아무튼 텍스트뷰를 이렇게 만들어주고, 다음 화면으로 넘어가는 버튼을 만들 건데! 내가 주로 쓰는 방식을 이용해 볼 것이다.
drawable에서 vector asset을 새로 만들어서 arrow 아이콘을 추가해주고
next.xml을 새로 만들어준다.
layer-list를 이용하면 이렇게 만들 수 있다.
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<size
android:width="24dp"
android:height="24dp"/>
<solid
android:color="#ffffff"/>
</shape>
</item>
<item android:drawable="@drawable/ic_baseline_arrow_right_alt_24">
</item>
</layer-list>
옛날에 이렇게 자주 했었는데.. 기억이 새록새록 피어난다..! 즐겁다!!
그리고 그림자를 추가하려면 이렇게 하면 된다.
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<padding
android:bottom="1dp"
android:left="1dp"
android:right="1dp"
android:top="1dp" />
<solid android:color="#00CCCCCC" />
</shape>
</item>
<item>
<shape android:shape="oval">
<padding
android:bottom="1dp"
android:left="1dp"
android:right="1dp"
android:top="1dp" />
<solid android:color="#10CCCCCC" />
</shape>
</item>
<item>
<shape android:shape="oval">
<padding
android:bottom="1dp"
android:left="1dp"
android:right="1dp"
android:top="1dp" />
<solid android:color="#20CCCCCC" />
</shape>
</item>
<item>
<shape android:shape="oval">
<padding
android:bottom="1dp"
android:left="1dp"
android:right="1dp"
android:top="1dp" />
<solid android:color="#30CCCCCC" />
</shape>
</item>
<item>
<shape android:shape="oval">
<padding
android:bottom="1dp"
android:left="1dp"
android:right="1dp"
android:top="1dp" />
<solid android:color="#50CCCCCC" />
</shape>
</item>
<item>
<shape android:shape="oval">
<size
android:width="24dp"
android:height="24dp"/>
<solid
android:color="#ffffff"/>
</shape>
</item>
<item android:drawable="@drawable/ic_baseline_arrow_right_alt_24">
</item>
</layer-list>
다시 fragment_main.xml로 가서, 적용해준다.
<?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=".fragment.MainFragment">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="140dp"
android:text="@string/main_title"
android:textSize="35sp"
android:textStyle="bold"
android:fontFamily="sans-serif"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_marginBottom="120dp"
android:src="@drawable/next"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
깔끔!
레이아웃 배치를 더 정교하게 할 수 있는 가이드라인 생성 방법에 대해서 잠깐 다뤄보자
화면에는 보이지 않고, 개발할 때만 보이는 이런게 생긴다.
ConstraintLayout의 장점은 Design UI와 활용할 수 있으므로
이렇게 시각적인 요소들을 이용해 이 남은 40% 안에서의 Center 정렬을 간편하게 할 수 있다.
그래서 fragment_main.xml 코드는 다음과 같다.
<?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=".fragment.MainFragment">
<ImageView
android:id="@+id/btn_next"
android:layout_width="84dp"
android:layout_height="84dp"
android:src="@drawable/next"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/guideline3" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="140dp"
android:fontFamily="sans-serif"
android:text="@string/main_title"
android:textSize="35sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.6019152" />
</androidx.constraintlayout.widget.ConstraintLayout>
비슷한 방식으로 fragment_selection.xml도 만들어준다
<?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=".fragment.SelectionFragment">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.8" />
<ImageView
android:id="@+id/btn_back"
android:layout_width="64dp"
android:layout_height="64dp"
android:src="@drawable/back"
app:layout_constraintTop_toBottomOf="@id/guideline4"
app:layout_constraintStart_toStartOf="parent"
/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:orientation="vertical"
android:padding="10dp"
app:layout_constraintBottom_toTopOf="@+id/guideline4"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/option_1"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:text="Question 1"
android:gravity="center"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:background="#3fddabfb"/>
<TextView
android:id="@+id/option_2"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:text="Question 2"
android:gravity="center"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:background="#fefeaa"/>
<TextView
android:id="@+id/option_3"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:text="Question 3"
android:gravity="center"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:background="#00feaa"/>
<TextView
android:id="@+id/option_4"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:text="Question 4"
android:gravity="center"
android:layout_marginTop="5dp"
android:layout_marginBottom="5dp"
android:background="#aaefff"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
마지막으로 fragment_result.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".fragment.ResultFragment">
<!-- TODO: Update blank fragment layout -->
<TextView
android:id="@+id/result"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="RESULT"
android:gravity="center"/>
</FrameLayout>
이제 nav_graph.xml 를 Design으로 열어줘서
프레그먼트 파일들 간의 관계도를 정해준다.
지금 팝업으로 뜬 + 버튼을 누르면 프래그먼트들을 추가할 수 있다.
세 개를 추가하여, 각 프래그먼트를 화살표로 연결지어줘서 흐름도를 구현해준다.
이렇게 하면 자동으로 이동 액션 파일이 만들어지고, 우린 이 이동 액션 파일을 이용해서 원하는 시점에 사용해주면 된다!
모든 레이아웃 파일은 손 봤으니, 이제 소스코드 파일들을 하나씩 보자
MainActivity.kt
package made.by.hevton.love
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.navigation.NavController
import androidx.navigation.findNavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController
import made.by.hevton.love.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
lateinit var navController: NavController
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root);
// 잘못된 예
// navController = binding.navHostFragment.findNavController()
navController = binding.navHostFragment.getFragment<NavHostFragment>().navController
}
}
뷰 바인딩을 이용해서 activity_main.xml 파일을 소스코드와 연결해줬다.
그리고 id로 nav_host_fragment 를 지정해줬던 FragmentContainerView를 가져와서 NavHostFragment로 캐스팅해주고 컨트롤러를 받아온다. 메인액티비티에서는 이렇게 사용해주면된다. 사용 예시는 아직 없지만 그냥 이렇게 알고 넘어가면 된다.
MainFragment.kt
코드가 알아서 좀 작성되어있을건데, 딴건 다 필요없고
일단 onCreateView에서 기존에 return inflater.inflate ~~ 로 구현되어 있는 부분을 뷰 바인딩으로 변경해준다.
그리고 onViewCreated() 메소드를 오버라이드해서,
Navigation.findNavController(view) 를 이용해서 view(프래그먼트뷰) 를 통해 NavController를 받아오고
이걸 이용해서 navigate 하면 화면 이동을 구현할 수 있다.
package made.by.hevton.love.fragment
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.navigation.NavController
import androidx.navigation.Navigation
import made.by.hevton.love.R
import made.by.hevton.love.databinding.FragmentMainBinding
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"
/**
* A simple [Fragment] subclass.
* Use the [MainFragment.newInstance] factory method to
* create an instance of this fragment.
*/
class MainFragment : Fragment() {
lateinit var binding: FragmentMainBinding
lateinit var navController: NavController
// TODO: Rename and change types of parameters
private var param1: String? = null
private var param2: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
// return inflater.inflate(R.layout.fragment_main, container, false)
binding = FragmentMainBinding.inflate(inflater, container, false)
return binding.root
}
// 뷰가 생성되고 난 후
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
navController = Navigation.findNavController(view)
binding.btnNext.setOnClickListener {
navController.navigate(R.id.action_mainFragment_to_selectionFragment)
}
}
companion object {
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param param1 Parameter 1.
* @param param2 Parameter 2.
* @return A new instance of fragment MainFragment.
*/
// TODO: Rename and change types and number of parameters
@JvmStatic
fun newInstance(param1: String, param2: String) =
MainFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
}
}
Fragment에는 기존 뷰 바인딩처럼 lazy를 하기엔, onCreateView에서 얻을 수 있으므로 lateinit으로 해줬다.
NavController는 이처럼 onViewCreated에서 받을 수 있다.
SelectionFragment.kt
이것도 비슷하다. 단, 이번에는 버튼 뷰에 리스너 객체를 직접 생성해서 달아주기보다
프래그먼트 자체적으로 View.OnClickListener를 implement 해주어서 구현했다.
마찬가지로 onViewCreated() 메서드를 오버라이드 해서 구현해주는게 핵심이다.
이번에는 화면 이동을 할 때, bundle 객체를 이용해서 데이터를 넘겨줬다.
하지만 프래그먼트 간 데이터 교환을 위해 ViewModel을 이용하는 방법도 있다. 이는 다음에 소개한다.
package made.by.hevton.love.fragment
import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.core.os.bundleOf
import androidx.navigation.NavController
import androidx.navigation.Navigation
import made.by.hevton.love.R
import made.by.hevton.love.databinding.FragmentSelectionBinding
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"
/**
* A simple [Fragment] subclass.
* Use the [SelectionFragment.newInstance] factory method to
* create an instance of this fragment.
*/
class SelectionFragment : Fragment(), View.OnClickListener { // Fragment()는 클래스 상속, View.OnClickListener는 인터페이스 구현
lateinit var binding: FragmentSelectionBinding
lateinit var navController: NavController
// TODO: Rename and change types of parameters
private var param1: String? = null
private var param2: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = FragmentSelectionBinding.inflate(inflater, container, false)
return binding.root
// Inflate the layout for this fragment
// return inflater.inflate(R.layout.fragment_selection, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
navController = Navigation.findNavController(view)
// 리스너 달아주기
binding.btnBack.setOnClickListener(this)
binding.option1.setOnClickListener(this)
binding.option2.setOnClickListener(this)
binding.option3.setOnClickListener(this)
binding.option4.setOnClickListener(this)
}
override fun onClick(v: View?) {
when(v?.id) {
R.id.option_1 -> {navigateWithIndex(1)}
R.id.option_2 -> {navigateWithIndex(2)}
R.id.option_3 -> {navigateWithIndex(3)}
R.id.option_4 -> {navigateWithIndex(4)}
R.id.btn_back -> {navController.popBackStack()} // 뛰로가기
}
}
fun navigateWithIndex(index: Int) {
val bundle = bundleOf("index" to index) // key, value
navController.navigate(R.id.action_selectionFragment_to_resultFragment, bundle)
}
companion object {
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param param1 Parameter 1.
* @param param2 Parameter 2.
* @return A new instance of fragment SelectionFragment.
*/
// TODO: Rename and change types and number of parameters
@JvmStatic
fun newInstance(param1: String, param2: String) =
SelectionFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
}
}
Navigation 에는 백스택 관리도 자동적으로 해줘서, 뒤로가기는 popBackStack() 메서드를 사용해주면 된다.
원래는 프래그먼트를 등록하고 백스택도 관리해줘야하는데 이걸 알아서 관리해준다.
프래그먼트를 통해 뒤로가기를 눌러서 프래그먼트를 삭제하여 이전 프래그먼트로 깔고 싶으면 백스택을 등록하는 등 수동으로 해줘야 하는데 Navigation에는 이러한 동작을 다 자동으로 해준다. 그래서 편리한 것이다.
ResultFragment.kt
마지막으로, 데이터를 받아서 사용해주는 프래그먼트이다.
package made.by.hevton.love.fragment
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.navigation.NavController
import androidx.navigation.Navigation
import made.by.hevton.love.R
import made.by.hevton.love.databinding.FragmentResultBinding
// TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2"
/**
* A simple [Fragment] subclass.
* Use the [ResultFragment.newInstance] factory method to
* create an instance of this fragment.
*/
class ResultFragment : Fragment() {
lateinit var binding: FragmentResultBinding
lateinit var navController: NavController
var option = -1
// TODO: Rename and change types of parameters
private var param1: String? = null
private var param2: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
param1 = it.getString(ARG_PARAM1)
param2 = it.getString(ARG_PARAM2)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
option = arguments?.getInt("index")?:-1 // 앨비스 연산자 사용
binding = FragmentResultBinding.inflate(inflater, container, false)
return binding.root
// Inflate the layout for this fragment
// return inflater.inflate(R.layout.fragment_result, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
navController = Navigation.findNavController(view)
setData()
}
fun setData() {
binding.result.text = "SELECTED : ${option}"
}
companion object {
/**
* Use this factory method to create a new instance of
* this fragment using the provided parameters.
*
* @param param1 Parameter 1.
* @param param2 Parameter 2.
* @return A new instance of fragment ResultFragment.
*/
// TODO: Rename and change types and number of parameters
@JvmStatic
fun newInstance(param1: String, param2: String) =
ResultFragment().apply {
arguments = Bundle().apply {
putString(ARG_PARAM1, param1)
putString(ARG_PARAM2, param2)
}
}
}
}
ResultFragment에는 앨비스 연산자를 사용했다.
arguments?.getInt("index")?:-1
arguments?.getInt("index") 를 하게되면, arguments가 null일 경우 전체가 그냥 null로 반환된다.
그럼 option이 null을 받게 되므로, 이를 방지하기 위해
arguments가 null일 경우 전체가 null로 반한될 때, -1을 반환하도록 해준다.
앨비스 연산자는, 좌항이 null이면 우항을 반환하고
좌항이 null이 아니면 좌항을 반환한다.
이렇게 하면, 구현이 모두 마무리되었다!
정상적으로 동작한다.
이번 글에서는 화면 이동을 할 때 데이터 교환이 필요할 경우 bundle 객체를 이용해서 데이터를 넘겨줬다.
하지만 프래그먼트 간 데이터 교환을 위해 ViewModel을 이용하는 방법도 있다. 이는 다음에 소개한다.
이번 글에서는 Navigation을 이용한 예제를 살펴봤습니다.
고생하셨습니다 끄읏!
'[Android]' 카테고리의 다른 글
Paging3 + Room 이틀동안 삽질 후 성공 정리 (0) | 2023.06.24 |
---|---|
뒤로가기 두 번으로 앱 종료시키기 ( OnBackPressedCallback ) (0) | 2023.04.11 |
[공부] 자바와 코틀린의 익명객체 (0) | 2023.01.13 |
안드로이드 Process와 Task [개념편] (1) | 2023.01.12 |
JIT vs AOT (0) | 2023.01.10 |